diff --git a/MODULE.bazel b/MODULE.bazel index f828679d3f..a36173378f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -101,7 +101,7 @@ bazel_dep(name = "bazel-orfs", version = "0.0.1") bazel_dep(name = "bazel-orfs-verilog", dev_dependency = True) -BAZEL_ORFS_COMMIT = "6ebadeb4be5c9ada103081c9a5e668c014126616" +BAZEL_ORFS_COMMIT = "3342596c1f7017b9fda5b4a55a25a54ff2aaedf8" BAZEL_ORFS_REMOTE = "https://github.com/The-OpenROAD-Project/bazel-orfs.git" diff --git a/flow/designs/BUILD b/flow/designs/BUILD index 0fe28bc544..c11384ea63 100644 --- a/flow/designs/BUILD +++ b/flow/designs/BUILD @@ -1 +1,12 @@ exports_files(["BUILD"]) + +# Local TL;DR of https://dashboard.precisioninno.com/: a GitHub-markdown +# status table over the forced-engine OpenROAD-SYN QoR tests already +# built in this workspace (see flow/designs/design.bzl). Reads +# bazel-testlogs only — builds nothing, instant. +# +# bazelisk run //flow/designs:syn-dashboard +alias( + name = "syn-dashboard", + actual = "//flow/util:synDashboard", +) diff --git a/flow/designs/README.md b/flow/designs/README.md new file mode 100644 index 0000000000..84cde748d3 --- /dev/null +++ b/flow/designs/README.md @@ -0,0 +1,72 @@ +# Fast local OpenROAD-SYN testing loop + +The maintainers' QoR signal comes from Jenkins runs published at +. The targets described here are +its local counterpart: the same designs, gated by the same +`rules-base.json` files with the same `checkMetadata.py`, but built +locally through Bazel so the loop can be sharpened onto exactly the +design, engine, and stage being worked on — fast enough for an agent +(e.g. Claude) to iterate against. + +Every design gets manual forced-engine flow variants (see +`design.bzl`), regardless of the engine its config.mk selects: + +- `_syn_*` pins `SYNTH_USE_SYN=1` (OpenROAD-SYN), so any design + can be tested under SYN before its config.mk officially switches. +- `_yosys_*` pins the yosys engine, so the baseline stays + reproducible after a design switches (asap7/gcd today). + +Designs with a `rules-base.json` get two QoR tiers per variant: + +- `__synth_test` — the fast tier: synthesis only, + checked against the `synth__`/`constraints__` subset of the rules. + Minutes. QoR-only — without LEC it cannot prove correctness, so use + it to iterate, not to conclude. +- `__test` — the full flow against all rules: the real, + dashboard-equivalent gate. It reuses the fast tier's synthesis + action, so escalating from fast to full never repeats synthesis. + +## The loop + +Build/test whatever is relevant to what you are changing — one design, +one stage, one tier, or a whole suite: + +```sh +# fastest signal for one design +bazelisk test //flow/designs/asap7/jpeg:jpeg_encoder_syn_synth_test + +# the real gate for that design +bazelisk test //flow/designs/asap7/jpeg:jpeg_encoder_syn_test + +# a single stage, for triage +bazelisk build //flow/designs/asap7/jpeg:jpeg_encoder_syn_route + +# yosys A/B on an officially-SYN design +bazelisk test //flow/designs/asap7/gcd:gcd_yosys_test + +# suite-wide status (asap7): fast tier, then the full flows +bazelisk test --keep_going //flow/designs/asap7:syn_synth_test +bazelisk test --keep_going //flow/designs/asap7:syn_test +``` + +Then display what you have — deliberately a separate concern from +deciding what to test or build: + +```sh +bazelisk run //flow/designs:syn-dashboard +``` + +It prints a GitHub-markdown TL;DR table of every forced-engine test +result already in `bazel-testlogs` — pass/fail per tier and engine, +failing rule fields, result age — without building anything. Paste it +into a PR or issue as-is. Because the display reads whatever exists, +the loop stays fast: iterate on the one target you care about, and the +dashboard aggregates across everything you (or earlier sessions) have +built so far. + +The suites include designs that fail OpenROAD-SYN today on purpose — +they are a status signal, not a green-only gate. A build failure (e.g. +synthesis dies) records no test result at all, so run the suites with +`--keep_going` and check their output for `FAILED TO BUILD`; designs +too slow for the suites are listed in `SYN_SLOW_DESIGNS` +(`asap7/BUILD`) and reproduced individually. diff --git a/flow/designs/asap7/BUILD b/flow/designs/asap7/BUILD new file mode 100644 index 0000000000..20005b4141 --- /dev/null +++ b/flow/designs/asap7/BUILD @@ -0,0 +1,111 @@ +# OpenROAD-SYN test suites for asap7 — the local counterpart of the +# Jenkins QoR signal (https://dashboard.precisioninno.com/). +# +# Every design gets manual forced-engine flow variants from the +# config.mk DSL (see flow/designs/design.bzl): _syn_* pins +# SYNTH_USE_SYN=1 and _yosys_* pins the yosys engine, regardless +# of the config.mk default. Designs with a rules-base.json get two QoR +# gates per variant, both checking the design's regular rules file with +# the regular checkMetadata.py — congruent with what Jenkins measures: +# +# bazelisk test --keep_going //flow/designs/asap7:syn_synth_test +# Fast tier: synthesis only, checks the synth__/constraints__ subset +# of rules-base.json. Minutes-scale; QoR-only (no LEC), so it cannot +# prove correctness — use it to iterate, not to conclude. +# +# bazelisk test --keep_going //flow/designs/asap7:syn_test +# Full flow, all rules-base.json gates. The real signal; shares its +# synthesis action with the fast tier. +# +# Sharpening the loop on one design or stage: +# +# bazelisk test //flow/designs/asap7/jpeg:jpeg_encoder_syn_synth_test +# bazelisk test //flow/designs/asap7/jpeg:jpeg_encoder_syn_test +# bazelisk build //flow/designs/asap7/jpeg:jpeg_encoder_syn_route +# bazelisk test //flow/designs/asap7/gcd:gcd_yosys_test # yosys A/B +# +# Both suites deliberately include designs that fail OpenROAD-SYN today +# — they are a status signal, not a green-only gate. A design can fail +# by not synthesizing at all, or by missing rules-base.json gates that +# its regular flow meets. + +# Design directory -> DESIGN_NAME (bazel target prefix), ordered +# fastest-first so --keep_going runs surface quick failures early. +# Known failures / quirks are listed last. "minimal" is excluded: it +# has no VERILOG_FILES and thus no generated design targets. +# +# Known OpenROAD-SYN failures today: +# - mock-alu: SDC references yosys-style register names (*io_out_REG*) +# absent from the SYN netlist. +# - riscv32i: slang rejects procedural assignment to nets (dmem.v). +# - mock-cpu: slang rejects endianness-mismatched part-select +# (wptr_full.v). +# - aes-block: hierarchical BLOCKS; synthesizes, but SYN area blows past +# the yosys-derived rules-base.json gate. +SYN_TEST_DESIGNS = { + "gcd": "gcd", # smoketest, fast + "uart": "uart", + "mock-alu": "MockAlu", + "riscv32i": "riscv_top", + "aes": "aes_cipher_top", + "aes_lvt": "aes_cipher_top", + "aes-mbff": "aes_cipher_top", + "jpeg": "jpeg_encoder", + "jpeg_lvt": "jpeg_encoder", + "ethmac": "ethmac", + "ethmac_lvt": "ethmac", + "mock-cpu": "mock_cpu", + "gcd-ccs": "gcd", # CCS liberty + "aes-block": "aes_cipher_top", # hierarchical BLOCKS +} + +# Designs whose OpenROAD-SYN run is too slow (or does not terminate) for +# the suites. Their generated per-design targets stay tags=["manual"] +# like all forced-engine targets, but they are deliberately left out of +# the suites so those remain runnable; reproduce individually, e.g. +# bazelisk build //flow/designs/asap7/ibex:ibex_core_syn_synth +# +# - ibex: sv_elaborate/synthesize/repair_design complete in seconds, but +# report_metrics ("Report metrics stage 1, synth...") spins in STA for +# hours (observed >2h on 48 threads with no progress). +# - cva6: synthesize itself takes ~73s, but repair_design -pre_placement +# was still running after 10+ minutes when the status sweep was cut +# off; not yet characterized to completion. +# - swerv_wrapper: largest design in the suite; not yet run to +# completion under OpenROAD-SYN in this environment. +# - riscv32i-mock-sram: hierarchical BLOCKS (fakeram) — the syn variant +# first needs the block's full PnR flow for its abstract; not yet run +# to completion. +SYN_SLOW_DESIGNS = { + "cva6": "cva6", + "ibex": "ibex_core", + "riscv32i-mock-sram": "riscv_top", + "swerv_wrapper": "swerv_wrapper", +} + +# Explicitly listed manual tests DO run through a test_suite (the manual +# tag only exempts them from wildcard expansion); the suites themselves +# are manual so `bazelisk test //flow/...` does not pull them in either. +test_suite( + name = "syn_test", + tags = ["manual"], + tests = [ + "//flow/designs/asap7/{pkg}:{name}_syn_test".format( + name = name, + pkg = pkg, + ) + for pkg, name in SYN_TEST_DESIGNS.items() + ], +) + +test_suite( + name = "syn_synth_test", + tags = ["manual"], + tests = [ + "//flow/designs/asap7/{pkg}:{name}_syn_synth_test".format( + name = name, + pkg = pkg, + ) + for pkg, name in SYN_TEST_DESIGNS.items() + ], +) diff --git a/flow/designs/asap7/gcd/BUILD b/flow/designs/asap7/gcd/BUILD index b8c3ddc518..9b63695b82 100644 --- a/flow/designs/asap7/gcd/BUILD +++ b/flow/designs/asap7/gcd/BUILD @@ -3,13 +3,13 @@ load("@orfs_designs//:designs.bzl", "DESIGNS") load("@rules_shell//shell:sh_test.bzl", "sh_test") load("//flow/designs:design.bzl", "design") -# SYNTH_USE_SYN is make-only for now: bazel-orfs's synth stage always -# runs the yosys flow and neither stages the Verilog sources nor sets -# VERILOG_FILES for the OpenROAD synthesis step, so the built-in -# synthesizer opt-in must not reach the bazel arguments. +# gcd's regular flow follows config.mk onto OpenROAD-SYN +# (SYNTH_USE_SYN=1). The single-process comparison below deliberately +# exercises the yosys netlist path (single_flow.tcl seeds from +# 1_2_yosys.v), so it consumes the forced-yosys gcd_yosys_* variant the +# config.mk DSL emits for every design (see flow/designs/design.bzl). design( config = "config.mk", - local_arguments = ["SYNTH_USE_SYN"], ) # Stage-boundary .odb/.sdc stems shared by the normal per-stage flow @@ -56,7 +56,7 @@ SINGLE_FLOW_EXTRA_OUTS = [ # this package. orfs_run( name = "gcd_single_flow", - src = ":gcd_synth", + src = ":gcd_yosys_synth", outs = [ "results/asap7/gcd/single/{}.{}".format(stem, ext) for stem in SINGLE_FLOW_STAGES.values() @@ -79,11 +79,13 @@ orfs_run( variant = "single", ) -# Normal-flow stage outputs, one filegroup per compared file. +# Normal-flow stage outputs, one filegroup per compared file. Taken from +# the yosys-engine variant above so both sides of the byte-compare start +# from the same yosys netlist. [ filegroup( name = "gcd_base_{}_{}".format(stage, ext), - srcs = [":gcd_" + stage], + srcs = [":gcd_yosys_" + stage], output_group = "{}.{}".format(stem, ext), ) for stage, stem in SINGLE_FLOW_STAGES.items() diff --git a/flow/designs/design.bzl b/flow/designs/design.bzl index b4efc0b1d0..c317e34b9a 100644 --- a/flow/designs/design.bzl +++ b/flow/designs/design.bzl @@ -1,5 +1,6 @@ """BUILD boilerplate for flow/designs/.""" +load("@bazel-orfs//:openroad.bzl", "orfs_flow") load("@orfs_designs//:designs.bzl", "orfs_design") # Per filegroup target: extensions included in the filegroup. @@ -51,6 +52,65 @@ def _export_design_files(): visibility = ["//visibility:private"], ) +def _engine_variant_targets( + name, + platform, + verilog_files, + arguments, + user_arguments, + sources, + user_sources, + macros, + stage_data, + tags): # buildifier: disable=unused-variable + """Forced-engine flow variants for one design (orfs_design extra hook). + + Emits, always tagged "manual" (even for CI designs, so only explicit + invocations such as //flow/designs/asap7:syn_test run them), two full + flows that pin the synthesis engine regardless of the config.mk + default: + + - variant "syn": SYNTH_USE_SYN=1 (OpenROAD-SYN) — lets any design be + tested under OpenROAD-SYN before its config.mk officially switches. + - variant "yosys": SYNTH_USE_SYN=0 — keeps the yosys baseline + reproducible after a design officially switches (e.g. asap7/gcd). + + When the design has a rules-base.json each variant gets the same QoR + gates as the regular flow, congruent with the Jenkins/dashboard + signal because they check the same rules file with the same script: + + - __test: the full flow gated on all of rules-base.json + (the real, dashboard-equivalent gate). + - __synth_test: the fast tier — synthesis plus a check + of only the synth__/constraints__ subset of rules-base.json. + QoR-only (no LEC), but a minutes-scale iteration signal that shares + its synth action with the full-flow variant. + + The generated __update targets must NOT be run: they + would overwrite the design's rules-base.json with the forced engine's + numbers. + """ + for engine, use_syn in [("syn", "1"), ("yosys", "0")]: + orfs_flow( + name = name, + verilog_files = verilog_files, + pdk = "//flow:" + platform, + arguments = arguments | {"SYNTH_USE_SYN": use_syn}, + user_arguments = user_arguments, + # sources carries the auto-detected rules-base.json as + # RULES_JSON, which gates the variant's tests. + sources = sources, + user_sources = user_sources, + macros = macros, + stage_data = stage_data, + variant = engine, + tags = ["manual"], + test_kwargs = {"tags": ["manual"]}, + # Sibling packages consume stage outputs (e.g. asap7/gcd's + # single-process comparison seeds from the yosys netlist). + visibility = ["//visibility:public"], + ) + def design(config = "config.mk", user_arguments = [], user_sources = [], local_arguments = []): """Standard BUILD body for flow/designs///. @@ -75,6 +135,7 @@ def design(config = "config.mk", user_arguments = [], user_sources = [], local_a user_sources = user_sources, local_arguments = local_arguments, blender = True, + extra = _engine_variant_targets, ) def files(group, extra_srcs = None): diff --git a/flow/scripts/synth_syn.tcl b/flow/scripts/synth_syn.tcl index efe1f7779b..bd6eaf0820 100644 --- a/flow/scripts/synth_syn.tcl +++ b/flow/scripts/synth_syn.tcl @@ -75,3 +75,12 @@ orfs_write_db $::env(RESULTS_DIR)/1_synth.odb # out by OpenSTA that has no dependencies. Sole writer of # 1_synth.sdc. orfs_write_sdc $::env(RESULTS_DIR)/1_synth.sdc + +# Gate-level netlist for LEC (write_lec_verilog strips physical-only +# masters, matching the CTS-stage LEC convention) and any other netlist +# consumer. The Bazel synthesis action declares this file as an output, +# so it must be written unconditionally. write_lec_verilog takes a bare +# filename and prefixes $::env(RESULTS_DIR) itself, like the cts.tcl +# call sites. +source $::env(SCRIPTS_DIR)/lec_check.tcl +write_lec_verilog 1_synth.v diff --git a/flow/util/BUILD b/flow/util/BUILD index cc21b559c6..a8e1092dbb 100644 --- a/flow/util/BUILD +++ b/flow/util/BUILD @@ -1,4 +1,4 @@ -load("@rules_python//python:defs.bzl", "py_library", "py_test") +load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") load("@rules_python//python:pip.bzl", "compile_pip_requirements") exports_files(["open_plots.sh"]) @@ -33,6 +33,14 @@ filegroup( visibility = ["//visibility:public"], ) +# TL;DR markdown dashboard over locally built OpenROAD-SYN QoR tests; +# run via the //flow/designs:syn-dashboard alias. +py_binary( + name = "synDashboard", + srcs = ["synDashboard.py"], + visibility = ["//flow/designs:__pkg__"], +) + py_library( name = "genMetrics_lib", srcs = ["genMetrics.py"], diff --git a/flow/util/checkMetadata.py b/flow/util/checkMetadata.py index 1a525d88a8..71c25425df 100755 --- a/flow/util/checkMetadata.py +++ b/flow/util/checkMetadata.py @@ -33,6 +33,14 @@ ) parser.add_argument("--metadata", "-m", required=True, help="The metadata file") parser.add_argument("--rules", "-r", required=True, nargs="+", help="The rules file") +parser.add_argument( + "--only-prefix", + nargs="+", + default=None, + help="Check only rules whose field starts with one of these prefixes. " + "Lets a partial run (e.g. synthesis-only) be gated by a full-flow " + "rules file without tripping the missing-field error.", +) args = parser.parse_args() with open(args.metadata) as metadataFile: @@ -46,6 +54,13 @@ else: print(f"[WARN] File {filePath} not found") +if args.only_prefix: + rules = { + field: rule + for field, rule in rules.items() + if any(field.startswith(prefix) for prefix in args.only_prefix) + } + if len(rules) == 0: print("No rules") sys.exit(1) diff --git a/flow/util/genMetrics.py b/flow/util/genMetrics.py index d845a4e065..9f20e89b16 100755 --- a/flow/util/genMetrics.py +++ b/flow/util/genMetrics.py @@ -192,9 +192,12 @@ def git_head_commit(git_exe, folder): def file_sha1(path): - """SHA-1 of `path`, or "N/A" if absent. Read in chunks so large + """SHA-1 of `path`, or "N/A" if absent or empty. Empty counts as + absent so Bazel's touched placeholder files (e.g. the canonicalize + RTLIL under SYNTH_USE_SYN or SYNTH_NETLIST_FILES) hash the same as + a make run that never wrote the file. Read in chunks so large netlists don't blow the heap.""" - if not os.path.isfile(path): + if not os.path.isfile(path) or os.path.getsize(path) == 0: return "N/A" hasher = hashlib.sha1() with open(path, "rb") as f: @@ -258,7 +261,13 @@ def extract_metrics( # Clocks # ========================================================================= - clk_list = read_sdc(resultPath + "/2_floorplan.sdc") + # Prefer the floorplan SDC; on a synthesis-only tree (e.g. the "syn" + # flow variant stops after 1_synth) fall back to the canonicalized + # synthesis SDC, which carries the same create_clock lines. + sdc_file = resultPath + "/2_floorplan.sdc" + if not os.path.isfile(sdc_file): + sdc_file = resultPath + "/1_synth.sdc" + clk_list = read_sdc(sdc_file) metrics_dict["constraints__clocks__count"] = len(clk_list) metrics_dict["constraints__clocks__details"] = clk_list @@ -277,22 +286,26 @@ def extract_metrics( # Global Route # ========================================================================= merge_jsons(logPath, metrics_dict, "5_*.json") - extractTagFromFile( - "globalroute__timing__clock__slack", - metrics_dict, - "^\\[INFO FLW-....\\] Clock .* slack (\\S+)", - logPath + "/5_1_grt.log", - ) + # Guarded so a synthesis-only tree doesn't pollute the metadata with + # "ERR" values for stages that never ran. + if os.path.isfile(logPath + "/5_1_grt.log"): + extractTagFromFile( + "globalroute__timing__clock__slack", + metrics_dict, + "^\\[INFO FLW-....\\] Clock .* slack (\\S+)", + logPath + "/5_1_grt.log", + ) # Finish # ========================================================================= merge_jsons(logPath, metrics_dict, "6_*.json") - extractTagFromFile( - "finish__timing__wns_percent_delay", - metrics_dict, - baseRegEx.format("finish slack div critical path delay", "(\\S+)"), - rptPath + "/6_finish.rpt", - ) + if os.path.isfile(rptPath + "/6_finish.rpt"): + extractTagFromFile( + "finish__timing__wns_percent_delay", + metrics_dict, + baseRegEx.format("finish slack div critical path delay", "(\\S+)"), + rptPath + "/6_finish.rpt", + ) extractGnuTime("finish", metrics_dict, logPath + "/6_report.log") diff --git a/flow/util/synDashboard.py b/flow/util/synDashboard.py new file mode 100644 index 0000000000..6204e8f4cb --- /dev/null +++ b/flow/util/synDashboard.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 + +"""TL;DR dashboard over locally built OpenROAD-SYN QoR test results. + +The local counterpart of https://dashboard.precisioninno.com/: scans +bazel-testlogs for the forced-engine QoR tests emitted by +flow/designs/design.bzl (_{syn,yosys}[_synth]_test) and prints a +GitHub-flavored markdown status table, so the output can be pasted +into a PR or issue as-is. Reads only what previous `bazelisk test` +runs left behind — it never builds anything, so it is instant. + +Run it via: + bazelisk run //flow/designs:syn-dashboard +""" + +import os +import re +import sys +import time +import xml.etree.ElementTree as ET +from pathlib import Path + +# _{syn,yosys}[_synth]_test, as emitted by design.bzl's +# forced-engine variants. "_synth" marks the fast synthesis-only tier. +TARGET_RE = re.compile(r"^(?P.+)_(?Psyn|yosys)(?P_synth)?_test$") + +FAIL_RULE_RE = re.compile(r"^\[ERROR\] (\S+) fail test: (.*)$") +MISSING_RULE_RE = re.compile(r"^\[ERROR\] Value not found for (\S+)\.$") + +# (engine, fast) -> table column, in display order. +COLUMNS = [ + ("syn", True, "SYN synth"), + ("syn", False, "SYN flow"), + ("yosys", True, "yosys synth"), + ("yosys", False, "yosys flow"), +] + + +def workspace_root(): + """The bazel workspace root: BUILD_WORKSPACE_DIRECTORY under + `bazel run`, else the nearest ancestor holding MODULE.bazel.""" + env = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + if env: + return Path(env) + for path in [Path.cwd()] + list(Path.cwd().parents): + if (path / "MODULE.bazel").is_file(): + return path + sys.exit("error: not inside a bazel workspace (no MODULE.bazel found)") + + +def test_status(test_dir): + """(passed, mtime) for one bazel-testlogs test dir, or None if the + test never produced a result. The test.xml duration is just the + metadata-check action, not the flow build, so it is not reported.""" + xml_file = test_dir / "test.xml" + if not xml_file.is_file(): + return None + try: + root = ET.parse(xml_file).getroot() + failures = 0 + for suite in root.iter("testsuite"): + failures += int(suite.get("failures") or 0) + int(suite.get("errors") or 0) + except (ET.ParseError, ValueError, TypeError): + # A corrupt test.xml is "no result", not a pass — swallowing the + # error inside the sum would render a green cell for it. + return None + return failures == 0, xml_file.stat().st_mtime + + +def failing_rules(test_dir): + """Failed rule fields harvested from checkMetadata.py output in + test.log.""" + log_file = test_dir / "test.log" + if not log_file.is_file(): + return [] + fields = [] + for line in log_file.read_text(encoding="utf-8", errors="replace").splitlines(): + match = FAIL_RULE_RE.match(line) + if match: + fields.append(match.group(1)) + continue + match = MISSING_RULE_RE.match(line) + if match: + fields.append(match.group(1) + " (missing)") + return fields + + +def age(mtime): + seconds = max(0, time.time() - mtime) + if seconds >= 86400: + return f"{seconds / 86400:.0f}d ago" + if seconds >= 3600: + return f"{seconds / 3600:.0f}h ago" + if seconds >= 60: + return f"{seconds / 60:.0f}m ago" + return "just now" + + +def collect(root): + """{(platform, design_pkg): {(engine, fast): result}} from + bazel-testlogs, where result adds the failed-rule fields.""" + designs = {} + testlogs = root / "bazel-testlogs" / "flow" / "designs" + if not testlogs.is_dir(): + return designs + for xml_file in testlogs.glob("*/*/*/test.xml"): + target_dir = xml_file.parent + match = TARGET_RE.match(target_dir.name) + if not match: + continue + status = test_status(target_dir) + if status is None: + continue + passed, mtime = status + key = (target_dir.parent.parent.name, target_dir.parent.name) + designs.setdefault(key, {})[ + (match.group("engine"), bool(match.group("fast"))) + ] = { + "passed": passed, + "mtime": mtime, + "failing": [] if passed else failing_rules(target_dir), + } + return designs + + +def format_rules(fields, limit=3): + if len(fields) > limit: + fields = fields[:limit] + [f"+{len(fields) - limit} more"] + return ", ".join(fields) + + +def main(): + designs = collect(workspace_root()) + print("# OpenROAD-SYN local status") + print() + if not designs: + print("No forced-engine test results under bazel-testlogs yet. Build some:") + print() + print(" bazelisk test --keep_going //flow/designs/asap7:syn_synth_test") + print(" bazelisk test --keep_going //flow/designs/asap7:syn_test") + return + header = ( + ["design"] + [title for _, _, title in COLUMNS] + ["failing rules", "last run"] + ) + print("| " + " | ".join(header) + " |") + print("|" + "|".join(["---"] * len(header)) + "|") + for (platform, pkg), results in sorted(designs.items()): + cells = [] + for engine, fast, _ in COLUMNS: + result = results.get((engine, fast)) + if result is None: + cells.append("—") + else: + cells.append("✅" if result["passed"] else "❌") + # The most meaningful failure detail: the full SYN flow if it + # failed, else the fast SYN tier, else any failing variant. + failing = [] + for key in [("syn", False), ("syn", True), ("yosys", False), ("yosys", True)]: + result = results.get(key) + if result and not result["passed"] and result["failing"]: + failing = result["failing"] + break + newest = max(r["mtime"] for r in results.values()) + row = [f"{platform}/{pkg}"] + cells + [format_rules(failing), age(newest)] + print("| " + " | ".join(row) + " |") + print() + print("Each cell reflects its own last `bazelisk test` run; `last run` is the") + print("newest across the row. `—` = no local result: never run, or its build") + print("failed outright (e.g. synthesis died) — bazel records no test result") + print("for build failures, so run the suites with `--keep_going` and check") + print("their output for `FAILED TO BUILD`.") + + +if __name__ == "__main__": + main() diff --git a/flow/util/utils.mk b/flow/util/utils.mk index 0dbeb1ea6e..abd1427484 100644 --- a/flow/util/utils.mk +++ b/flow/util/utils.mk @@ -3,6 +3,19 @@ .PHONY: metadata metadata: finish metadata-generate metadata-check +# Synthesis-only metadata: generate and check QoR after just the synth +# stage, without running the full flow. The synthesis-stage subset of +# the design's regular rules file gates the run, so no per-variant +# rules file needs to be committed. Sequential sub-makes rather than +# prerequisites: each step consumes the previous one's outputs, which +# plain prerequisites would race under make -j. +.PHONY: metadata-synth +metadata-synth: export RULES_JSON = $(DESIGN_DIR)/rules-base.json +metadata-synth: + $(MAKE) synth + $(MAKE) metadata-generate + $(MAKE) metadata-check-synth + .PHONY: metadata-generate metadata-generate: mkdir -p $(REPORTS_DIR) @@ -25,6 +38,16 @@ metadata-check: -r $(RULES_JSON) 2>&1 \ | tee $(abspath $(REPORTS_DIR)/metadata-check.log) +# Check only the synthesis-stage subset of RULES_JSON, so a +# synthesis-only run can be gated by the design's full-flow rules file. +.PHONY: metadata-check-synth +metadata-check-synth: + $(PYTHON_EXE) $(UTILS_DIR)/checkMetadata.py \ + -m $(REPORTS_DIR)/metadata.json \ + -r $(RULES_JSON) \ + --only-prefix synth__ constraints__ 2>&1 \ + | tee $(abspath $(REPORTS_DIR)/metadata-check.log) + .PHONY: clean_metadata clean_metadata: rm -f $(REPORTS_DIR)/design-dir.txt