Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/web/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright (c) 2026, The OpenROAD Authors

load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("@rules_python//python:defs.bzl", "py_binary")
load("//bazel:tcl_encode_or.bzl", "tcl_encode")
load("//bazel:tcl_wrap_cc.bzl", "tcl_wrap_cc")

Expand All @@ -10,6 +11,60 @@ package(
features = ["layering_check"],
)

py_binary(
name = "embed_report_assets",
srcs = ["src/embed_report_assets.py"],
main = "src/embed_report_assets.py",
)

genrule(
name = "report_assets",
srcs = [
"src/style.css",
"src/theme.js",
"src/coordinates.js",
"src/ui-utils.js",
"src/checkbox-tree-model.js",
"src/vis-tree.js",
"src/websocket-manager.js",
"src/websocket-tile-layer.js",
"src/display-controls.js",
"src/inspector.js",
"src/ruler.js",
"src/tcl-completer.js",
"src/hierarchy-browser.js",
"src/menu-bar.js",
"src/clock-tree-widget.js",
"src/schematic-widget.js",
"src/charts-widget.js",
"src/timing-widget.js",
"src/main.js",
],
outs = ["src/report_assets.cpp"],
cmd = "$(execpath :embed_report_assets)" +
" --output $@" +
" --css $(location src/style.css)" +
" --js $(location src/theme.js)" +
" $(location src/coordinates.js)" +
" $(location src/ui-utils.js)" +
" $(location src/checkbox-tree-model.js)" +
" $(location src/vis-tree.js)" +
" $(location src/websocket-manager.js)" +
" $(location src/websocket-tile-layer.js)" +
" $(location src/display-controls.js)" +
" $(location src/inspector.js)" +
" $(location src/ruler.js)" +
" $(location src/tcl-completer.js)" +
" $(location src/hierarchy-browser.js)" +
" $(location src/menu-bar.js)" +
" $(location src/clock-tree-widget.js)" +
" $(location src/schematic-widget.js)" +
" $(location src/charts-widget.js)" +
" $(location src/timing-widget.js)" +
" $(location src/main.js)",
tools = [":embed_report_assets"],
)

cc_library(
name = "web",
srcs = [
Expand All @@ -29,6 +84,7 @@ cc_library(
"src/timing_report.cpp",
"src/timing_report.h",
"src/web.cpp",
":report_assets",
],
hdrs = [
"include/web/web.h",
Expand Down
49 changes: 49 additions & 0 deletions src/web/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,54 @@ swig_lib(NAME web
${ODB_HOME}/include
)

set(REPORT_ASSETS_CPP ${CMAKE_CURRENT_BINARY_DIR}/report_assets.cpp)
add_custom_command(
OUTPUT ${REPORT_ASSETS_CPP}
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/src/embed_report_assets.py
--output ${REPORT_ASSETS_CPP}
--css ${CMAKE_CURRENT_SOURCE_DIR}/src/style.css
--js ${CMAKE_CURRENT_SOURCE_DIR}/src/theme.js
${CMAKE_CURRENT_SOURCE_DIR}/src/coordinates.js
${CMAKE_CURRENT_SOURCE_DIR}/src/ui-utils.js
${CMAKE_CURRENT_SOURCE_DIR}/src/checkbox-tree-model.js
${CMAKE_CURRENT_SOURCE_DIR}/src/vis-tree.js
${CMAKE_CURRENT_SOURCE_DIR}/src/websocket-manager.js
${CMAKE_CURRENT_SOURCE_DIR}/src/websocket-tile-layer.js
${CMAKE_CURRENT_SOURCE_DIR}/src/display-controls.js
${CMAKE_CURRENT_SOURCE_DIR}/src/inspector.js
${CMAKE_CURRENT_SOURCE_DIR}/src/ruler.js
${CMAKE_CURRENT_SOURCE_DIR}/src/tcl-completer.js
${CMAKE_CURRENT_SOURCE_DIR}/src/hierarchy-browser.js
${CMAKE_CURRENT_SOURCE_DIR}/src/menu-bar.js
${CMAKE_CURRENT_SOURCE_DIR}/src/clock-tree-widget.js
${CMAKE_CURRENT_SOURCE_DIR}/src/schematic-widget.js
${CMAKE_CURRENT_SOURCE_DIR}/src/charts-widget.js
${CMAKE_CURRENT_SOURCE_DIR}/src/timing-widget.js
${CMAKE_CURRENT_SOURCE_DIR}/src/main.js
DEPENDS
src/embed_report_assets.py
src/style.css
src/theme.js
src/coordinates.js
src/ui-utils.js
src/checkbox-tree-model.js
src/vis-tree.js
src/websocket-manager.js
src/websocket-tile-layer.js
src/display-controls.js
src/inspector.js
src/ruler.js
src/tcl-completer.js
src/hierarchy-browser.js
src/menu-bar.js
src/clock-tree-widget.js
src/schematic-widget.js
src/charts-widget.js
src/timing-widget.js
src/main.js
COMMENT "Generating report_assets.cpp"
)

target_sources(web
PRIVATE
src/clock_tree_report.cpp
Expand All @@ -23,6 +71,7 @@ target_sources(web
src/timing_report.cpp
src/web.cpp
src/MakeWeb.cpp
${REPORT_ASSETS_CPP}
)

target_link_libraries(web
Expand Down
45 changes: 45 additions & 0 deletions src/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,51 @@ save_image -web -display_option {routing false} \
layout.png
```

### Save Report

Generate a self-contained HTML timing report. The report uses the same
JavaScript frontend as the live web viewer but serves all data from a cache
embedded in the HTML file. No running server is required to view the report.

```tcl
web_save_report
[-setup_paths count]
[-hold_paths count]
path
```

#### Options

| Switch Name | Description |
| ----- | ----- |
| `-setup_paths` | Maximum number of setup timing paths to include. Default: `100`. |
| `-hold_paths` | Maximum number of hold timing paths to include. Default: `100`. |
| `path` | Output HTML file path. |

The report includes:

- **Layout view** with pre-rendered tiles at a fixed zoom level. Layer
visibility can be toggled using the same display controls as the live viewer.
Zoom is disabled; pan is allowed.
- **Timing table** with setup and hold paths. Clicking a path highlights it
on the layout via a pre-rendered overlay image.
- **Slack histogram** with setup/hold tabs.
- **Display controls**, hierarchy browser, clock tree, and other panels from
the live viewer (features that require server interaction show empty states).

The report requires an internet connection to load Leaflet and GoldenLayout
CSS/JS from CDN.

#### Examples

```tcl
# Generate a report with default settings
web_save_report timing.html

# Include more paths
web_save_report -setup_paths 200 -hold_paths 200 timing.html
```

## Features

- **Tile-based rendering** — The server renders 256x256 PNG tiles on demand,
Expand Down
4 changes: 4 additions & 0 deletions src/web/include/web/web.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class WebServer

void serve(int port, const std::string& doc_root);

void saveReport(const std::string& filename,
int max_setup_paths,
int max_hold_paths);

void saveImage(const std::string& filename,
int x0,
int y0,
Expand Down
11 changes: 5 additions & 6 deletions src/web/src/charts-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function computeYAxis(maxCount) {
}

export class ChartsWidget {
constructor(container, app, redrawAllLayers) {
constructor(app, redrawAllLayers) {
this._app = app;
this._redrawAllLayers = redrawAllLayers;
this._currentTab = 'setup';
Expand All @@ -107,12 +107,12 @@ export class ChartsWidget {
this._chartArea = null;
this._hoveredBar = null;

this._build(container);
this._build();
}

// ---- DOM construction ----

_build(container) {
_build() {
const el = document.createElement('div');
el.className = 'charts-widget';

Expand Down Expand Up @@ -184,8 +184,7 @@ export class ChartsWidget {
this._tooltip.style.display = 'none';
el.appendChild(this._tooltip);

container.element.appendChild(el);
this._el = el;
this.element = el;

this._ctx = this._canvas.getContext('2d');
this._bindEvents();
Expand Down Expand Up @@ -482,7 +481,7 @@ export class ChartsWidget {
`Slack: [${bar.lower.toFixed(precision)}, ${bar.upper.toFixed(precision)}) ${unit}`;
this._tooltip.style.display = 'block';

const rect = this._el.getBoundingClientRect();
const rect = this.element.getBoundingClientRect();
const tx = e.clientX - rect.left + 12;
const ty = e.clientY - rect.top - 10;
this._tooltip.style.left = tx + 'px';
Expand Down
131 changes: 131 additions & 0 deletions src/web/src/embed_report_assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026, The OpenROAD Authors
#
# Embed JS/CSS files as C++ raw string literals for the standalone
# timing report HTML. Produces a .cpp file with const char* constants.
#
# Each JS file is wrapped in an IIFE to isolate file-private const/let
# declarations. Exported symbols are forwarded to outer-scope vars.

import argparse
import re


def process_js_file(content):
"""Process a single JS file for concatenation into a shared scope."""
# Remove import lines.
content = re.sub(r"^import\s+.*;\s*$", "", content, flags=re.MULTILINE)

# Find exported names and strip the export keyword.
# Two patterns:
# 1. export function/class/const Name ...
# 2. export { InternalA as ExportedA, InternalB, ... }
exported_names = []

# Pattern 1: export function/class/const Name
def capture_export_decl(m):
keyword = m.group(1) # function, class, or const
name = m.group(2)
exported_names.append(name)
return keyword + " " + name

content = re.sub(
r"^export\s+(function|class|const)\s+(\w+)",
capture_export_decl,
content,
flags=re.MULTILINE,
)

# Pattern 2: export { InternalA as ExportedA, InternalB, ... }
# Collects (internal_name, exported_name) pairs; removes the line;
# will add alias assignments after the IIFE.
renamed_exports = [] # (internal, exported)

def capture_export_block(m):
for item in m.group(1).split(","):
item = item.strip()
if not item:
continue
if " as " in item:
internal, exported = item.split(" as ", 1)
renamed_exports.append((internal.strip(), exported.strip()))
else:
renamed_exports.append((item, item))
return "" # remove the export block

content = re.sub(r"export\s*\{([^}]+)\}", capture_export_block, content)

content = content.strip()
if not content:
return ""

# Merge both export lists into a unified set of outer-scope names.
all_exported = list(exported_names)
for _, exported in renamed_exports:
if exported not in all_exported:
all_exported.append(exported)

if not all_exported:
# No exports — wrap in a bare block for const/let isolation.
return "{\n" + content + "\n}"

# Wrap in an IIFE. Exported names get outer-scope `var` declarations.
lines = []
lines.append("var " + ", ".join(all_exported) + ";")
# IIFE returns an object with the exported names.
# For pattern-1 exports, the name is the same inside and out.
# For pattern-2 exports, we map internal -> exported.
return_pairs = []
for name in exported_names:
return_pairs.append(name + ": " + name)
for internal, exported in renamed_exports:
return_pairs.append(exported + ": " + internal)
exports_obj = "{ " + ", ".join(return_pairs) + " }"
lines.append("var __e = (function() {")
lines.append(content)
lines.append("return " + exports_obj + ";")
lines.append("})();")
# Assign from returned object to outer vars.
for name in all_exported:
lines.append(name + " = __e." + name + ";")
return "\n".join(lines)


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--output", "-o", required=True)
parser.add_argument("--css", required=True, help="style.css path")
parser.add_argument(
"--js", nargs="+", required=True, help="JS files in dependency order"
)
args = parser.parse_args()

# Read and process JS files.
js_parts = []
for path in args.js:
with open(path, encoding="utf-8") as f:
content = f.read()
js_parts.append(f'// ── {path.split("/")[-1]} ──')
js_parts.append(process_js_file(content))
combined_js = "\n".join(js_parts)

# Read CSS.
with open(args.css, encoding="utf-8") as f:
css_content = f.read()

with open(args.output, "w", encoding="utf-8") as out:
out.write("// Auto-generated — do not edit.\n")
out.write("#include <string_view>\n")
out.write("namespace web {\n")
out.write('extern const std::string_view kReportCSS = R"__CSS__(\n')
out.write(css_content)
out.write(')__CSS__";\n\n')
out.write('extern const std::string_view kReportJS = R"__JS__(\n')
out.write(combined_js)
out.write(')__JS__";\n')
out.write("} // namespace web\n")


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions src/web/src/json_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ class JsonBuilder
buf_ += val ? "true" : "false";
}

void field(const std::string& key, const char* val)
{
field(key.c_str(), val);
}
void field(const std::string& key, int val) { field(key.c_str(), val); }
void field(const std::string& key, float val) { field(key.c_str(), val); }
void field(const std::string& key, double val) { field(key.c_str(), val); }
Expand Down
Loading
Loading