Skip to content

Static web features catch-up to Qt#10218

Open
oharboe wants to merge 14 commits intoThe-OpenROAD-Project:masterfrom
oharboe:static-web-features
Open

Static web features catch-up to Qt#10218
oharboe wants to merge 14 commits intoThe-OpenROAD-Project:masterfrom
oharboe:static-web-features

Conversation

@oharboe
Copy link
Copy Markdown
Collaborator

@oharboe oharboe commented Apr 21, 2026

Please pick whatever is useful here for the static web GUI and close this PR.

To test new features, run:

bazelisk run test/orfs/gcd:gcd_synth_html
  • Click above the green bucket and you can still go to the timing report for that bucket image
  • Pin inspector
image - Sortable columns image

oharboe added 6 commits April 21, 2026 06:53
Upstream bazel-orfs dropped the @docker_orfs image extraction in favour
of source-built tools (OpenROAD/OpenSTA from @openroad, yosys/abc from
BCR, GNU Make from source) and gained a non-dev bazel_dep on the orfs
module. Overrides declared in non-root modules are ignored, so mirror
bazel-orfs's orfs git_override and the yosys single_version_override at
root. Patches are copied into bazel/orfs-patches/ because git_override
only accepts patches from the main repo.

BCR yosys 0.62 ships backends/aiger2/aiger.cc but doesn't compile it,
so the internal write_xaiger2 call from abc_new.cc aborts synthesis.
Add yosys-backend-aiger2.patch to wire backends/aiger2 into the yosys
binary.

test/orfs references the asap7 stdcell dff.v/empty.v that used to come
from @docker_orfs; copy them next to the existing per-test .v files.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Gemini review flagged that test/orfs/asap7/BUILD listed different
Verilog files in asap7_files and exports_files (empty.v was exported
but not in the filegroup; OA was in the filegroup but not exported).
Share a single ASAP7_VERILOG list between both so the set stays in
sync.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Enables `bazelisk run test/orfs/gcd:gcd_synth_html` and similar
HTML report targets across the gcd, ram_8x7, and mock-array flows.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Clicking a histogram bucket previously required landing on the short
colored bar, so bins with few endpoints were very hard to hit.  Extend
the hit-test to the full column strip above the x-axis, matching the
Qt GUI's QBarSet::clicked behavior.  Bars with count == 0 still never
match.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
The Qt timing widget opens a pin/instance inspector on a double-click
in the Data Path / Capture Path view.  The static HTML export and the
live web UI had no equivalent — the biggest gap in the gap analysis
on issue The-OpenROAD-Project#10215.

Extend each TimingNode with the pin direction, owning instance name,
master cell, and connected net, and surface them via a small popover
opened on double-click.  Close on Escape, × button, outside click, or
by opening another row.  Pure additive JSON fields (emitted only when
non-empty) so existing clients are unaffected.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Clicking any header in the Setup / Hold path table or in the Data /
Capture path detail table now sorts by that column; repeating the
click toggles direction.  The path table defaults to Slack ascending
(most-negative first), matching the Qt GUI default.

Skew / Logic Delay / Logic Depth headers gain the same explanatory
tooltips used in the Qt widget (staGui.cpp:183-196).

Selection survives a re-sort: the widget finds the selected path
object by identity after sorting and updates the selected row index
accordingly.  Clicks on the resize grip are ignored so header
resizing and sorting do not conflict.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
@github-actions
Copy link
Copy Markdown
Contributor

clang-tidy review says "All clean, LGTM! 👍"

Copy link
Copy Markdown
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 enhances the web-based timing report by adding a Pin Inspector popover, implementing table sorting for paths and details, and improving histogram hit detection. It also updates the Bazel build configuration to handle ORFS and Yosys overrides at the root and transitions to local ASAP7 files for testing. Feedback addressed critical issues regarding index mismatches in sorted tables, potential memory leaks in event listener management, and incorrect comparison logic for identical missing values during sorting.

Comment thread src/web/src/timing-widget.js Outdated
Comment thread src/web/src/timing-widget.js
Comment thread src/web/src/timing-widget.js Outdated
Comment thread src/web/src/timing-widget.js
Comment thread src/web/src/timing-widget.js
Comment thread src/web/test/js/test-timing-widget.js Outdated
oharboe added 5 commits April 21, 2026 10:29
Fix issues flagged by gemini-code-assist on PR The-OpenROAD-Project#10218:

* _showInspector now takes the node object directly instead of an
  index. When the detail table is sorted, the row index no longer
  maps to the underlying path.data_nodes / path.capture_nodes array,
  so the index-based lookup returned the wrong pin. The dblclick
  handler already has the node in scope; pass it through.

  Silent `if (!path) return;` / `if (!n) return;` early returns
  masked programmer errors. Internal callers always pass a real
  node; a null there is a bug that should surface as a natural
  TypeError rather than a quiet no-op.

* Store the setTimeout id in _inspectorCloseHandlers and cancel it
  in _closeInspector. Without this, a rapid open/close (e.g. two
  quick double-clicks, or Escape before the deferred callback runs)
  attached the outside-click listener *after* cleanup, leaking a
  document-level listener that never gets removed.

* compareValues now returns 0 for identical values up front.
  Previously compareValues(null, null, dir) returned 1 because the
  "missing value sorts last" branch triggered for both sides,
  breaking stable sort on columns with repeated blank cells.

Tests updated to pass node objects instead of indexes.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
The .timing-inspector-popover rule referenced var(--bg-primary), which
is not defined in either theme. The popover fell back to transparent
and leaked the underlying pane background. Switch to var(--bg-panel),
matching the other popover/panel backgrounds in the file.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Replace hardcoded rgba(0, 0, 0, 0.25) with var(--shadow). Other popovers
and panels in the stylesheet already use var(--shadow) / var(--shadow-strong),
which resolve differently for dark and light themes. The hardcoded value
rendered too heavy in light mode relative to the rest of the UI.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
The .hover-highlight stroke/fill and @Keyframes hover-pulse hardcoded a
yellow palette (#ffe85c, #fff27a, #fff6b0, #ffffff) that was tuned for
the dark canvas. In light mode the same palette washes out against the
white background.

Introduce a --hover-pulse theme variable (yellow in dark, orange in
light) and drive stroke/fill from it. The per-keyframe color swap is
removed; the pulsing size and opacity already provide the emphasis.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
The path and detail table containers have tabindex=0 so arrow-key
navigation works, but the code then set style.outline = 'none' with no
CSS replacement. Keyboard users could tab between the two tables with
no visual indication of which one had focus.

Drop the inline outline override and add :focus-visible rules that
draw a 2px accent-colored ring inset into the container.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
@oharboe
Copy link
Copy Markdown
Collaborator Author

oharboe commented Apr 21, 2026

/gemini review

@github-actions
Copy link
Copy Markdown
Contributor

clang-tidy review says "All clean, LGTM! 👍"

Copy link
Copy Markdown
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 enhances the web-based timing report and chart widgets, introducing sortable tables for timing paths and details, a new 'Pin Inspector' popover for deep-diving into node properties, and improved hit-testing for histogram interactions. Additionally, it updates Bazel dependencies for the ORFS integration, replacing Docker-based file references with local overrides and patches to improve build hermeticity. Review feedback identifies a logic error in the value comparator that violates the comparator contract for missing values and suggests preserving the selection state when the detail table is re-sorted.

Comment on lines +428 to +430
const nodes = this._detailSort
? sortRowsInPlace([...nodesSrc], this._detailSort)
: nodesSrc;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The _selectedDetailIndex is not updated when the detail table is re-sorted. This will cause the selection highlight to point to the wrong row after a sort operation. Consider tracking the selected node by identity and updating the index, similar to how _selectedPathIndex is handled in _renderPathTable.

        const selectedNode = this._selectedDetailIndex >= 0 ? nodesSrc[this._selectedDetailIndex] : null;
        const nodes = this._detailSort
            ? sortRowsInPlace([...nodesSrc], this._detailSort)
            : nodesSrc;
        if (selectedNode) {
            this._selectedDetailIndex = nodes.indexOf(selectedNode);
        }

Comment on lines +495 to +505
export function compareValues(a, b, dir) {
if (a === b) return 0;
const mul = dir === 'desc' ? -1 : 1;
const na = typeof a === 'number';
const nb = typeof b === 'number';
if (na && nb) return (a - b) * mul;
// Missing values sort last regardless of direction.
if (a === undefined || a === null || a === '') return 1;
if (b === undefined || b === null || b === '') return -1;
return String(a).localeCompare(String(b)) * mul;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The compareValues function has an inconsistent comparison logic for missing values (null, undefined, or empty string). If a is null and b is undefined, it returns 1. If a is undefined and b is null, it also returns 1. This violates the contract of a comparator where compare(a, b) should be the negative of compare(b, a). It should first check if both values are missing.

Suggested change
export function compareValues(a, b, dir) {
if (a === b) return 0;
const mul = dir === 'desc' ? -1 : 1;
const na = typeof a === 'number';
const nb = typeof b === 'number';
if (na && nb) return (a - b) * mul;
// Missing values sort last regardless of direction.
if (a === undefined || a === null || a === '') return 1;
if (b === undefined || b === null || b === '') return -1;
return String(a).localeCompare(String(b)) * mul;
}
export function compareValues(a, b, dir) {
if (a === b) return 0;
const isMissing = (v) => v === undefined || v === null || v === '';
if (isMissing(a) && isMissing(b)) return 0;
if (isMissing(a)) return 1;
if (isMissing(b)) return -1;
const mul = dir === 'desc' ? -1 : 1;
const na = typeof a === 'number';
const nb = typeof b === 'number';
if (na && nb) return (a - b) * mul;
return String(a).localeCompare(String(b)) * mul;
}

oharboe added 3 commits April 21, 2026 10:53
Add src/web/src/error-banner.js exporting showErrorBanner(err) — a
fixed-position red banner with the error message and a collapsible
stack — and installGlobalErrorHandlers(doc, win) that wires the
banner to 'unhandledrejection' and 'error' on the window.

main.js calls installGlobalErrorHandlers() once at module load, so
anything that throws anywhere in the widget layer — a promise with
no .catch, a handler that lets an exception propagate — surfaces to
the user instead of ending up only in the console. Static-mode
feature gaps are surfaced separately as calm in-widget messages
(follow-up commits); the banner is for bugs.

Styling uses existing theme vars (--error, --fg-white, --shadow-strong)
so it themes correctly in both light and dark modes.

Tests cover render, stack extraction, multi-stack, dismiss button,
and the two window event hooks via jsdom.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Widgets that depend on backend request types the static cache cannot
answer (DRC, Clock Tree, Schematic, Hierarchy) previously issued the
doomed request anyway and caught the rejection with console.error.
The user saw nothing; the pane displayed with an empty selector or
status bar and no hint that the feature was off-limits in a static
report.

Expose the static-cache whitelist from WebSocketManager via a new
supportsInStatic(type) predicate (single source of truth alongside
_cacheRequest's existing dispatch). Add a small buildUnavailableStub
helper in ui-utils.js that renders a neutral, centered message using
the existing fg-secondary muted color — not red, not an error, just
a clear statement of the mode's limits.

Each of the four widgets checks isStaticMode in its constructor and
returns early with the stub instead of wiring the normal UI. The
Tcl console input is disabled with hint-text placeholder. The File
menu's Open DB / Save DB items gate on isStaticMode too.

Tests:
- WebSocketManager.supportsInStatic allowlist vs denylist
- DrcWidget and ClockTreeWidget render the stub and fire no requests

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
With the top-level banner (installGlobalErrorHandlers) and the
static-mode stubs (supportsInStatic + buildUnavailableStub) in place,
.catch blocks that only called console.error were turning bugs back
into invisible console noise. Remove them so the errors reach the
banner and are surfaced to the user.

* drc-widget: drop console.error catches on _loadCategories,
  _highlightMarker, _updateMarker, _toggleCategoryVisibility. The
  per-widget .drc-error div on _loadMarkers stays (in-widget context
  is useful) but now re-throws so the banner also fires.
* charts-widget: drop the catch on bar click that swallowed the
  timing_report failure with a console.error.
* clock-tree-widget: drop console.error in update(); keep the
  _statusLabel text and rethrow.
* schematic-widget: drop console.error/log lines; keep setStatus
  and rethrow.
* display-controls: drop console.error on heatmap_update.
* hierarchy-browser: drop console.error on set_module_colors.
* inspector: drop console.error on set_focus_nets, clearFocusNets,
  set_route_guides, inspect, inspect_back. Keep the two intentional
  silent catches on hover requests with an explanatory comment.
* tcl-completer: annotate the two intentional silent catches
  (opportunistic cache refresh, popup-hide on no-suggestions).
* main.js: drop the log-only catch on click-to-select and the
  outer try/catch around initial tech/bounds/heatmaps load. Comment
  the blank-tile fallback on heatmap tile failures (legitimate UX
  for empty regions).
* timing-widget: drop console.error catches on update() and
  _selectPathRow's timing_highlight request.

No behavior change for the kept catches; the diff to main.js is
inflated by de-indenting the formerly-wrapped block.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
@oharboe oharboe requested a review from maliberty April 21, 2026 09:10
@github-actions github-actions Bot added size/XL and removed size/L labels Apr 21, 2026
@github-actions
Copy link
Copy Markdown
Contributor

clang-tidy review says "All clean, LGTM! 👍"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant