Skip to content

Feature Proposal: Static HTML Timing Report #10020

@oharboe

Description

@oharboe

Description

Based on commit ce7ee82f49.

Summary

Add a write_timing_html Tcl command that generates a single self-contained HTML file displaying timing analysis — with no running OpenROAD server required. The file can be emailed, attached to GitHub issues, or stored alongside build artifacts for offline review.

Requirements

R1: Zero click-and-wait

All data must be pre-cooked at generation time. Opening the HTML file must immediately display fully rendered timing data with no "Update" button click, no loading spinner, no server round-trip. The user sees results the instant the page loads.

This means:

  • All timing paths (setup + hold) are pre-extracted and embedded
  • Slack histograms are pre-computed (bins, counts, totals)
  • Clock trees are pre-traversed and serialized
  • Chart filter lists (path groups, clocks) are pre-populated
  • No lazy loading — everything is in the HTML file

R2: Timing features that must be supported

Path analysis:

  • Setup timing paths table with columns: Clock, Required, Arrival, Slack, Skew, Logic Delay, Logic Depth, Fanout, Start Pin, End Pin
  • Hold timing paths table (same columns)
  • Per-path detail view: Data Path and Capture Path tabs showing per-node: Pin, Fanout, Rise/Fall, Time, Delay, Slew, Load
  • Tab switching between setup and hold
  • Path selection with keyboard navigation (arrow keys)
  • Sorting paths by slack

Slack histogram:

  • Canvas-rendered histogram of endpoint slack distribution
  • Negative slack bins (red) vs positive slack bins (green)
  • Hover tooltips showing bin range and count
  • Click-to-drill-down: clicking a histogram bar filters the path table to that slack range
  • Setup/Hold tab switching
  • Pre-computed bins for the unfiltered ("All") view

Clock tree:

  • Hierarchical tree visualization with node types (root, buffer, inverter, clock_gate, register, macro)
  • Per-node: arrival time, delay, fanout, level
  • Min/max arrival time range per clock
  • Multiple clock support

UI:

  • Dark/light theme toggle
  • Ghosted tabs for server-only features (Layout, Schematic, Tcl Console, Hierarchy, Inspector) — visible but grayed out with "(server)" label

R3: dsltcl — JavaScript Tcl command-line query interface

The static HTML must include a dsltcl console — a JavaScript implementation of the restricted Tcl subset needed for read-only timing queries. This provides a command-line inquiry capability with zero wait (all queries resolve against the embedded in-memory model).

What is dsltcl: dsltcl is a DSL that supports exactly the .tcl that is needed for OpenROAD, ORFS, and OpenSTA. It is drop-in replaceable with real Tcl — the same commands, the same syntax, the same argument parsing — but implemented as a restricted interpreter that only needs to support the subset of Tcl used in SDC and STA query commands.

dsltcl in C++ already exists as the concept: the C++ OpenROAD codebase uses Tcl as its command language, with commands defined via define_cmd_args and implemented via SWIG bindings. The JavaScript dsltcl mirrors this: a minimal Tcl parser (variable substitution, command substitution, list handling, expr) connected to command handlers that query the embedded data model.

Read-only query commands that dsltcl must support:

Command What it queries Example
report_checks Timing paths with filtering report_checks -path_delay max -to [get_ports out*]
report_tns Total negative slack report_tns -max
report_wns Worst negative slack report_wns -max
report_clock_skew Clock skew report_clock_skew -setup
report_clock_properties Clock waveforms report_clock_properties [get_clocks clk]
get_cells Cell objects by pattern get_cells -filter "ref_name == BUFX2" *
get_pins Pin objects get_pins */CLK
get_nets Net objects get_nets clk*
get_ports Port objects get_ports {in* out*}
get_clocks Clock objects get_clocks *
all_clocks All defined clocks all_clocks
all_registers All register cells all_registers -clock clk
get_property Object properties get_property [get_cells U1] ref_name
report_slack Per-pin slack report_slack [get_pins _698_/CLK]

Commands explicitly NOT supported (non-read-only):

Command Why excluded
group_path Modifies SDC constraints — creates/changes path groups, which alters how report_checks groups results. This is a write to the timing model.
set_false_path Modifies timing constraints
set_multicycle_path Modifies timing constraints
create_clock Modifies clock definitions
set_*_delay Modifies I/O constraints
Any set_* / create_* / delete_* All modify the timing model

dsltcl Tcl language subset required:

  • Variable assignment and substitution (set x 5, $x)
  • Command substitution ([get_pins *])
  • List operations (list, lindex, llength, foreach, lsort, lsearch)
  • String operations (string match, string length, regexp)
  • expr for arithmetic
  • proc for simple user-defined procedures
  • if/else, for, while
  • Glob pattern matching (used by get_cells, get_pins, etc.)
  • puts for output

R4: Shared cached format between live GUI and static HTML

The embedded data format should be designed as an improvement to the live web GUI, not a separate path. The live GUI's WebSocket handlers could cache timing results in this same compact format, so that:

  1. The C++ StaticReport class serializes to the cached format
  2. The live web GUI can optionally use the same cached format for faster re-display (avoiding re-querying STA on every "Update" click)
  3. The dsltcl JS interpreter queries this same in-memory model
  4. The static HTML is simply the cached format + the GUI code + dsltcl, bundled into one file

This means the format is not an afterthought appendage — it's the canonical intermediate representation between STA and the GUI.

R5: Single self-contained HTML file

  • No external dependencies (no CDN links, no separate JS/CSS files)
  • Must work when opened as file:// (no CORS issues)
  • All CSS, JS, and data inlined

Why group_path + "Update" Is Non-Read-Only

The web GUI's slack histogram currently supports filtering by path group via a dropdown (charts-widget.js:284). In the live GUI, changing the path group filter triggers a server round-trip that calls TimingReport::getSlackHistogram() with the selected group.

The group_path Tcl command creates or modifies path groups in the SDC constraint set. It's an ExceptionPathType::group_path (SdcClass.hh:71) that changes how the STA engine partitions paths for reporting. After calling group_path, a subsequent report_checks or histogram "Update" will return different results — not because new timing data was computed, but because the grouping/classification of existing paths changed.

This is why group_path cannot be supported in static mode:

  1. It mutates the SDC model (path group definitions)
  2. The mutation changes which paths appear in which group
  3. Re-querying after the mutation requires the STA search engine to re-partition endpoints
  4. The static HTML has pre-cooked results for the path groups that existed at generation time

More generally, any SDC command (set_false_path, set_multicycle_path, create_clock, etc.) invalidates the pre-cooked timing data. The static HTML's data is a frozen snapshot — read-only queries against it are valid, but mutations require a live STA engine.

Payload Size: Full OpenSTA Queries vs GUI-Only

GUI-only payload (proposed for static HTML)

The GUI displays pre-extracted summaries. The data model is:

Data What's stored Size (100K endpoint design)
Worst N paths (setup+hold) N path summaries with per-node detail ~260 KB for 200 paths
Slack histogram bins ~15 pre-computed bins per setup/hold <1 KB
Clock trees Tree structure with arrival/delay/fanout ~500 KB for 10K nodes
Chart filters Path group and clock name lists <1 KB
String table Unique pin/instance names across above ~150 KB
Total ~950 KB

This is compact because it stores only aggregated results — the worst N paths, pre-binned histograms, and the clock tree topology. It doesn't need to answer arbitrary queries like "what's the slack at pin X?" or "show me paths through net Y".

Full OpenSTA query payload (needed for dsltcl)

Supporting arbitrary report_checks -from/-through/-to, report_slack <pin>, get_fanin/get_fanout, get_property, etc. requires the per-pin timing model:

Data What's stored Size (100K endpoint design)
Pin-level timing Per-pin: arrival, required, slack, slew (setup+hold, rise+fall) = 16 floats per pin ~25 MB for 400K pins
Netlist connectivity Pin-to-net-to-pin adjacency (for -through, get_fanin/get_fanout) ~15 MB
Instance properties ref_name, cell type, location, for get_cells -filter, get_property ~8 MB
Liberty cell summaries Cell function, pin directions, timing arc existence (for report_dcalc) ~5 MB
Clock network Per-pin clock domain, ideal/propagated, clock edges ~3 MB
SDC constraints Clock definitions, I/O delays, false paths, multicycle paths, path groups ~500 KB
String table All pin/net/instance/cell names ~12 MB
Total ~70 MB

With aggressive compression (interning, quantization, column-oriented arrays), this could be reduced to ~20-30 MB. With gzip (applied by browser to base64-encoded blob), perhaps ~8-15 MB.

Comparison

Aspect GUI-only Full OpenSTA queries
Payload size (100K endpoints) ~1 MB ~20-70 MB
Payload size (1M endpoints) ~3-5 MB ~200-700 MB
Supports arbitrary pin queries No Yes
Supports -through filtering No Yes
Supports get_fanin/get_fanout No Yes
Supports get_property No (only names in paths) Yes
Email-attachable Yes Only small designs
Page load time Instant 1-5 seconds (decompression)
dsltcl command coverage View-only (path table, histogram, clock tree) Most read-only STA commands

Recommended approach: tiered

Rather than choosing one or the other, support both tiers:

  1. Tier 1 (default): GUI-only payload (~1 MB). All visual widgets work. dsltcl supports report_tns, report_wns, all_clocks, report_clock_properties, and queries that can be answered from the pre-extracted paths and clock tree. Other commands print "requires full model — regenerate with -full".

  2. Tier 2 (-full flag): Full per-pin model (~20-70 MB). dsltcl supports the full read-only STA query set including report_checks -through, report_slack <pin>, get_fanin, get_fanout, get_property, arbitrary get_cells/get_pins/get_nets filtering.

# Tier 1: compact, email-friendly
write_timing_html -output timing.html

# Tier 2: full query support, larger file
write_timing_html -output timing_full.html -full

Why This Is a Good Fit for the Existing Architecture

The web GUI already separates concerns cleanly:

  • Server (C++): queries STA, renders layout tiles, manages session state
  • Client (JS): pure data-driven renderers that consume JSON from WebSocket responses

The timing widgets (timing-widget.js, charts-widget.js, clock-tree-widget.js) have zero dependency on server-side rendering. They receive JSON, build DOM tables, and draw on <canvas>. Replacing the WebSocket transport with an embedded JSON blob is a minimal change.

The server-side data extraction is already factored into reusable classes:

These classes are independent of the WebSocket server and can be called directly from a Tcl command.

Compact Embedded Data Format

Data is embedded as <script id="timing-data" type="application/json"> inside the HTML file.

String interning

Pin names like _698_/CLK repeat across paths. A string table eliminates duplication:

{
  "design": "gcd",
  "generated": "2026-04-01T12:00:00Z",
  "time_unit": "ns",
  "strings": ["_698_/CLK", "_698_/Q", "_345_/A"],

  "setup_paths": [
    {
      "sclk": 0, "eclk": 0,
      "slk": -0.1234, "arr": 9.4321, "req": 9.3087,
      "skw": 0.012, "pd": 1.234, "ld": 3, "fo": 12,
      "sp": 1, "ep": 45,
      "dn": [[0,2,1,0, 0.0000,0.0000,0.0312,0.0000]],
      "cn": []
    }
  ],
  "hold_paths": [],

  "setup_histogram": {
    "bins": [[-0.5, -0.4, 23, true]],
    "total": 50000, "unconstr": 200
  },
  "hold_histogram": {},

  "filters": { "path_groups": ["group1"], "clocks": ["clk"] },

  "clock_trees": [
    {
      "name": "clk",
      "min_arrival": 0.0, "max_arrival": 2.5,
      "nodes": [[0, -1, 0, 1, 0, 0.0, 0.0, 32, 0]]
    }
  ]
}

Timing nodes as flat arrays [pin_idx, fanout, rise, clk, time, delay, slew, load] — ~50% smaller than named-key JSON.

Size estimates (Tier 1, GUI-only payload)

Design scale Endpoints Paths Clock tree nodes Estimated file size
Small (gcd) ~100 100+100 ~50 ~20 KB
Medium ~10K 100+100 ~2K ~200 KB
Large ~100K 100+100 ~10K ~950 KB
Very large ~1M 500+500 ~50K (capped) ~3-5 MB

Proposed Tcl Command

write_timing_html -output timing_report.html \
  [-setup_paths 100] \
  [-hold_paths 100] \
  [-max_clock_tree_nodes 10000] \
  [-full]

Implementation Approach

JS widget reuse

A mock websocketManager resolves requests from the embedded data instead of a WebSocket. The existing widgets work unmodified — they already have .catch() handlers for failed requests (see timing-widget.js:220), so ghosted operations like path-highlight fail silently.

Five existing JS files are inlined into the template with import/export stripped:

No external dependencies (no Leaflet, no GoldenLayout). A simple tab bar replaces the docking layout.

Zero click-and-wait implementation

On page load, the JS immediately:

  1. Parses the embedded JSON
  2. Populates the timing path table (setup tab active by default)
  3. Renders the slack histogram
  4. Renders the clock tree
  5. Populates filter dropdowns

The "Update" button is replaced with a "Loaded from snapshot" label. No async operations, no promises to await — everything is synchronous from the embedded data.

dsltcl console

The dsltcl console widget provides:

  • A text input with command history (up/down arrows)
  • Tab completion against the known command set
  • Output display area
  • Commands resolve synchronously against the in-memory model

The dsltcl interpreter is a JavaScript module (~5-10 KB) that implements:

  • Tcl tokenizer (braces, quotes, brackets, backslash substitution)
  • Variable store (set/$ substitution)
  • Command dispatch table (registered read-only commands)
  • expr evaluator
  • List operations
  • proc definitions
  • Control flow (if/for/foreach/while)

Each registered command (e.g., report_checks, get_pins) is a JS function that queries the embedded data model — the same model the GUI widgets use.

C++ data extraction

A new StaticReport class calls the existing extraction APIs:

JSON serialization patterns are in request_handler.cpp (timing report) and request_handler.cpp (clock tree).

New files

  • src/web/src/static_report.h / .cpp — extraction + interned JSON generation
  • src/web/src/static_report_template.html — self-contained HTML template with dsltcl
  • src/web/src/dsltcl.js — JavaScript Tcl subset interpreter

Modified files

No modifications to existing widget JS or C++ report classes

The existing TimingReport, ClockTreeReport, and all widget JS files are reused as-is.

Suggested Solution

@tspyrou Not to shabby, eh?

Additional Context

No response

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions