Skip to content

Timing report HTML generation (integration demo)#9770

Open
oharboe wants to merge 10 commits intoThe-OpenROAD-Project:masterfrom
Pinata-Consulting:timing-html-report
Open

Timing report HTML generation (integration demo)#9770
oharboe wants to merge 10 commits intoThe-OpenROAD-Project:masterfrom
Pinata-Consulting:timing-html-report

Conversation

@oharboe
Copy link
Collaborator

@oharboe oharboe commented Mar 15, 2026

Summary

No more click and wait in timing reports. Run a build overnight and get histogram and timing reports in seconds without having to load timing information.

This is the first demo of a grander vision of static HTML GUI and the Qt GUI being a developer tool and the single source of truth for the HTML GUI.

Self-contained HTML timing reports generated from any ORFS stage, matching the OpenROAD Qt GUI (Charts + Timing Report widgets) as closely as possible without modifying src/sta.

This is an integration demo. Individual features and bug fixes will be broken out into separate PRs. This branch will be force-rebased on top of those changes as they land, serving as a living status report.

image

Usage

cd tools/OpenROAD

# Build timing report for any ORFS stage
bazelisk build //test/orfs/gcd:gcd_synth_timing
bazelisk build //test/orfs/mock-array:MockArray_4x4_base_synth_timing

# Open in browser
bazelisk run //test/orfs/gcd:gcd_synth_timing
bazelisk run //test/orfs/mock-array:MockArray_4x4_base_synth_timing

Adding timing reports to any ORFS design is one line in BUILD:

load("//test/orfs:timing.bzl", "orfs_timing_stages")

orfs_timing_stages(
    name = "gcd",
    stages = ["synth", "floorplan", "place", "cts", "grt", "route"],
    timing_script = "//etc:timing_report",
)

What it produces

A single self-contained HTML file (~300KB for MockArray) with:

  • Endpoint Slack Histogram — nice-bucket algorithm ported from chartsWidget.cpp, red/green bars, dropdowns for path group and clock filtering
  • Timing Report table — columns match Qt TimingPathsModel: Capture Clock, Required, Arrival, Slack, Skew, Logic Delay, Logic Depth, Fanout, Start, End (all in ps)
  • Data Path Details — per-arc Pin, Fanout, ↑/↓, Time, Delay, Slew, Load (matching TimingPathDetailModel)
  • Unconstrained pin count below histogram
  • Draggable sashes between panels

Known Issues

STA search state crash (blocker for C++ extension points)

The C++ Timing::getTimingPaths(), getClockInfo(), and getSlackHistogram() abort when called from the Bazel Python API. The root cause is that STA's internal search state is left in a condition where subsequent calls crash. ensureGraph() + searchPreamble() don't help.

Workaround: timing paths extracted via Tcl report_checks -format json. Histogram bucketing done in JS.

Fix needed: changes to src/sta to properly reset search state between Python API calls.

Histogram buckets differ slightly from Qt

The JS snapInterval algorithm matches chartsWidget.cpp but unconstrained endpoint filtering differs slightly.

See docs/timing_report_todo.md for the full Qt GUI discrepancy list.

An .md file for pull requests?

There's also an .md file with information that could be used in a pull request, but that's a little bit off topic, but here is what it looks like currently. Perhaps drop this and focus on .html in this PR.

PASS Timing — MockArrayasap7 | Setup WNS 0.0000 ns TNS 0.0000 ns | Hold WNS 0.0000 ns

Setup Hold
WNS 0.0000 ns 0.0000 ns
TNS 0.0000 ns 0.0000 ns
Endpoints 5172
Slack distribution (setup):
                     █    ▃     ▃         ▂▆   2120 endpoints
    0.000                            0.000 ns
Clock Domains
Clock Period (ns) Sources
clock 250.0 ``
Cell Type Breakdown (top 10 by delay)
Cell Count Total Delay (ns) Avg Slew (ns)
MockArray 200 0.0000 0.0000

Full interactive report: see 1_timing.html artifact

Test plan

  • bazelisk build //test/orfs/gcd:gcd_synth_timing passes
  • bazelisk build //test/orfs/mock-array:MockArray_4x4_base_synth_timing passes
  • bazelisk run opens HTML in browser with histogram + path table + detail
  • Path group dropdown shows all groups (including empty ones)
  • Clicking histogram bar filters path table
  • Screenshots match Qt GUI layout

🤖 Generated with Claude Code

oharboe and others added 9 commits March 15, 2026 14:22
No GUI can fix the most pressing problem: click and wait. In a GUI,
you've lost before you've started. In the age of Claude, a
self-contained HTML artifact generated as part of the build is
strictly superior: instant, shareable, diffable, CI-native, air-gap
safe.

This commit adds 5 new methods to the ord::Timing class that expose
the same timing data the Qt GUI uses (STAGuiInterface), but as POD
structs accessible from Python via SWIG — no framework inversion,
no Qt dependency:

  - getWorstSlack(MinMax) — WNS
  - getTotalNegativeSlack(MinMax) — TNS
  - getEndpointCount() — number of timing endpoints
  - getEndpointSlackMap(MinMax) — (pin_name, slack) for all
    endpoints (histogram data source, mirrors chartsWidget.cpp)
  - getClockInfo() — clock domain metadata
  - getTimingPaths(MinMax, max_paths, slack_threshold) — full path
    detail with per-arc delay/slew/load/fanout (mirrors
    TimingPathsModel + TimingPathDetailModel from the Qt GUI)

Three new structs (ClockInfo, TimingArcInfo, TimingPathInfo) return
plain-old-data with strings and floats — no opaque STA pointers, no
lifetime issues, trivial JSON serialization.

New Python/Tcl API additions are now free to implement and maintain.
It is policy, not work, whether we expose timing data or not. This
commit demonstrates the pattern with unit tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the timing report pipeline: Python script + Bazel targets
to generate self-contained HTML timing reports and PR-ready markdown
from any ORFS stage.

The HTML replaces Qt GUI timing features in a browser:
- Endpoint slack histogram (click to filter, red/green bars)
- Sortable timing path table with all columns from TimingPathsModel
- Arc waterfall detail (cell/net delay bars, per-arc tooltip)
- Cell type breakdown chart
- Zero external dependencies, works offline

The markdown (<10KB) is optimized for GitHub PR comments:
- Summary table, ASCII histogram, top failing paths
- Auto-pruned to fit 65KB limit

Bazel integration via timing.bzl macro (no bazel-orfs patches needed):
- orfs_timing_stages() creates {name}_{stage}_timing for all stages
- Uses filegroup output_group to extract only ODB+SDC (lightweight)
- 'bazelisk run' opens browser (only depends on HTML, not ODB)

Usage:
  cd tools/OpenROAD
  bazelisk build //test/orfs/gcd:gcd_synth_timing
  bazelisk run //test/orfs/gcd:gcd_synth_timing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous commit used `openroad -python` which doesn't work in
Bazel (no Python compiled in).  This rewrites the pipeline:

- Replace genrule with custom Starlark rule (`_timing_gen`) that
  accesses `PdkInfo.libs` for liberty files and `OrfsInfo` for ODB/SDC
- Use `py_binary` (//etc:timing_report) with //python/openroad dep
- Filter PdkInfo.libs to NLDM FF corner only (loading all corners OOMs)
- Move ClockInfo/TimingArcInfo/TimingPathInfo structs to namespace
  level for SWIG compatibility (nested structs don't get Python attrs)
- Extract timing paths via Tcl `report_checks -format json` (the C++
  getTimingPaths() aborts after getWorstSlack() — STA search state
  reuse bug, documented in docs/timing_api_bugs.md)
- Extract clock info via Tcl `all_clocks` / `get_property`
- Switch HTML theme from dark to white to match OpenROAD
- Add mock-array timing targets (Element + MockArray 4x4_base)

Known bugs documented in docs/timing_api_bugs.md:
- C++ getTimingPaths()/getClockInfo() abort after getWorstSlack()
- Likely fix: call sta->searchPreamble() before findPathEnds()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
- All values in picoseconds (×1e12) matching Qt GUI staToUser()
- Histogram: nice bucket algorithm from chartsWidget.cpp
  (snapInterval with 10 default buckets, red/green bars)
- Path table: columns match Qt TimingPathsModel — Capture Clock,
  Required, Arrival, Slack, Skew, Logic Delay, Logic Depth, Fanout,
  Start, End
- Path detail: Pin (with cell), Fanout, ↑/↓, Time, Delay, Slew, Load
  matching TimingPathDetailModel
- Dropdowns: Setup Slack, No Path Group, All Clocks (from chartsWidget)
- Draggable sash between path table and detail panel
- Extracted timing paths via Tcl report_checks -format json
- Updated TODO with Qt GUI discrepancy audit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Qt source-of-truth comment added to timing_report.py.

Fixes applied (matching chartsWidget.cpp / staGui.cpp behavior):
- Filter unconstrained endpoints (infinite slack) from histogram
- Show "Number of unconstrained pins: N" below histogram
- All path groups from STA (including empty ones like in2out)
- Tooltip format: "Number of Endpoints: N / Interval: [lo, hi) ps"
- Bar colors match Qt: #f08080/#8b0000 neg, #90ee90/The-OpenROAD-Project#6400 pos
- <No clock> for unconstrained paths in Capture Clock column
- Logic Delay column fixed (was showing arrival)
- Tiebreaker sort by endpoint name for equal slack
- Histogram click filter with float precision epsilon
- Double-click histogram to clear filter
- Sash between histogram and timing report panels
- X-axis labels use actual bin interval

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Qt source-of-truth comment added to timing_report.py.

Fixes applied (matching chartsWidget.cpp / staGui.cpp behavior):
- Filter unconstrained endpoints (infinite slack) from histogram
- Show "Number of unconstrained pins: N" below histogram
- All path groups from STA (including empty ones like in2out)
- Tooltip format: "Number of Endpoints: N / Interval: [lo, hi) ps"
- Bar colors match Qt: #f08080/#8b0000 neg, #90ee90/The-OpenROAD-Project#6400 pos
- <No clock> for unconstrained paths in Capture Clock column
- Logic Delay column fixed (was showing arrival)
- Tiebreaker sort by endpoint name for equal slack
- Histogram click filter with float precision epsilon
- Double-click histogram to clear filter
- Sash between histogram and timing report panels
- X-axis labels use actual bin interval

Note: C++ getSlackHistogram() was attempted but crashes due to
the same STA search state issue as getTimingPaths(). The JS
bucketing algorithm (snapInterval) matches the C++ logic but
may produce slightly different results due to float precision.
The C++ approach is saved in git stash for when the STA crash
is resolved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an impressive feature for generating self-contained HTML timing reports. The implementation is comprehensive, covering C++ API extensions, Python scripting for report generation, and Bazel rules for integration. My review focuses on improving the correctness of the generated reports, enhancing robustness, and increasing maintainability. I've identified a few areas for improvement, including the calculation of logic delay, error handling, and configuration management.

Comment on lines +50 to +51
except Exception:
return []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a generic Exception can hide unexpected errors and make debugging more difficult. It's better to catch specific exceptions that you expect to handle, such as json.JSONDecodeError if the file content is not valid JSON, or IOError for file-related issues.

Suggested change
except Exception:
return []
except json.JSONDecodeError as e:
print(f"[timing_report] ERROR: Failed to parse JSON from report_checks output: {e}", file=sys.stderr)
return []

Comment on lines +92 to +97
# Compute logic depth
cells = set()
for a in p["arcs"]:
if not a["net"] and a["cell"]:
cells.add(a["to"])
p["logic_depth"] = len(cells)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic_delay for each path is not being calculated, causing the report to incorrectly display the arrival time instead. You can calculate logic_delay here by summing the delays of the non-net arcs in the path.

After this change, you should also update line 524 in the Javascript template from <td>${{ps(p.logic_delay || p.arrival)}}</td> to <td>${{ps(p.logic_delay)}}</td> to ensure the correct value is displayed.

Suggested change
# Compute logic depth
cells = set()
for a in p["arcs"]:
if not a["net"] and a["cell"]:
cells.add(a["to"])
p["logic_depth"] = len(cells)
# Compute logic depth and logic delay
cells = set()
logic_delay = 0.0
for a in p["arcs"]:
if not a["net"] and a["cell"]:
cells.add(a["to"])
logic_delay += a["delay"]
p["logic_depth"] = len(cells)
p["logic_delay"] = logic_delay

file=sys.stderr)
sys.exit(1)

lib_files = os.environ.get("LIB_FILES", "").split()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the LIB_FILES environment variable is not set or is an empty string, os.environ.get("LIB_FILES", "").split() will result in a list containing an empty string ['']. This could cause issues later when trying to read the liberty file. It's safer to handle the case of an empty string explicitly.

Suggested change
lib_files = os.environ.get("LIB_FILES", "").split()
lib_files_str = os.environ.get("LIB_FILES", "")
lib_files = lib_files_str.split() if lib_files_str else []

Comment on lines +605 to +611
if (!is_net && !pin_is_clock && inst) {
if (logic_insts.find(inst) == logic_insts.end()) {
logic_insts.insert(inst);
logic_depth_count++;
logic_delay_total += pin_delay;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of logic_delay_total includes delays from all instance arcs that are not on a clock path. However, the standard definition of logic delay typically excludes buffers and inverters. To improve accuracy, consider checking if the instance is a buffer or inverter and excluding its delay from logic_delay_total.

You could use design_->getResizer()->isBuffer(lib_cell) and is_inverter for this check, which would require including rsz/Resizer.hh.

Comment on lines +45 to +49
lib_files = [
f
for f in stage[PdkInfo].libs.to_list()
if "NLDM" in f.path and "_FF_" in f.path
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hardcoding the filter for liberty files based on "NLDM" and "_FF_" in the path is brittle and may not work for all PDKs or corners. As noted in your documentation, this should be improved. A more robust solution would be to propagate the correct list of library files from the orfs_flow rule, possibly through a custom provider, rather than relying on string matching within this rule.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

}
}
for (const sta::Pin* pin : clk->pins()) {
info.sources.push_back(network->pathName(pin));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: use emplace_back instead of push_back [modernize-use-emplace]

Suggested change
info.sources.push_back(network->pathName(pin));
info.sources.emplace_back(network->pathName(pin));


const bool is_setup = (minmax == Max);
sta::SceneSeq scenes = sta->scenes();
sta::StringSeq group_names;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "sta::StringSeq" is directly included [misc-include-cleaner]

src/Timing.cc:35:

- #include "sta/TimingArc.hh"
+ #include "sta/StringUtil.hh"
+ #include "sta/TimingArc.hh"

sta::StringSeq group_names;

sta::Search* search = sta->search();
sta::PathEndSeq path_ends = search->findPathEnds(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "sta::PathEndSeq" is directly included [misc-include-cleaner]

src/Timing.cc:35:

- #include "sta/TimingArc.hh"
+ #include "sta/SearchClass.hh"
+ #include "sta/TimingArc.hh"


for (auto& path_end : path_ends) {
TimingPathInfo path_info;
sta::Path* path = path_end->path();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "sta::Path" is directly included [misc-include-cleaner]

src/Timing.cc:27:

- #include "sta/PathEnd.hh"
+ #include "sta/Path.hh"
+ #include "sta/PathEnd.hh"

}
if (node_fanout > max_fanout) {
max_fanout = node_fanout;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: use std::max instead of > [readability-use-std-min-max]

Suggested change
}
max_fanout = std::max(node_fanout, max_fanout);

Qt becomes a developer-only dependency. Python is build-time only.
The static HTML replicates all user-facing GUI features with zero
runtime dependencies. Prioritized feature list from minimum churn
(Tcl-only fixes) to full layout viewer parity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
@oharboe oharboe force-pushed the timing-html-report branch from a9639df to 4e36086 Compare March 15, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant