2026 02 14_servo_architecture_constraints - mark-ik/graphshell GitHub Wiki
This document is research, not a committed architecture decision. It intentionally includes only source-validated findings. If something cannot be validated from current source, it is recorded as a research question.
This analysis was validated against current local source in:
components/servo/servo.rscomponents/servo/webview.rscomponents/servo/webview_delegate.rscomponents/constellation/constellation.rscomponents/shared/embedder/lib.rsports/graphshell/running_app_state.rsports/graphshell/window.rsports/graphshell/desktop/gui.rsports/graphshell/desktop/webview_controller.rsports/graphshell/desktop/app.rs
-
WebViewDelegatecallbacks are dispatched from Servo's embedder-message handling path duringServo::spin_event_loop(). - Evidence chain:
-
Servo::spin_event_loop()->ServoInner::spin_event_loop()(components/servo/servo.rs). -
ServoInner::spin_event_loop()drainsembedder_receiverand callshandle_embedder_message(...). -
handle_embedder_message(...)invokesWebViewmutators and delegate callbacks.
-
Constraint for Graphshell: callback effects are part of the same event-loop pump that Graphshell drives via RunningAppState::spin_event_loop() (ports/graphshell/running_app_state.rs).
In ports/graphshell/running_app_state.rs:
- Implemented:
request_create_newnotify_page_title_changed-
notify_history_changed(currently onlyset_needs_update()) notify_load_status_changednotify_new_frame_readynotify_favicon_changed
- Not implemented:
-
notify_url_changedoverride (trait default no-op remains in effect)
-
Constraint for Graphshell: same-tab navigation semantics are not currently driven through notify_url_changed.
-
WebView::set_history(...)in Servo updates history and then calls:delegate.notify_url_changed(...)-
delegate.notify_history_changed(...)(components/servo/webview.rs).
Constraint for Graphshell: Servo provides explicit URL + history callbacks; polling webview.url() is not the only mechanism.
- Servo embedder message
EmbedderMsg::AllowOpeningWebViewtriggerswebview.request_create_new(...)(components/servo/servo.rs). - Graphshell handles
request_create_new(...)by building a newWebView, adding it to window collection, and activating it (unless webdriver mode) (ports/graphshell/running_app_state.rs).
Constraint for Graphshell: new-webview creation is currently wired at delegate level, but graph node creation is still not in that callback path.
-
webview_controller::sync_to_graph(...)inspectswindow.webviews()every update (ports/graphshell/desktop/webview_controller.rs). - On URL mismatch, it explicitly creates a new node and an edge (code comment: "Always create a NEW node for the new URL").
Constraint for Graphshell: current behavior models same-tab URL changes as structural graph growth.
Validated from ports/graphshell/desktop/app.rs and ports/graphshell/running_app_state.rs:
- Winit event -> headed window handlers -> then
pump_servo_event_loop(...). -
RunningAppState::spin_event_loop(...)does, in order:- window interface commands
- webdriver handling
servo.spin_event_loop()window.update_and_request_repaint_if_necessary(...)
Constraint for Graphshell: UI commands and Servo callbacks are already serialized by the app's event-loop pump; mutation discipline is an architecture choice, not an inferred thread-separation requirement.
Validated from components/shared/embedder/lib.rs and components/servo/servo.rs:
- Embedder-facing messages include
ChangePageTitle,HistoryChanged,NotifyLoadStatusChanged,NewFavicon,AllowOpeningWebView,WebViewClosed, etc. - Servo creates embedder channels with
crossbeam_channel::unbounded()increate_embedder_channel(...).
Constraint for Graphshell: message transport already exists and is wake-integrated via EventLoopWaker; Graphshell can consume signal-driven events without inventing new transport primitives.
Validated from components/constellation/constellation.rs docs:
- Declares can-block-on relation and warns about IPC send blocking.
- Recommends routing IPC receivers through router threads when non-blocking behavior is required.
Constraint for Graphshell: avoid introducing blocking IPC patterns in Graphshell integration paths; prefer existing generic/crossbeam mechanisms already used by embedder-facing code.
The following prior claims are not retained because they are not supported as stated:
- "WebViewDelegate callbacks run on compositor thread."
- Not validated from current dispatch path. Dispatch is through
Servo::spin_event_loop()processing.
- "Graphshell must introduce a cross-thread channel for delegate->GUI correctness."
- Not strictly validated as a correctness requirement from current call graph.
- A reducer/intent queue may still be desirable for determinism and conflict resolution.
- "Pipeline creation has fixed latency characteristics (e.g., hundreds of ms) and therefore requires pending-node state."
- No local benchmark evidence in current source audit.
Given validated callback availability (notify_url_changed, notify_history_changed, request_create_new) and current polling misbehavior in sync_to_graph:
- Same-tab navigation should update node URL/history in place.
- New-tab requests should create new nodes/edges explicitly.
- Back/forward should update traversal state/history, not create nodes.
Because Graphshell currently has multiple mutation sources (toolbar commands, tile interactions, graph interactions, Servo delegate updates), it should keep or adopt a single apply boundary (intent/reducer style).
This is justified by determinism and conflict resolution needs, not by an unverified callback-thread claim.
Validated runtime structure supports separation:
- Graph semantic model (node identity/lifecycle/edges)
- Tile/workspace presentation model (pane/tab arrangement)
- Runtime webview instances (Servo
WebViewhandles + rendering contexts)
This boundary reduces accidental coupling between presentation edits and semantic graph mutations.
Current Graphshell already leverages stable embedder APIs (WebViewDelegate, EmbedderMsg, WebViewBuilder) and ServoShellWindow abstraction.
Prefer extending Graphshell-side handling over patching Servo core unless there is a proven API gap.
The navigation fixes attempted so far did not resolve the behavior because they did not replace polling-driven node creation with delegate-driven updates. notify_url_changed remained unused as the authority for same-tab navigation, so sync_to_graph continued to interpret URL changes as new nodes. The conclusion is that the delegate callback path must become primary, and polling should be reduced to cleanup only. See NAVIGATION_NEXT_STEPS_OPTIONS.md for the options.
-
What is measured
request_create_new-> first usable paint latency across representative pages/hardware for Graphshell's headed mode? -
Under what workload does delegate-driven event handling require additional buffering/queueing beyond existing event-loop serialization?
-
What ordering guarantees (if any) are observed between
notify_url_changed,notify_page_title_changed, andnotify_history_changedfor complex navigations (redirects, SPA transitions, history traversal)? -
Do any Graphshell-specific callbacks currently perform heavy work that could regress input/paint latency within a single event-loop pump?
-
What is the minimal migration path to replace URL-polling structural mutation in
sync_to_graphwhile preserving existing tile remap cleanup behavior?
- Implement
notify_url_changedinports/graphshell/running_app_state.rsand route it to Graphshell graph-update logic. - Extend
notify_history_changedusage beyond UI invalidation. - Remove URL-change node-creation logic from
ports/graphshell/desktop/webview_controller.rs:sync_to_graph. - Keep stale mapping cleanup and tile remap cleanup, but separate them from semantic node-creation decisions.
Research only. This file intentionally avoids unvalidated assertions and should be read as a validated constraints baseline for follow-on architecture decisions.