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:
- The C++
StaticReport class serializes to the cached format
- The live web GUI can optionally use the same cached format for faster re-display (avoiding re-querying STA on every "Update" click)
- The dsltcl JS interpreter queries this same in-memory model
- 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:
- It mutates the SDC model (path group definitions)
- The mutation changes which paths appear in which group
- Re-querying after the mutation requires the STA search engine to re-partition endpoints
- 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:
-
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".
-
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:
- Parses the embedded JSON
- Populates the timing path table (setup tab active by default)
- Renders the slack histogram
- Renders the clock tree
- 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
Description
Based on commit
ce7ee82f49.Summary
Add a
write_timing_htmlTcl 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:
R2: Timing features that must be supported
Path analysis:
Slack histogram:
Clock tree:
UI:
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
.tclthat 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_argsand 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:
report_checksreport_checks -path_delay max -to [get_ports out*]report_tnsreport_tns -maxreport_wnsreport_wns -maxreport_clock_skewreport_clock_skew -setupreport_clock_propertiesreport_clock_properties [get_clocks clk]get_cellsget_cells -filter "ref_name == BUFX2" *get_pinsget_pins */CLKget_netsget_nets clk*get_portsget_ports {in* out*}get_clocksget_clocks *all_clocksall_clocksall_registersall_registers -clock clkget_propertyget_property [get_cells U1] ref_namereport_slackreport_slack [get_pins _698_/CLK]Commands explicitly NOT supported (non-read-only):
group_pathreport_checksgroups results. This is a write to the timing model.set_false_pathset_multicycle_pathcreate_clockset_*_delayset_*/create_*/delete_*dsltcl Tcl language subset required:
set x 5,$x)[get_pins *])list,lindex,llength,foreach,lsort,lsearch)string match,string length,regexp)exprfor arithmeticprocfor simple user-defined proceduresif/else,for,whileget_cells,get_pins, etc.)putsfor outputR4: 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:
StaticReportclass serializes to the cached formatThis 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
file://(no CORS issues)Why
group_path+ "Update" Is Non-Read-OnlyThe 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 callsTimingReport::getSlackHistogram()with the selected group.The
group_pathTcl command creates or modifies path groups in the SDC constraint set. It's anExceptionPathType::group_path(SdcClass.hh:71) that changes how the STA engine partitions paths for reporting. After callinggroup_path, a subsequentreport_checksor 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_pathcannot be supported in static mode: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:
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:-through,get_fanin/get_fanout)get_cells -filter,get_propertyreport_dcalc)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
-throughfilteringget_fanin/get_fanoutget_propertyRecommended approach: tiered
Rather than choosing one or the other, support both tiers:
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".Tier 2 (
-fullflag): Full per-pin model (~20-70 MB). dsltcl supports the full read-only STA query set includingreport_checks -through,report_slack <pin>,get_fanin,get_fanout,get_property, arbitraryget_cells/get_pins/get_netsfiltering.Why This Is a Good Fit for the Existing Architecture
The web GUI already separates concerns cleanly:
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:
TimingReport— timing paths, slack histograms, chart filtersClockTreeReport— clock tree dataThese 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_/CLKrepeat 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)
Proposed Tcl Command
Implementation Approach
JS widget reuse
A mock
websocketManagerresolves requests from the embedded data instead of a WebSocket. The existing widgets work unmodified — they already have.catch()handlers for failed requests (seetiming-widget.js:220), so ghosted operations like path-highlight fail silently.Five existing JS files are inlined into the template with
import/exportstripped:theme.jsui-utils.jstiming-widget.jscharts-widget.jsclock-tree-widget.jsNo 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:
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:
The dsltcl interpreter is a JavaScript module (~5-10 KB) that implements:
set/$substitution)exprevaluatorprocdefinitionsif/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
StaticReportclass calls the existing extraction APIs:TimingReport::getReport()for setup/hold pathsTimingReport::getSlackHistogram()for pre-computed histogram binsTimingReport::getChartFilters()for path groups and clocksClockTreeReport::getReport()for clock tree dataJSON serialization patterns are in
request_handler.cpp(timing report) andrequest_handler.cpp(clock tree).New files
src/web/src/static_report.h/.cpp— extraction + interned JSON generationsrc/web/src/static_report_template.html— self-contained HTML template with dsltclsrc/web/src/dsltcl.js— JavaScript Tcl subset interpreterModified files
src/web/CMakeLists.txt— new sources + template embeddingsrc/web/src/web.tcl— registerwrite_timing_htmlcommandsrc/web/src/web.i— SWIG bindingNo 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