blob: e4c30a22e7e69b1965a889b8be8a3abef9369c0a [file] [log] [blame]
# Copyright 2020-2021 Efabless Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import re
import sys
import yaml
from typing import Iterable, Optional
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from .get_file_name import get_name # noqa E402
from utils.utils import get_run_path # noqa E402
def debug(*args, **kwargs):
if os.getenv("REPORT_INFRASTRUCTURE_VERBOSE") == "1":
print(*args, **kwargs, file=sys.stderr)
def parse_to_report(
input_log: str, output_report: str, start: str, end: Optional[str] = None
):
"""
Parses a log in the format
START_MARKER
data
END_MARKER
to a report file.
"""
if end is None:
end = f"{start}_end"
log_lines = open(input_log).read().split("\n")
with open(output_report, "w") as f:
started = False
for line in log_lines:
if line.strip() == end:
break
if started:
f.write(line + "\n")
if line.strip() == start:
started = True
if not started:
f.write("SKIPPED!")
class Artifact(object):
def __init__(
self,
run_path: str,
kind: str,
step: str,
filename: str,
find_by_partial_match: bool = False,
):
self.run_path = run_path
self.kind = kind
self.step = step
self.pathname = os.path.join(self.run_path, self.kind, self.step)
self.filename = filename
self.index, self.path = get_name(
self.pathname, self.filename, find_by_partial_match
)
if self.is_valid():
debug(f"Resolved {kind}, {step}, {filename} to {self.path}")
else:
debug(f"Failed to resolve {kind}, {step}, {filename}")
def is_valid(self) -> bool:
valid = os.path.exists(self.path) and os.path.isfile(self.path)
return valid
def get_content(self) -> Optional[str]:
if not self.is_valid():
return None
return open(self.path).read()
def is_logtoreport_valid(self) -> bool:
# >10 bytes is a magic number, yes. It was this way in the script I rewrote and I'm not a fan of shaking beehives.
return self.is_valid() and os.path.getsize(self.path) > 10
def log_to_report(self, report_name: str, start: str, end: Optional[str] = None):
report_path = os.path.join(self.run_path, "reports", self.step, report_name)
if not self.is_logtoreport_valid():
debug(f"{self.step}:{self.filename} not found or empty.")
return
parse_to_report(self.path, report_path, start, end)
def generate_reports(self, *args: Iterable[Iterable[str]]):
if (self.index or "") == "":
self.index = "X"
for report in args:
filename = f"{self.index}-{report[0]}"
start = report[1]
end = None
try:
end = report[2]
except Exception:
pass
self.log_to_report(filename, start, end)
class Report(object):
def __init__(self, design_path, tag, design_name, params, run_path=None):
self.design_path = design_path
self.design_name = design_name
self.tag = tag
self.current_directory = os.path.dirname(__file__)
if run_path is None:
run_path = get_run_path(design=design_path, tag=tag)
self.run_path = run_path
self.configuration = params
self.raw_report = None
self.formatted_report = None
values = (
[
"design",
"design_name",
"config",
"flow_status",
"total_runtime",
"routed_runtime",
"DIEAREA_mm^2",
"CellPer_mm^2",
"OpenDP_Util",
"Peak_Memory_Usage_MB",
"cell_count",
"tritonRoute_violations",
"Short_violations",
"MetSpc_violations",
"OffGrid_violations",
"MinHole_violations",
"Other_violations",
"Magic_violations",
"antenna_violations",
"lvs_total_errors",
"cvc_total_errors",
"klayout_violations",
"wire_length",
"vias",
"wns",
"pl_wns",
"optimized_wns",
"fastroute_wns",
"spef_wns",
"tns",
"pl_tns",
"optimized_tns",
"fastroute_tns",
"spef_tns",
"HPWL",
"routing_layer1_pct",
"routing_layer2_pct",
"routing_layer3_pct",
"routing_layer4_pct",
"routing_layer5_pct",
"routing_layer6_pct",
"wires_count",
"wire_bits",
"public_wires_count",
"public_wire_bits",
"memories_count",
"memory_bits",
"processes_count",
"cells_pre_abc",
"AND",
"DFF",
"NAND",
"NOR",
"OR",
"XOR",
"XNOR",
"MUX",
]
+ (["NOT"] if os.getenv("MORE_METRICS") else [])
+ [
"inputs",
"outputs",
"level",
"EndCaps",
"TapCells",
"Diodes",
"Total_Physical_Cells",
]
)
@classmethod
def get_header(Self):
header = ",".join(Self.values)
return header
def reports_from_logs(self):
rp = self.run_path
basic_set = [
("_sta.rpt", "check_report"),
("_sta.min.rpt", "min_report"),
("_sta.max.rpt", "max_report"),
("_sta.wns.rpt", "wns_report"),
("_sta.tns.rpt", "tns_report"),
("_sta.clock_skew.rpt", "clock_skew"),
]
additional_set = [
("_sta.slew.rpt", "check_slew"),
("_sta.worst_slack.rpt", "worst_slack"),
("_sta.clock_skew.rpt", "clock_skew"),
("_sta.power.rpt", "power_report"),
("_sta.area.rpt", "area_report"),
]
for name, log in [
("cts", Artifact(rp, "logs", "cts", "cts.log")),
("gpl", Artifact(rp, "logs", "placement", "global.log")),
("grt", Artifact(rp, "logs", "routing", "global.log")),
]:
generate_report_args = [
(name + report_postfix, report_locus)
for report_postfix, report_locus in basic_set
]
log.generate_reports(*generate_report_args)
for name, log in [
("syn", Artifact(rp, "logs", "synthesis", "sta.log")),
("cts_rsz", Artifact(rp, "logs", "cts", "resizer.log")),
("pl_rsz", Artifact(rp, "logs", "placement", "resizer.log")),
("rt_rsz", Artifact(rp, "logs", "routing", "resizer.log")),
("rcx", Artifact(rp, "logs", "signoff", "parasitics_sta.log")),
(
"rcx_mca",
Artifact(rp, "logs", "signoff", "parasitics_multi_corner_sta.log"),
),
]:
generate_report_args = [
(name + report_postfix, report_locus)
for report_postfix, report_locus in (basic_set + additional_set)
]
log.generate_reports(*generate_report_args)
def extract_all_values(self):
rp = self.run_path
self.reports_from_logs()
def re_get_last_capture(rx, string):
matches = re.findall(rx, string)
if len(matches) == 0:
return None
return matches[-1]
# Runtime
flow_status = "flow_exceptional_failure"
total_runtime = -1
routed_runtime = -1
try:
runtime_yaml_str = open(os.path.join(rp, "runtime.yaml")).read()
yaml_docs = list(yaml.safe_load_all(runtime_yaml_str))
if len(yaml_docs) != 2:
raise Exception("Attempted to generate report on a non-finalized run.")
routed_info, total_info = yaml_docs[1]
flow_status = total_info["status"]
total_runtime = total_info["runtime_ts"]
routed_runtime = routed_info["runtime_ts"]
except Exception as e:
print(
f"Failed to extract runtime info for {self.design_name}/{self.tag}: {e}",
file=sys.stderr,
)
# Cell Count
cell_count = -1
yosys_report = Artifact(rp, "reports", "synthesis", "synthesis.stat.rpt", True)
yosys_report_content = yosys_report.get_content()
if yosys_report_content is not None:
match = re.search(r"Number of cells:\s*(\d+)", yosys_report_content)
if match is not None:
cell_count = int(match[1])
# Die Area
die_area = -1
placed_def = Artifact(rp, "results", "floorplan", f"{self.design_name}.def")
def_content = placed_def.get_content()
if def_content is not None:
match = re.search(
r"DIEAREA\s*\(\s*(\d+)\s+(\d+)\s*\)\s*\(\s*(\d+)\s+(\d+)\s*\)",
def_content,
)
if match is not None:
lx, ly, ux, uy = (
float(match[1]),
float(match[2]),
float(match[3]),
float(match[4]),
)
die_area = ((ux - lx) / 1000) * ((uy - ly) / 1000)
die_area /= 1000000 # To mm^2
# Cells per micrometer
cells_per_mm = -1
if cell_count != -1 and die_area != -1:
cells_per_mm = cell_count / die_area
# OpenDP Utilization and HPWL
utilization = -1
hpwl = -1 # Half Perimeter Wire Length?
global_placement_log = Artifact(rp, "logs", "placement", "global.log")
global_log_content = global_placement_log.get_content()
if global_log_content is not None:
match = re.search(r"Util\(%\):\s*([\d\.]+)", global_log_content)
if match is not None:
utilization = float(match[1])
match = re_get_last_capture(r"HPWL:\s*([\d\.]+)", global_log_content)
if match is not None:
hpwl = float(match)
# TritonRoute Logged Info Extraction
tr_log = Artifact(rp, "logs", "routing", "detailed.log")
tr_log_content = tr_log.get_content()
tr_memory_peak = -1
tr_violations = -1
wire_length = -1
vias = -1
if tr_log_content is not None:
match = re_get_last_capture(r"peak\s*=\s*([\d\.]+)", tr_log_content)
if match is not None:
tr_memory_peak = float(match)
match = re_get_last_capture(
r"Number of violations\s*=\s*(\d+)", tr_log_content
)
if match is not None:
tr_violations = int(match)
match = re_get_last_capture(
r"Total wire length = ([\d\.]+)\s*\wm", tr_log_content
)
if match is not None:
wire_length = int(match)
match = re_get_last_capture(r"Total number of vias = (\d+)", tr_log_content)
if match is not None:
vias = int(match)
# TritonRoute DRC Extraction
tr_drc = Artifact(rp, "reports", "routing", "detailed.drc")
tr_drc_content = tr_drc.get_content()
other_violations = tr_violations
short_violations = -1
metspc_violations = -1
offgrid_violations = -1
minhole_violations = -1
if tr_drc_content is not None:
short_violations = 0
metspc_violations = 0
offgrid_violations = 0
minhole_violations = 0
for line in tr_drc_content.split("\n"):
if "Short" in line:
short_violations += 1
other_violations -= 1
if "MetSpc" in line:
metspc_violations += 1
other_violations -= 1
if "OffGrid" in line:
offgrid_violations += 1
other_violations -= 1
if "MinHole" in line:
minhole_violations += 1
other_violations -= 1
# Magic Violations
magic_drc = Artifact(rp, "reports", "signoff", "drc.rpt")
magic_drc_content = magic_drc.get_content()
magic_violations = -1
if magic_drc_content is not None:
# Magic DRC Content
match = re.search(r"COUNT:\s*(\d+)", magic_drc_content)
if match is not None:
magic_violations_raw = int(match[1])
# Not really sure why we do this
magic_violations = (magic_violations_raw + 3) // 4
# Klayout DRC Violations
klayout_drc = Artifact(rp, "reports", "signoff", "magic.lydrc", True)
klayout_drc_content = klayout_drc.get_content()
klayout_violations = -1
if klayout_drc_content is not None:
klayout_violations = 0
for line in klayout_violations.split("\n"):
if "<item>" in line:
klayout_violations += 1
# Antenna Violations
arc_antenna_report = Artifact(rp, "reports", "signoff", "antenna.rpt")
aar_content = arc_antenna_report.get_content()
antenna_violations = -1
if aar_content is not None:
match = re.search(r"Number of pins violated:\s*(\d+)", aar_content)
if match is not None:
antenna_violations = int(match[1])
else:
# Old Magic-Based Check: Just Count The Lines
magic_antenna_report = Artifact(
rp, "reports", "routing", "antenna_violators.rpt"
)
mar_content = magic_antenna_report.get_content()
if mar_content is not None:
antenna_violations = len(mar_content.split("\n"))
# STA Report Extractions
def sta_report_extraction(
sta_report_filename: str, filter: str, kind="reports", step="synthesis"
):
value = -1
report = Artifact(rp, kind, step, sta_report_filename)
report_content = report.get_content()
if report_content is not None:
match = re.search(rf"{filter}\s+(-?[\d\.]+)", report_content)
if match is not None:
value = float(match[1])
else:
debug(
f"Didn't find {filter} in {kind}/{step}/{sta_report_filename}"
)
else:
debug(f"Can't find {sta_report_filename}")
return value
wns = sta_report_extraction("syn_sta.wns.rpt", "wns", step="synthesis")
spef_wns = sta_report_extraction("rcx_sta.wns.rpt", "wns", step="routing")
opt_wns = sta_report_extraction("rt_rsz_sta.wns.rpt", "wns", step="routing")
pl_wns = sta_report_extraction(
"global.log", "wns", kind="logs", step="placement"
)
fr_wns = sta_report_extraction("global.log", "wns", kind="logs", step="routing")
tns = sta_report_extraction("syn_sta.tns.rpt", "tns", step="synthesis")
spef_tns = sta_report_extraction("rcx_sta.tns.rpt", "tns", step="routing")
opt_tns = sta_report_extraction("rt_rsz_sta.tns.rpt", "tns", step="routing")
pl_tns = sta_report_extraction(
"global.log", "tns", kind="logs", step="placement"
)
fr_tns = sta_report_extraction("global.log", "tns", kind="logs", step="routing")
# Yosys Metrics
yosys_metrics = [
"Number of wires:",
"Number of wire bits:",
"Number of public wires:",
"Number of public wire bits:",
"Number of memories:",
"Number of memory bits:",
"Number of processes:",
"Number of cells:",
"$_AND_",
"$_DFF_",
"$_NAND_",
"$_NOR_",
"$_OR_",
"$_XOR_",
"$_XNOR_",
"$_MUX_",
]
if os.getenv("MORE_METRICS"):
yosys_metrics.append("$_NOT_")
yosys_log = Artifact(rp, "logs", "synthesis", "synthesis.log")
yosys_log_content = yosys_log.get_content()
yosys_metrics_values = []
for metric in yosys_metrics:
metric_value = -1
if yosys_log_content is not None:
metric_value = 0
metric_name_escaped = re.escape(metric)
if metric == "$_DFF_":
metric_name_escaped = r"\$_DFF_(?:\w+)?"
match = re.search(rf"{metric_name_escaped}\s+(\d+)", yosys_log_content)
if match is not None:
metric_value = int(match[1])
yosys_metrics_values.append(metric_value)
# ABC Info
abc_i = -1
abc_o = -1
abc_level = -1
if yosys_log_content is not None:
match = re.search(
r"ABC:\s*netlist\s*:\s*i\/o\s*=\s*(\d+)\/\s*(\d+)\s+lat\s*=\s*(\d+)\s+nd\s*=\s*(\d+)\s*edge\s*=\s*(\d+)\s*area\s*=\s*([\d\.]+)\s+delay\s*=\s*([\d\.]+)\s*lev\s*=\s*(\d+)",
yosys_log_content,
)
if match is not None:
abc_i = match[1]
abc_o = match[2]
# We don't use most of the ones in the middle.
abc_level = match[8]
# Fastroute Layer Usage Percentages
routing_log = Artifact(rp, "logs", "routing", "global.log")
routing_log_content = routing_log.get_content()
# MAGIC NUMBER ALERT
layer_usage = [-1] * 6
if routing_log_content is not None:
routing_log_lines = routing_log_content.split("\n")
final_congestion_report_start_line = None
for i, line in enumerate(routing_log_lines):
if "Final congestion report" in line:
final_congestion_report_start_line = i
break
if final_congestion_report_start_line is not None:
start = final_congestion_report_start_line
header = start + 1
separator = header + 1
layer_start = separator + 1
for i in range(6):
line = routing_log_lines[layer_start + i]
match = re.search(r"([\d\.]+)%", line)
if match is not None:
layer_usage[i] = float(match[1])
# Process Filler Cells
# Also includes endcap info
tapcell_log = Artifact(rp, "logs", "floorplan", "tap.log")
tapcell_log_content = tapcell_log.get_content()
diode_log = Artifact(rp, "logs", "routing", "diodes.log")
diode_log_content = diode_log.get_content()
tapcells, endcaps, diodes = 0, 0, 0
if tapcell_log_content is not None:
match = re.search(r"Inserted (\d+) end\s*caps\.", tapcell_log_content)
if match is not None:
endcaps = int(match[1])
match = re.search(r"Inserted (\d+) tap\s*cells\.", tapcell_log_content)
if match is not None:
tapcells = int(match[1])
if diode_log_content is not None:
match = None
if "inserted!" in diode_log_content:
match = re.search(r"(\d+)\s+of\s+.+?\s+inserted!", diode_log_content)
else:
match = re.search(r"(\d+)\s+diodes\s+inserted\.", diode_log_content)
if match is not None:
diodes = int(match[1])
filler_cells = tapcells + endcaps + diodes
# LVS Total Errors
lvs_report = Artifact(rp, "logs", "signoff", f"{self.design_name}.lvs.lef.log")
lvs_report_content = lvs_report.get_content()
lvs_total_errors = -1
if lvs_report_content is not None:
lvs_total_errors = 0
match = re.search(r"Total errors\s*=\s*(\d+)", lvs_report_content)
if match is not None:
lvs_total_errors = int(match[1])
# CVC Total Errors
cvc_log = Artifact(rp, "logs", "signoff", "erc_screen.log")
cvc_log_content = cvc_log.get_content()
cvc_total_errors = -1
if cvc_log_content is not None:
match = re.search(r"CVC:\s*Total:\s*(\d+)", cvc_log_content)
if match is not None:
cvc_total_errors = int(match[1])
return [
flow_status,
total_runtime,
routed_runtime,
die_area,
cells_per_mm,
utilization,
tr_memory_peak,
cell_count,
tr_violations,
short_violations,
metspc_violations,
offgrid_violations,
minhole_violations,
other_violations,
magic_violations,
antenna_violations,
lvs_total_errors,
cvc_total_errors,
klayout_violations,
wire_length,
vias,
wns,
pl_wns,
opt_wns,
fr_wns,
spef_wns,
tns,
pl_tns,
opt_tns,
fr_tns,
spef_tns,
hpwl,
*layer_usage,
*yosys_metrics_values,
abc_i,
abc_o,
abc_level,
endcaps,
tapcells,
diodes,
filler_cells,
]
def get_report(self):
row = [
self.design_path,
self.design_name,
self.tag,
*self.extract_all_values(),
*self.configuration,
]
return ",".join([f"{cell}" for cell in row])