Static web features catch-up to Qt#10218
Static web features catch-up to Qt#10218oharboe wants to merge 14 commits intoThe-OpenROAD-Project:masterfrom
Conversation
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>
|
clang-tidy review says "All clean, LGTM! 👍" |
There was a problem hiding this comment.
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.
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>
|
/gemini review |
|
clang-tidy review says "All clean, LGTM! 👍" |
There was a problem hiding this comment.
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.
| const nodes = this._detailSort | ||
| ? sortRowsInPlace([...nodesSrc], this._detailSort) | ||
| : nodesSrc; |
There was a problem hiding this comment.
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);
}| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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; | |
| } |
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>
|
clang-tidy review says "All clean, LGTM! 👍" |
Please pick whatever is useful here for the static web GUI and close this PR.
To test new features, run: