Skip to main content

incular_devtools_protocol/
lib.rs

1//! Typed, versioned wire protocol between a running Incular application (the
2//! *target*) and Incular DevTools.
3//!
4//! This crate intentionally depends only on `serde`: it must stay usable by
5//! the standalone DevTools UI, the in-target agent, and offline tooling
6//! without pulling in the framework runtime or renderer.
7
8use serde::{Deserialize, Serialize};
9
10/// Oldest wire protocol that remains compatible with this implementation.
11/// Protocol 2 only adds typed property-edit requests, so protocol 1 clients
12/// can continue to inspect a protocol 2 target.
13pub const MIN_SUPPORTED_PROTOCOL_VERSION: u32 = 1;
14
15/// Current wire protocol version. Protocol 3 adds read-only runtime, semantics,
16/// layer and work-count inspection, rectangle highlighting, and session
17/// take-over. Protocol 4 adds editor metadata on [`DebugProperty`] (`options`,
18/// `min`, `max`, `step`) and [`DebugOption::PerformanceOverlay`]; all additions
19/// decode with defaults on older peers.
20pub const PROTOCOL_VERSION: u32 = 4;
21
22/// DevTools implementation version carried in the handshake for diagnostics.
23pub const DEVTOOLS_VERSION: &str = env!("CARGO_PKG_VERSION");
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum PeerKind {
28    Target,
29    Devtools,
30}
31
32/// First message sent by either side after the WebSocket opens. The target
33/// validates `auth_token` against its random session token before accepting
34/// any further frame.
35#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct Hello {
37    pub protocol_version: u32,
38    pub devtools_version: String,
39    pub kind: PeerKind,
40    pub auth_token: String,
41    /// Ask the target to end its active DevTools session in favour of this
42    /// client. Without it a second client receives [`ErrorCode::SessionBusy`].
43    #[serde(default)]
44    pub take_over: bool,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum ErrorCode {
50    ProtocolVersionMismatch,
51    Unauthorized,
52    UnknownId,
53    StaleId,
54    Unsupported,
55    InvalidRequest,
56    InternalError,
57    /// Another DevTools window is already inspecting this target.
58    SessionBusy,
59}
60
61// ---------------------------------------------------------------------------
62// Opaque identifiers: raw arena indices and pointers never cross the wire.
63// The generation field lets targets reject ids whose retained object was
64// destroyed and whose slot was reused.
65// ---------------------------------------------------------------------------
66
67macro_rules! opaque_id {
68    ($name:ident) => {
69        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
70        pub struct $name {
71            index: u64,
72            generation: u64,
73        }
74        impl $name {
75            pub const fn new(index: u64, generation: u64) -> Self {
76                Self { index, generation }
77            }
78            pub const fn index(&self) -> u64 {
79                self.index
80            }
81            pub const fn generation(&self) -> u64 {
82                self.generation
83            }
84        }
85        impl std::fmt::Display for $name {
86            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87                write!(
88                    f,
89                    "{}:{}:{}",
90                    stringify!($name),
91                    self.index,
92                    self.generation
93                )
94            }
95        }
96    };
97}
98
99opaque_id!(DevWindowId);
100opaque_id!(DevWidgetId);
101opaque_id!(DevRenderId);
102opaque_id!(DevLayerId);
103opaque_id!(DevSemanticsId);
104opaque_id!(DevSignalId);
105
106// ---------------------------------------------------------------------------
107// Structured diagnostic values (closed set; no arbitrary JSON).
108// ---------------------------------------------------------------------------
109
110#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum DebugValue {
113    Bool(bool),
114    Int(i64),
115    Uint(u64),
116    Float(f64),
117    /// Truncated string content; sensitive sources become [`Self::Redacted`].
118    Str(String),
119    Color(u8, u8, u8, u8),
120    Enum(String),
121    Size([f32; 2]),
122    Offset([f32; 2]),
123    Rect([f32; 4]),
124    Insets([f32; 4]),
125    Constraints {
126        min_width: f32,
127        /// JSON `null` represents an unbounded maximum, including in messages
128        /// emitted by older targets through serde's non-finite float encoding.
129        #[serde(with = "unbounded_maximum")]
130        max_width: f32,
131        min_height: f32,
132        #[serde(with = "unbounded_maximum")]
133        max_height: f32,
134    },
135    Optional(Option<Box<DebugValue>>),
136    List(Vec<DebugValue>),
137    Redacted,
138}
139
140mod animation_speed_payload {
141    use serde::{Deserialize, Deserializer, Serialize, Serializer};
142
143    #[derive(Serialize, Deserialize)]
144    struct Payload {
145        scale: f32,
146    }
147
148    pub(super) fn serialize<S: Serializer>(scale: &f32, serializer: S) -> Result<S::Ok, S::Error> {
149        Payload { scale: *scale }.serialize(serializer)
150    }
151
152    pub(super) fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f32, D::Error> {
153        Payload::deserialize(deserializer).map(|payload| payload.scale)
154    }
155}
156
157mod profiler_mode_payload {
158    use super::DevtoolsProfilerMode;
159    use serde::{Deserialize, Deserializer, Serialize, Serializer};
160
161    #[derive(Serialize, Deserialize)]
162    struct Payload {
163        mode: DevtoolsProfilerMode,
164    }
165
166    pub(super) fn serialize<S: Serializer>(
167        mode: &DevtoolsProfilerMode,
168        serializer: S,
169    ) -> Result<S::Ok, S::Error> {
170        Payload { mode: *mode }.serialize(serializer)
171    }
172
173    pub(super) fn deserialize<'de, D: Deserializer<'de>>(
174        deserializer: D,
175    ) -> Result<DevtoolsProfilerMode, D::Error> {
176        Payload::deserialize(deserializer).map(|payload| payload.mode)
177    }
178}
179
180mod unbounded_maximum {
181    use serde::{Deserialize, Deserializer, Serializer, ser::Error};
182
183    pub fn serialize<S: Serializer>(value: &f32, serializer: S) -> Result<S::Ok, S::Error> {
184        if value.is_finite() {
185            serializer.serialize_f32(*value)
186        } else if *value == f32::INFINITY {
187            serializer.serialize_none()
188        } else {
189            Err(S::Error::custom(
190                "constraint maximum must be finite or positive infinity",
191            ))
192        }
193    }
194
195    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f32, D::Error> {
196        Option::<f32>::deserialize(deserializer).map(|maximum| maximum.unwrap_or(f32::INFINITY))
197    }
198}
199
200#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
201pub struct DebugProperty {
202    pub name: String,
203    pub value: DebugValue,
204    /// `DEV OVERRIDE` active for this property.
205    #[serde(default)]
206    pub overridden: bool,
207    #[serde(default)]
208    pub editable: bool,
209    /// Allowed values for an editable [`DebugValue::Enum`] property (protocol 4).
210    #[serde(default, skip_serializing_if = "Vec::is_empty")]
211    pub options: Vec<String>,
212    /// Inclusive lower bound for an editable numeric property (protocol 4).
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub min: Option<f64>,
215    /// Inclusive upper bound for an editable numeric property (protocol 4).
216    #[serde(default, skip_serializing_if = "Option::is_none")]
217    pub max: Option<f64>,
218    /// Suggested increment for drag or arrow-key edits (protocol 4).
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    pub step: Option<f64>,
221}
222
223#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
224pub struct PropertyChange {
225    pub name: String,
226    pub old: Option<DebugValue>,
227    pub new: Option<DebugValue>,
228}
229
230#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
231#[serde(rename_all = "snake_case", tag = "kind")]
232pub enum InvalidationReason {
233    SignalWrite {
234        signal: DevSignalId,
235        name: String,
236        old: Option<String>,
237        new: Option<String>,
238    },
239    ParentReconciliation {
240        changed: Vec<PropertyChange>,
241    },
242    WidgetConfigurationChanged,
243    EnvironmentChanged {
244        field: String,
245        old: Option<String>,
246        new: Option<String>,
247    },
248    LocaleChanged,
249    WindowMetricsChanged,
250    ConstraintsChanged,
251    Animation,
252    TaskCompletion,
253    Navigation,
254    Restoration,
255    ManualInvalidation,
256    Mounted,
257}
258
259#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
260pub struct SourceLocation {
261    pub file: String,
262    pub line: u32,
263    pub column: u32,
264}
265
266// ---------------------------------------------------------------------------
267// Widget tree snapshot + incremental deltas.
268// ---------------------------------------------------------------------------
269
270#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
271pub struct WidgetNode {
272    pub id: DevWidgetId,
273    pub parent: Option<DevWidgetId>,
274    pub type_name: String,
275    pub key: Option<String>,
276    /// Short label for Text-like leaves (truncated; sensitive → redacted).
277    pub label: Option<String>,
278    pub child_ids: Vec<DevWidgetId>,
279    /// Bumped whenever inspected configuration changes; deltas compare this
280    /// instead of resending unchanged subtrees.
281    pub revision: u64,
282}
283
284#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case", tag = "kind")]
286pub enum TreeDelta {
287    Snapshot {
288        window: DevWindowId,
289        root: Box<WidgetNode>,
290        nodes: Vec<WidgetNode>,
291        truncated: bool,
292    },
293    Insert {
294        node: Box<WidgetNode>,
295    },
296    Remove {
297        id: DevWidgetId,
298    },
299    Move {
300        id: DevWidgetId,
301        parent: DevWidgetId,
302        position: usize,
303    },
304    Update {
305        node: Box<WidgetNode>,
306    },
307}
308
309// ---------------------------------------------------------------------------
310// Details, telemetry, signals.
311// ---------------------------------------------------------------------------
312
313#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
314pub struct ElementState {
315    pub mount_generation: u64,
316    pub builds: u64,
317    pub layouts: u64,
318    pub paints: u64,
319    pub composites: u64,
320    pub semantic_updates: u64,
321    pub constraints: Option<DebugValue>,
322    pub size: Option<[f32; 2]>,
323    pub offset: Option<[f32; 2]>,
324    pub world_bounds: Option<[f32; 4]>,
325    /// Resolved retained baseline, when this render object establishes one.
326    #[serde(default)]
327    pub baseline: Option<f32>,
328    /// The actual rectangular clip used by a retained clip layer, expressed
329    /// in the same target coordinate space as `world_bounds`.
330    #[serde(default)]
331    pub clip: Option<[f32; 4]>,
332}
333
334/// Read-only geometry captured from the retained layout result.  This is a
335/// snapshot, never a handle to a mutable render object; DevTools must not run
336/// layout again merely to answer an inspection request.
337#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
338pub struct LayoutInspection {
339    pub node: DevWidgetId,
340    pub parent: Option<DevWidgetId>,
341    pub incoming_constraints: Option<DebugValue>,
342    pub resolved_size: [f32; 2],
343    pub local_offset: [f32; 2],
344    /// Kurbo affine coefficients `[xx, yx, xy, yy, dx, dy]`.
345    pub local_transform: [f32; 6],
346    /// Retained world transform, expressed using Kurbo's coefficients.
347    pub world_transform: [f32; 6],
348    pub world_bounds: [f32; 4],
349    pub content_bounds: [f32; 4],
350    pub padding: Option<[f32; 4]>,
351    pub clip: Option<[f32; 4]>,
352    pub baseline: Option<f32>,
353    pub details: LayoutDetails,
354}
355
356#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
357pub struct LayoutHistoryEntry {
358    pub sequence: u64,
359    pub old_constraints: Option<DebugValue>,
360    pub new_constraints: DebugValue,
361    pub old_size: [f32; 2],
362    pub new_size: [f32; 2],
363    pub cause: Option<String>,
364}
365
366#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
367pub struct WorkReasons {
368    pub layout: Option<String>,
369    pub paint: Option<String>,
370    pub composite: Option<String>,
371}
372
373/// Layout-strategy-specific, compact inspection data.  Every coordinate and
374/// allocation comes from the completed retained layout pass; the DevTools UI
375/// only visualizes this data.
376#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
377#[serde(rename_all = "snake_case", tag = "kind")]
378pub enum LayoutDetails {
379    Box,
380    Flex {
381        axis: String,
382        available_main: Option<f32>,
383        non_flex_extent: f32,
384        flexible_extent: f32,
385        used_extent: f32,
386        remaining_extent: f32,
387        overflow: f32,
388        children: Vec<FlexChildInspection>,
389    },
390    Stack {
391        alignment: [f32; 2],
392        indexed_active: Option<usize>,
393        children: Vec<StackChildInspection>,
394    },
395    Positioned {
396        left: Option<f32>,
397        right: Option<f32>,
398        top: Option<f32>,
399        bottom: Option<f32>,
400        width: Option<f32>,
401        height: Option<f32>,
402    },
403    Transform {
404        matrix: [f32; 6],
405        determinant: f32,
406        invertible: bool,
407    },
408    Fitted {
409        fit: String,
410        alignment: [f32; 2],
411        source_size: Option<[f32; 2]>,
412        destination_size: [f32; 2],
413        matrix: [f32; 6],
414        determinant: f32,
415        invertible: bool,
416    },
417    Scroll {
418        axis: String,
419        viewport_extent: f32,
420        content_extent: f32,
421        offset: f32,
422        min_scroll: f32,
423        max_scroll: f32,
424    },
425    LazyViewport {
426        item_count: usize,
427        materialized_start: usize,
428        materialized_end: usize,
429        materialized_items: usize,
430        viewport_extent: f32,
431        cache_extent: f32,
432        scroll_offset: f32,
433    },
434    Text {
435        text_length: usize,
436        max_lines: Option<usize>,
437        overflow: String,
438        line_count: Option<usize>,
439    },
440    Custom {
441        layout_kind: String,
442    },
443}
444
445#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
446pub struct FlexChildInspection {
447    pub node: DevWidgetId,
448    pub index: usize,
449    pub type_name: String,
450    pub size: [f32; 2],
451    pub offset: [f32; 2],
452    pub flex: Option<u32>,
453    pub fit: Option<String>,
454    /// The actual max constraint delivered to this child in the main axis.
455    pub allocated_main_extent: Option<f32>,
456    pub actual_main_extent: f32,
457    pub cross_extent: f32,
458}
459
460#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
461pub struct StackChildInspection {
462    pub node: DevWidgetId,
463    pub index: usize,
464    pub type_name: String,
465    pub bounds: [f32; 4],
466    pub painted: bool,
467    pub left: Option<f32>,
468    pub right: Option<f32>,
469    pub top: Option<f32>,
470    pub bottom: Option<f32>,
471    pub width: Option<f32>,
472    pub height: Option<f32>,
473}
474
475#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
476pub struct NodeDetails {
477    pub id: DevWidgetId,
478    pub window: DevWindowId,
479    pub type_name: String,
480    pub key: Option<String>,
481    pub state: ElementState,
482    /// Exact retained layout state for the selected node.  This is optional
483    /// only for compatibility with targets that cannot expose a live render.
484    #[serde(default)]
485    pub layout: Option<LayoutInspection>,
486    /// Last relevant retained geometry changes, populated only while Deep
487    /// capture is active and bounded by the target.
488    #[serde(default)]
489    pub layout_history: Vec<LayoutHistoryEntry>,
490    #[serde(default)]
491    pub work_reasons: WorkReasons,
492    pub properties: Vec<DebugProperty>,
493    /// Bounded diff captured from the last compatible widget configuration
494    /// update. It uses only curated inspectable properties.
495    #[serde(default)]
496    pub property_changes: Vec<PropertyChange>,
497    /// Bounded coalesced invalidations observed before this selected node's
498    /// next BUILD. `invalidation` remains the latest cause for compatibility.
499    #[serde(default)]
500    pub invalidation_causes: Vec<InvalidationReason>,
501    /// Debug-visible signals this live element actually consumed during its
502    /// current reactive subscription. Stale/unmounted entries are omitted.
503    #[serde(default)]
504    pub consumed_signals: Vec<DevSignalId>,
505    pub invalidation: Option<InvalidationReason>,
506    pub render: Option<DevRenderId>,
507    pub semantics: Option<DevSemanticsId>,
508    pub source: Option<SourceLocation>,
509}
510
511#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
512pub struct FrameTimingsWire {
513    pub event_processing: u32,
514    pub runtime_messages: u32,
515    pub build: u32,
516    pub layout: u32,
517    pub composite: u32,
518    pub semantics: u32,
519    pub paint: u32,
520    pub cpu_total: u32,
521    pub prepare: u32,
522    pub encode: u32,
523    pub submit: u32,
524    /// GPU main-pass time in microseconds when timestamp queries resolve.
525    pub gpu_us: Option<f64>,
526}
527
528#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
529pub struct FrameRecordEvent {
530    pub window: DevWindowId,
531    pub frame: u64,
532    pub timings: FrameTimingsWire,
533    /// Actual monitor-derived CPU frame budget when the platform exposes a
534    /// refresh rate. `None` means the target cannot report one.
535    #[serde(default)]
536    pub budget_us: Option<u32>,
537    pub over_budget: bool,
538    pub draw_calls: u32,
539    pub instances: u32,
540    pub upload_bytes: u64,
541    pub pipelines_created: u32,
542}
543
544#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
545#[serde(rename_all = "snake_case")]
546pub enum DevtoolsProfilerMode {
547    #[default]
548    Basic,
549    Performance,
550    Deep,
551}
552
553#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
554#[serde(rename_all = "snake_case")]
555pub enum TracePhase {
556    Build,
557    Layout,
558    Paint,
559    Semantics,
560    Composite,
561}
562
563/// One real, hierarchical per-node timing recorded only in Deep mode.
564#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
565pub struct TraceEvent {
566    pub node: DevWidgetId,
567    pub phase: TracePhase,
568    pub parent: Option<u32>,
569    pub start_us: u32,
570    pub duration_us: u32,
571}
572
573/// Bounded trace batch for one target/window frame. `truncated` and
574/// `dropped_events` make partial data impossible to mistake for a full trace.
575#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
576pub struct DeepFrameTrace {
577    pub window: DevWindowId,
578    pub frame: u64,
579    pub events: Vec<TraceEvent>,
580    pub truncated: bool,
581    pub dropped_events: u32,
582}
583
584#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
585pub struct ResourceCounts {
586    pub elements: usize,
587    pub render_objects: usize,
588    pub layers: usize,
589    pub semantics_nodes: usize,
590    pub signals: usize,
591    pub tasks_active: usize,
592    pub glyph_atlas_pages: usize,
593    pub image_resources: usize,
594    pub gradient_resources: usize,
595    pub path_meshes: usize,
596    pub offscreen_bytes: usize,
597    pub effect_cached_bytes: usize,
598    pub rss_mb: usize,
599}
600
601#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
602pub struct MemorySnapshot {
603    pub label: String,
604    pub counts: ResourceCounts,
605}
606
607#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
608pub struct SignalSummary {
609    pub id: DevSignalId,
610    pub name: Option<String>,
611    pub type_name: String,
612    /// Monotonic value generation; increments only after a real signal write.
613    #[serde(default)]
614    pub generation: u64,
615    pub write_count: u64,
616    pub subscriber_count: usize,
617    pub last_write_summary: Option<String>,
618    pub editable: bool,
619}
620
621#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
622pub struct SignalSubscriber {
623    pub signal: DevSignalId,
624    pub element: DevWidgetId,
625    pub path: String,
626}
627
628#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
629#[serde(rename_all = "snake_case")]
630pub enum DebugOption {
631    LayoutBounds,
632    LayoutBoundsSubtree,
633    LayoutBoundsWholeWindow,
634    PaddingContent,
635    Baselines,
636    Clips,
637    HitTestRegions,
638    SemanticsBounds,
639    ScrollViewports,
640    LayerBoundaries,
641    RepaintRainbow,
642    FlashRebuilds,
643    HighlightBuild,
644    HighlightLayout,
645    HighlightPaint,
646    HighlightSemantics,
647    HighlightComposite,
648    HighlightOversizedImages,
649    /// In-app frame-time graph drawn in the window's top-right (protocol 4).
650    PerformanceOverlay,
651}
652
653#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
654#[serde(rename_all = "snake_case")]
655pub enum CacheKind {
656    TextLayouts,
657    GlyphAtlas,
658    Images,
659    Paths,
660    Gradients,
661    Offscreen,
662}
663
664#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
665#[serde(rename_all = "snake_case")]
666pub enum EditableValue {
667    Bool(bool),
668    Int(i64),
669    Uint(u64),
670    Float(f64),
671    Str(String),
672    Color(u8, u8, u8, u8),
673}
674
675#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
676pub struct CacheReport {
677    pub which: CacheKind,
678    pub entries: usize,
679    pub hits: u64,
680    pub misses: u64,
681}
682
683// ---------------------------------------------------------------------------
684// Envelope: requests, responses, target events.
685// ---------------------------------------------------------------------------
686
687/// DevTools → target. Every request produces exactly one
688/// [`ResponsePayload`] correlated by `request_id`.
689#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
690#[serde(rename_all = "snake_case", tag = "method")]
691pub enum RequestMethod {
692    GetTargetInfo,
693    GetWidgetTree {
694        window: DevWindowId,
695    },
696    GetNodeDetails {
697        id: DevWidgetId,
698    },
699    EditProperty {
700        id: DevWidgetId,
701        name: String,
702        value: DebugValue,
703    },
704    StartInspectMode {
705        window: DevWindowId,
706    },
707    StopInspectMode {
708        window: DevWindowId,
709    },
710    HighlightNode {
711        window: DevWindowId,
712        id: Option<DevWidgetId>,
713    },
714    SetDebugOption {
715        name: DebugOption,
716        enabled: bool,
717    },
718    SetProfilerMode {
719        mode: DevtoolsProfilerMode,
720    },
721    StartRecording,
722    StopRecording,
723    TakeMemorySnapshot {
724        label: String,
725    },
726    ListSignals,
727    GetSignalSubscribers {
728        id: DevSignalId,
729    },
730    EditSignal {
731        id: DevSignalId,
732        value: EditableValue,
733    },
734    ResetOverrides,
735    ClearCache {
736        which: CacheKind,
737    },
738    SetAnimationSpeed {
739        scale: f32,
740    },
741    /// Cumulative text, widget pipeline, semantics and compositor counters.
742    GetRuntimeDiagnostics {
743        window: DevWindowId,
744    },
745    /// Bounded snapshot of the platform-neutral semantics tree.
746    GetSemanticsTree {
747        window: DevWindowId,
748    },
749    /// Bounded snapshot of the retained compositor layer tree.
750    GetLayerTree {
751        window: DevWindowId,
752    },
753    /// Per-element work counters ranked by build count (`limit` <= 200).
754    GetWorkCounts {
755        window: DevWindowId,
756        limit: u32,
757    },
758    /// Draws (or with `None` clears) a selection overlay over a window-space
759    /// rectangle `[x, y, width, height]`.
760    HighlightRect {
761        window: DevWindowId,
762        rect: Option<[f32; 4]>,
763    },
764}
765
766/// Retained widget pipeline counters; cumulative since process start.
767#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
768#[serde(default)]
769pub struct WidgetPipelineCounters {
770    pub mounts: u64,
771    pub unmounts: u64,
772    pub rebuilds: u64,
773    pub layouts: u64,
774    pub paints: u64,
775    pub composites: u64,
776    pub animation_ticks: u64,
777    pub scroll_offset_updates: u64,
778    pub reconciliation_fast_paths: u64,
779    pub layout_cache_hits: u64,
780    pub display_lists_reused: u64,
781    pub compositor_only_updates: u64,
782    pub lazy_layouts: u64,
783    pub items_built: u64,
784    pub items_reused: u64,
785    pub child_list_scans: u64,
786    pub identical_child_bailouts: u64,
787    pub elements_created: u64,
788    pub elements_removed: u64,
789    pub elements_moved: u64,
790    pub dirty_requests: u64,
791    pub dirty_queue_deduplicated: u64,
792    pub elements_total: u64,
793    pub render_objects_total: u64,
794    pub layers_total: u64,
795}
796
797/// Mirror of the text engine's cache diagnostics (cumulative).
798#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
799#[serde(default)]
800pub struct TextCacheCounters {
801    pub layouts_requested: u64,
802    pub cache_hits: u64,
803    pub cache_misses: u64,
804    pub font_resolution_requests: u64,
805    pub font_resolution_cache_hits: u64,
806    pub font_resolution_cache_misses: u64,
807    pub coverage_queries: u64,
808    pub fallback_runs: u64,
809    pub fallback_fonts_used: u64,
810    pub missing_clusters: u64,
811    pub shaping_runs: u64,
812    pub paragraphs_considered: u64,
813    pub paragraphs_reshaped: u64,
814    pub parley_layouts_reused: u64,
815    pub documents_composed: u64,
816    pub documents_reused: u64,
817    pub width_reuses: u64,
818    pub cache_growths: u64,
819    pub cache_shrinks: u64,
820    pub paragraphs_carried: u64,
821    pub shapes_reused: u64,
822}
823
824/// Semantics tree counters (`nodes` is the live node count; others cumulative).
825#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
826#[serde(default)]
827pub struct SemanticsCounters {
828    pub nodes: u64,
829    pub nodes_created: u64,
830    pub nodes_removed: u64,
831    pub nodes_updated: u64,
832    pub property_updates: u64,
833    pub geometry_updates: u64,
834    pub native_updates: u64,
835    pub actions_received: u64,
836    pub revisions: u64,
837}
838
839/// Compositor counters (`layers` is cumulative picture layers created).
840#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
841#[serde(default)]
842pub struct CompositorCounters {
843    pub layers: u64,
844    pub picture_layers_repainted: u64,
845    pub transform_updates: u64,
846    pub clip_updates: u64,
847    pub layers_culled: u64,
848    pub opacity_updates: u64,
849    pub leader_publish_passes: u64,
850    pub leaders_published: u64,
851}
852
853#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
854#[serde(default)]
855pub struct RuntimeDiagnosticsWire {
856    pub widgets: WidgetPipelineCounters,
857    pub text: TextCacheCounters,
858    pub semantics: SemanticsCounters,
859    pub compositor: CompositorCounters,
860}
861
862#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
863#[serde(default)]
864pub struct SemanticsNodeWire {
865    pub id: u64,
866    pub parent: Option<u64>,
867    pub children: Vec<u64>,
868    pub role: String,
869    pub label: Option<String>,
870    pub value: Option<String>,
871    pub description: Option<String>,
872    pub bounds: [f32; 4],
873    /// Names of the state flags that are set.
874    pub flags: Vec<String>,
875    pub actions: Vec<String>,
876}
877
878#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
879#[serde(default)]
880pub struct LayerNodeWire {
881    pub id: u64,
882    pub parent: Option<u64>,
883    pub children: Vec<u64>,
884    pub kind: String,
885    pub summary: String,
886    pub bounds: Option<[f32; 4]>,
887}
888
889#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
890pub struct WorkCountEntry {
891    pub id: DevWidgetId,
892    pub type_name: String,
893    #[serde(default)]
894    pub builds: u64,
895    #[serde(default)]
896    pub layouts: u64,
897    #[serde(default)]
898    pub paints: u64,
899    #[serde(default)]
900    pub semantics: u64,
901    #[serde(default)]
902    pub composites: u64,
903}
904
905#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
906#[serde(rename_all = "snake_case", tag = "result")]
907pub enum ResponsePayload {
908    Ok,
909    TargetInfo(Box<TargetInfo>),
910    WidgetTree {
911        deltas: Vec<TreeDelta>,
912        tree_revision: u64,
913    },
914    NodeDetails(Box<NodeDetails>),
915    RecordingStarted,
916    RecordingStopped {
917        frames: u64,
918    },
919    MemorySnapshot(MemorySnapshot),
920    Signals {
921        signals: Vec<SignalSummary>,
922    },
923    SignalSubscribers {
924        subscribers: Vec<SignalSubscriber>,
925    },
926    Edited,
927    OverridesReset,
928    CacheCleared(CacheReport),
929    /// Encoded as `{"scale": f32}`: an internally tagged enum cannot carry
930    /// a bare number, so the plain newtype form never serialized.
931    AnimationSpeedSet(#[serde(with = "animation_speed_payload")] f32),
932    /// Encoded as `{"mode": …}` for the same reason as `AnimationSpeedSet`.
933    ProfilerModeSet(#[serde(with = "profiler_mode_payload")] DevtoolsProfilerMode),
934    RuntimeDiagnostics(Box<RuntimeDiagnosticsWire>),
935    SemanticsTree {
936        window: DevWindowId,
937        nodes: Vec<SemanticsNodeWire>,
938        truncated: bool,
939    },
940    LayerTree {
941        window: DevWindowId,
942        layers: Vec<LayerNodeWire>,
943        truncated: bool,
944    },
945    WorkCounts {
946        window: DevWindowId,
947        entries: Vec<WorkCountEntry>,
948    },
949    Error {
950        code: ErrorCode,
951        message: String,
952    },
953}
954
955/// Target → DevTools unsolicited events (bounded queues apply upstream).
956#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
957#[serde(rename_all = "snake_case", tag = "event")]
958pub enum TargetEvent {
959    /// Incremental retained-tree update for a window that the client has
960    /// explicitly inspected. Targets never serialize these while no client
961    /// has subscribed to a tree.
962    WidgetTreeDeltas {
963        window: DevWindowId,
964        deltas: Vec<TreeDelta>,
965        tree_revision: u64,
966    },
967    FrameRecord(FrameRecordEvent),
968    DeepTrace(DeepFrameTrace),
969    WidgetSelectedByUser {
970        window: DevWindowId,
971        id: DevWidgetId,
972    },
973    Log {
974        level: String,
975        target: String,
976        message: String,
977    },
978    WindowsChanged,
979    DroppedTelemetry {
980        count: u64,
981    },
982    InspectModeEnded {
983        window: DevWindowId,
984    },
985}
986
987/// One wire frame after the handshake.
988#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
989#[serde(rename_all = "snake_case", tag = "type")]
990pub enum Message {
991    Request {
992        request_id: u64,
993        body: RequestMethod,
994    },
995    Response {
996        request_id: u64,
997        payload: Result<ResponsePayload, ErrorCode>,
998    },
999    Event(TargetEvent),
1000    /// Human-readable rejection during handshake; the peer closes after.
1001    Rejection {
1002        code: ErrorCode,
1003        message: String,
1004    },
1005}
1006
1007#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1008pub struct TargetInfo {
1009    pub protocol_version: u32,
1010    pub framework_version: String,
1011    pub pid: u32,
1012    pub executable: String,
1013    pub platform: String,
1014    pub windows: Vec<WindowSummary>,
1015}
1016
1017#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1018pub struct WindowSummary {
1019    pub id: DevWindowId,
1020    pub title: String,
1021    pub logical_size: [f32; 2],
1022    pub scale_factor: f64,
1023}
1024
1025/// Per-user discovery record written by devtools-enabled targets so the
1026/// DevTools launcher can list running applications without port scanning.
1027#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1028pub struct DiscoveryRecord {
1029    pub pid: u32,
1030    pub app_name: String,
1031    pub port: u16,
1032    /// Session token; the discovery file lives in a user-only directory, and
1033    /// DevTools still authenticates over the socket before any data flows.
1034    pub auth_token: String,
1035    pub started_unix_ms: u64,
1036    pub protocol_version: u32,
1037}
1038
1039/// Validates a handshake from the *other* side. Targets call this with the
1040/// client hello (expecting [`PeerKind::Devtools`]) and vice versa.
1041pub fn check_hello(
1042    hello: &Hello,
1043    expected_kind: PeerKind,
1044    expected_token: Option<&str>,
1045) -> Result<(), ErrorCode> {
1046    if !(MIN_SUPPORTED_PROTOCOL_VERSION..=PROTOCOL_VERSION).contains(&hello.protocol_version) {
1047        return Err(ErrorCode::ProtocolVersionMismatch);
1048    }
1049    if let Some(token) = expected_token {
1050        // Constant-time-ish comparison: equal-length early exit leaks only
1051        // length, which the wire already reveals.
1052        if !token_eq(&hello.auth_token, token) {
1053            return Err(ErrorCode::Unauthorized);
1054        }
1055    }
1056    if hello.kind != expected_kind {
1057        return Err(ErrorCode::InvalidRequest);
1058    }
1059    Ok(())
1060}
1061
1062fn token_eq(a: &str, b: &str) -> bool {
1063    let (a, b) = (a.as_bytes(), b.as_bytes());
1064    if a.len() != b.len() {
1065        return false;
1066    }
1067    let mut diff = 0u8;
1068    for (x, y) in a.iter().zip(b.iter()) {
1069        diff |= x ^ y;
1070    }
1071    diff == 0
1072}