Web UI internals
This page documents the internal architecture of the web UI for developers: the client–server protocol, the frontend module structure, and — in detail — the view event bus that coordinates the viewers, including its unintuitive properties. The user-facing introduction is at Web UI.
Components
Backend (
ordec/server.py): a WebSocket server that evaluates ORD/Python sources, discovers views, and serializes view data to the browser. In production it also serves the static frontend fromordec/webdist.tar; during development, a separate Vite dev server (cd web && npm run dev) serves the frontend with hot reload whileordec -bprovides only the backend.Frontend (
web/src/): vanilla JS built with Vite, using Golden Layout for the tabbed/split panel arrangement.
Warning
The web tests (pytest -m web) run against the built bundle. After changing anything under web/src/, run cd web && npm run build first — otherwise the tests silently exercise the stale previous bundle and may pass although the new code is broken (or fail although it is fine).
Frontend module map
main.jsEntry point: Golden Layout setup, toolbar, editor, opening/focusing result views, event-bus wiring for
*:request-openevents.client.jsOrdecClient: WebSocket connection, view list, sequential view requests, dispatch to result viewers.auth.jsSession/auth token management, HMAC verification of module/view query parameters in local mode.
resultviewer.jsResultViewer(one per Golden Layout panel: view selector + content area) and the per-type view classes (schematic/symbol SVG viewer, DRC viewer, LVS report viewer, plots, HTML, …), keyed by thetypefield of view messages.layout-gl.jsWebGL-based layout renderer (its own view class).
simplot.jsD3-based interactive simulation plots.
hier-selector.jsHierarchical path selector for browsing simulation results.
event-bus.jsviewEventBussingleton (see below).viewer-coordinates.js,siformat.js,theme.js,ace-ord-mode.jsHelpers: coordinate transforms, SI formatting, colors, ORD syntax highlighting.
For automated browser tests, main.js exposes window.ordecClient and window.viewEventBus, and each ResultViewer provides testInfo(); see tests/test_web.py and tests/test_web_eventbus.py.
Client–server protocol
All communication runs over one WebSocket (/api/websocket) with JSON messages:
On connect, the client authenticates and submits the source:
{msg: 'source', srctype, src, auth}(integrated mode, code from the browser editor) or{msg: 'localmodule', module, auth}(local mode, module on the server’s filesystem).The server builds the cells, discovers all views (
discover_views: every@generatemethod and@generate_funcfunction reachable from the module) and answers with{msg: 'viewlist', views: [...]}— or{msg: 'exception', exception}if evaluation failed.For each result panel that has a view selected, the client requests
{msg: 'getview', view: <view name>}. Requests are strictly sequential (reqPendingflag): the nextgetviewis sent only after the previous{msg: 'view', ...}response arrived.The server evaluates the view and responds
{msg: 'view', view, type, data}, wheretypeselects the frontend view class anddatais the output of the view’swebdata()method. Errors during view generation are reported per-view via anexceptionfield instead.In local mode, the server watches the source files with inotify and pushes
{msg: 'localmodule_changed'}, upon which the client reconnects (unless auto-refresh is disabled).
View names are evaluated with eval()
The server does not look up view names in a table; query_view() in server.py evaluates the requested view name as a Python expression in the connection’s module globals (eval(view_name, conn_globals, conn_globals)). MyCell().schematic is therefore just the common case — any expression that evaluates to a subgraph with a webdata() method works.
This is load-bearing for the LVS viewer: an LvsReport references the compared layout/schematic subgraphs of subcircuit pairs only via nodes inside the report subgraph, and the frontend addresses them with view expressions like MyCell().lvs_report.subgraph.cursor_at(<nid>).ref_layout. Anything reachable from the report can be opened as a view this way, without server-side support code.
(Arbitrary expression evaluation is intentional and consistent with the security model: it is only reachable on an authenticated WebSocket, and the authenticated user may execute arbitrary code by design.)
The view event bus
event-bus.js provides the singleton viewEventBus, a minimal pub/sub hub that lets viewers in different Golden Layout panels talk to each other (e.g. “highlight this DRC violation in the layout viewer”). API: emit(event, data), on(event, cb), off(event, cb), hasListeners(event), plus a pending store (setPending, getPending, consumePending, clearPending) for delivering a payload to viewers that are not open yet.
Events
Event |
Emitter → Listener |
Meaning / payload |
|---|---|---|
|
DRC viewer → layout viewer |
Highlight a DRC violation; payload has the violation geometry. Pending key: |
|
DRC viewer → layout viewer |
Remove DRC highlight. |
|
LVS viewer → layout viewer |
Highlight an LVS item in the layout. Payload: |
|
LVS viewer → schematic viewer |
Highlight an LVS item in the schematic (same payload). |
|
LVS viewer → both |
Remove LVS highlights. |
(pending key |
LVS viewer → late viewers |
Last selection payload, applied by layout/schematic viewers that open after the click (kept, not consumed — see below). |
|
any viewer → |
Open (or focus) a result panel showing |
|
any viewer → |
Same for schematics. |
|
LVS viewer → |
Open layout and/or schematic panels ( |
Opening new viewers from a viewer (open-and-highlight flow)
When a report viewer (DRC, LVS) wants to highlight an object in a layout or schematic, the target viewer may not be open yet. A viewer cannot create panels itself — panel management lives in main.js — and it cannot deliver a highlight synchronously to a panel whose content does not exist yet. The flow that solves both problems:
Derive the target view expression. The initiating viewer builds the view name of the layout/schematic to open from its own
viewName, by appending attribute accesses: e.g.${this.viewName}.ref_layoutfor the report’s layout, or${this.viewName}.subgraph.cursor_at(${nid}).ref_layoutfor the layout of an LVS subcircuit pair. This works because view names are Python expressions evaluated by the server (see above) — any subgraph reachable from the report can be named this way, without the server knowing about it in advance.If a suitable viewer is already open, just emit. If
viewEventBus.hasListeners('lvs:layout-select')(or the targeted viewer is known to be open), emitting the select event is sufficient; no panel needs to be created.Otherwise, park the payload and request a panel. The viewer stores the selection payload in the pending store (
setPending) and emitslayout:request-open/schematic:request-open(orlvs:request-open-viewsfor a layout+schematic pair at once), passing the view expression and its own Golden Layout container assourceContainer(available on view classes asthis.glContainer). The pending store is what bridges the asynchronous gap: an event emitted now would simply be lost, since the future viewer is not subscribed yet.main.js opens or focuses the panel. The
*:request-openhandlers first look for an existing result panel whose selected view equals the requested expression (findResultViewerByView) and focus it instead of duplicating it. Otherwise they add a newresultcomponent withcomponentState: {view, directView: true}, placed in a split next to the requesting panel (derived fromsourceContainer);lvs:request-open-viewsstacks layout and schematic in one column.Direct-view panels skip the view selector. A
ResultViewercreated withdirectView: truehas no view dropdown/hierarchy selector — it shows a fixed label with the view expression, immediately requests its view, and ignores the auto-refresh gating that normal panels apply.The view data arrives via the normal protocol. The new panel enters the client’s sequential
getviewqueue; the servereval()’s the expression and returns the rendered view data;updateView()instantiates the view class and then assignsviewName/glContainer.The new viewer picks up the pending payload. In its constructor it reads the pending selection (
consumePendingfor DRC,getPendingfor LVS) and applies the highlight duringupdate()— not in the constructor, because targeted payloads must be filtered againstviewName, which is only assigned after construction (see pitfalls below).
For a complete reference implementation of this flow, see the DRC viewer’s marker click handler and the LVS viewer’s _attachEventHandlers() in resultviewer.js, and the corresponding pending-consumption code in layout-gl.js.
Targeted vs. broadcast selection
LVS item payloads carry positions (layout) and node ids (schematic) that are only meaningful relative to one specific subgraph. For the report’s top-level circuit pair, the payload is broadcast with layoutView/schemView set to null, and every open layout/schematic viewer highlights (an open view of the top cell is correct regardless of the view expression it was opened under). For subcircuit pairs, the payload carries the pair’s view expressions (<report view>.subgraph.cursor_at(<circuit nid>).ref_layout / .ref_schematic), and listeners must filter: a viewer ignores the event unless the target view name equals its own viewName. Without this filtering, nids and positions of different subgraphs would collide and highlight nonsense in unrelated viewers.
Pitfalls
Hard-won properties of this design — read before touching viewer event code:
viewName is assigned only after construction.
ResultViewer.updateView()instantiates a view class and then setsview.viewName(andview.glContainer). A view-class constructor therefore cannot filter targeted payloads by view name. Pattern used by the layout and schematic viewers: stashviewEventBus.getPending('lvs:select')in the constructor, and apply/filter it at the start of the firstupdate()call, whereviewNameis known.The LVS pending payload must not be consume-once. A single LVS item click may open two viewers (layout and schematic), and both need the same pending payload — hence
getPending+ an explicitclearPending('lvs:select')on deselect, in contrast to the DRC viewer which usesconsumePending(only one target viewer).hasListeners is a heuristic. For top-pair selections, the LVS viewer emits to existing listeners and only requests opening new panels when no listener exists at all. Any open layout viewer counts, which is fine for the top pair (broadcast semantics) but wrong for subcircuit pairs — those always request their own (named) views and rely on
request-open-viewsfocusing already-open panels instead of duplicating them.Event handlers are attached in a separate method from rendering. The LVS viewer builds its DOM in
update()but attaches handlers in_attachEventHandlers(itemMap, circuitMap); every lookup table the handlers need must be passed explicitly. A handler referencing a variable that only exists in the rendering scope fails with an uncaught ReferenceError visible only in the browser console — the UI just silently does nothing. When debugging “click has no effect”, check the browser console first.destroy() must mirror every on(). Golden Layout creates and destroys view instances as panels open and close. A view class that subscribes in its constructor must unsubscribe in
destroy(), otherwise stale listeners of closed panels keep reacting to events.