Skip to main content

sim_lib_view_daw/
placement.rs

1//! Placement report inspector Scene builders.
2
3use sim_kernel::{Expr, Symbol};
4use sim_lib_scene::{data_map, node, sym};
5use sim_lib_stream_core::BridgeLatency;
6use sim_lib_topology::{
7    DomainBridge, PlacedNode, PlacementRefusal, PlacementRefusalReason, PlacementReport,
8    PortLatency,
9};
10use sim_lib_web_bridge::BrowserPlacementReport;
11use sim_value::build::uint;
12
13/// Stable lens id for the full placement inspector.
14pub const PLACEMENT_INSPECTOR_VIEW_ID: &str = "view:placement-inspector";
15/// Stable lens id for the placed graph panel.
16pub const PLACEMENT_GRAPH_VIEW_ID: &str = "view:placement-graph";
17/// Stable lens id for the bridge table.
18pub const PLACEMENT_BRIDGE_TABLE_VIEW_ID: &str = "view:placement-bridge-table";
19/// Stable lens id for the latency-budget table.
20pub const PLACEMENT_LATENCY_BUDGET_VIEW_ID: &str = "view:placement-latency-budget";
21/// Stable lens id for placement refusal diagnostics.
22pub const PLACEMENT_REFUSAL_TABLE_VIEW_ID: &str = "view:placement-refusals";
23/// Stable lens id for browser-wasm placement diagnostics.
24pub const PLACEMENT_BROWSER_DIAGNOSTICS_VIEW_ID: &str = "view:placement-browser-diagnostics";
25/// Stable lens id for bounded runtime diagnostics.
26pub const PLACEMENT_RUNTIME_DIAGNOSTICS_VIEW_ID: &str = "view:placement-runtime-diagnostics";
27/// Stable lens id for deterministic placement fault fixtures.
28pub const PLACEMENT_FAULT_TIMELINE_VIEW_ID: &str = "view:placement-fault-timeline";
29
30/// Fault fixture name for a forced placement refusal.
31pub const PLACEMENT_FORCED_REFUSAL_FAULT: &str = "forced-refusal";
32/// Fault fixture name for a latency jitter spike.
33pub const PLACEMENT_JITTER_SPIKE_FAULT: &str = "jitter-spike";
34/// Fault fixture name for a stalled worker.
35pub const PLACEMENT_WORKER_STALL_FAULT: &str = "worker-stall";
36/// Fault fixture name for a site disconnect.
37pub const PLACEMENT_DISCONNECT_FAULT: &str = "disconnect";
38
39/// Bounded diagnostic counter accepted from a runtime or browser bridge.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct PlacementRuntimeDiagnostic {
42    source: Symbol,
43    diagnostic: Symbol,
44    count: u64,
45}
46
47impl PlacementRuntimeDiagnostic {
48    /// Builds a bounded diagnostic counter for the given source and code.
49    pub fn new(source: Symbol, diagnostic: Symbol, count: u64) -> Self {
50        Self {
51            source,
52            diagnostic,
53            count,
54        }
55    }
56
57    /// Returns the source that reported this diagnostic.
58    pub fn source(&self) -> &Symbol {
59        &self.source
60    }
61
62    /// Returns the diagnostic code.
63    pub fn diagnostic(&self) -> &Symbol {
64        &self.diagnostic
65    }
66
67    /// Returns the accumulated count for this diagnostic.
68    pub fn count(&self) -> u64 {
69        self.count
70    }
71}
72
73/// Deterministic placement fault fixture rendered by the inspector.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct PlacementFaultFixture {
76    name: Symbol,
77    response: Symbol,
78    diagnostics: Vec<PlacementRuntimeDiagnostic>,
79}
80
81impl PlacementFaultFixture {
82    /// Builds a fault fixture with the given name, response, and diagnostics.
83    pub fn new(
84        name: Symbol,
85        response: Symbol,
86        diagnostics: Vec<PlacementRuntimeDiagnostic>,
87    ) -> Self {
88        Self {
89            name,
90            response,
91            diagnostics,
92        }
93    }
94
95    /// Returns the fixture name.
96    pub fn name(&self) -> &Symbol {
97        &self.name
98    }
99
100    /// Returns the simulated runtime response for this fault.
101    pub fn response(&self) -> &Symbol {
102        &self.response
103    }
104
105    /// Returns the diagnostics emitted by this fault fixture.
106    pub fn diagnostics(&self) -> &[PlacementRuntimeDiagnostic] {
107        &self.diagnostics
108    }
109}
110
111/// Returns the names of the built-in placement fault fixtures.
112pub fn placement_fault_fixture_names() -> [&'static str; 4] {
113    [
114        PLACEMENT_FORCED_REFUSAL_FAULT,
115        PLACEMENT_JITTER_SPIKE_FAULT,
116        PLACEMENT_WORKER_STALL_FAULT,
117        PLACEMENT_DISCONNECT_FAULT,
118    ]
119}
120
121/// Returns the named placement fault fixture, if it exists.
122pub fn placement_fault_fixture(name: &str) -> Option<PlacementFaultFixture> {
123    let fixture = match name {
124        PLACEMENT_FORCED_REFUSAL_FAULT => fault_fixture(name, "refused", 1),
125        PLACEMENT_JITTER_SPIKE_FAULT => fault_fixture(name, "jitter-buffered", 3),
126        PLACEMENT_WORKER_STALL_FAULT => fault_fixture(name, "worker-restarted", 1),
127        PLACEMENT_DISCONNECT_FAULT => fault_fixture(name, "site-disconnected", 1),
128        _ => return None,
129    };
130    Some(fixture)
131}
132
133/// Renders a placement report without runtime or browser-side diagnostics.
134pub fn placement_inspector_view(report: &PlacementReport) -> Expr {
135    placement_inspector_view_with_diagnostics(report, &[], &[], &[])
136}
137
138/// Renders a placement report with bounded runtime, browser, and fault data.
139pub fn placement_inspector_view_with_diagnostics(
140    report: &PlacementReport,
141    browser_reports: &[BrowserPlacementReport],
142    runtime_diagnostics: &[PlacementRuntimeDiagnostic],
143    faults: &[PlacementFaultFixture],
144) -> Expr {
145    node(
146        "stack",
147        vec![
148            ("lens", sym(PLACEMENT_INSPECTOR_VIEW_ID)),
149            ("role", sym("placement-inspector")),
150            ("dir", sym("column")),
151            ("accepted", Expr::Bool(report.is_accepted())),
152            (
153                "children",
154                Expr::List(vec![
155                    placement_graph_view(report),
156                    bridge_table_view(&report.bridges),
157                    latency_budget_view(&report.latency),
158                    refusal_table_view(&report.refusals),
159                    runtime_diagnostics_view(runtime_diagnostics),
160                    browser_diagnostics_view(browser_reports),
161                    fault_timeline_view(faults),
162                ]),
163            ),
164        ],
165    )
166}
167
168fn placement_graph_view(report: &PlacementReport) -> Expr {
169    node(
170        "graph",
171        vec![
172            ("lens", sym(PLACEMENT_GRAPH_VIEW_ID)),
173            ("role", sym("placement-graph")),
174            (
175                "nodes",
176                Expr::List(report.placed.iter().map(placed_node_view).collect()),
177            ),
178            (
179                "edges",
180                Expr::List(report.bridges.iter().map(bridge_edge_view).collect()),
181            ),
182        ],
183    )
184}
185
186fn placed_node_view(placed: &PlacedNode) -> Expr {
187    node(
188        "node",
189        vec![
190            ("id", Expr::Symbol(placed.node.as_symbol().clone())),
191            (
192                "title",
193                Expr::String(placed.node.as_symbol().as_qualified_str()),
194            ),
195            ("site", Expr::Symbol(placed.site.as_symbol().clone())),
196            ("clock-domain", Expr::Symbol(placed.clock_domain.symbol())),
197            ("latency-class", Expr::Symbol(placed.latency_class.symbol())),
198            ("realtime-pin", Expr::Bool(placed.realtime_pin)),
199            (
200                "status",
201                node(
202                    "badge",
203                    vec![
204                        (
205                            "status",
206                            sym(if placed.realtime_pin {
207                                "realtime"
208                            } else {
209                                "placed"
210                            }),
211                        ),
212                        (
213                            "label",
214                            Expr::String(if placed.realtime_pin {
215                                "realtime".to_owned()
216                            } else {
217                                "placed".to_owned()
218                            }),
219                        ),
220                    ],
221                ),
222            ),
223        ],
224    )
225}
226
227fn bridge_edge_view(bridge: &DomainBridge) -> Expr {
228    node(
229        "edge",
230        vec![
231            ("id", uint(u64::from(bridge.edge.0))),
232            ("from", Expr::Symbol(bridge.from.as_symbol().clone())),
233            ("to", Expr::Symbol(bridge.to.as_symbol().clone())),
234            (
235                "from-site",
236                Expr::Symbol(bridge.from_site.as_symbol().clone()),
237            ),
238            ("to-site", Expr::Symbol(bridge.to_site.as_symbol().clone())),
239            (
240                "bridge-kind",
241                Expr::Symbol(bridge.descriptor.kind().symbol()),
242            ),
243            (
244                "bridge-diagnostics",
245                Expr::List(
246                    bridge
247                        .descriptor
248                        .diagnostics()
249                        .iter()
250                        .cloned()
251                        .map(Expr::Symbol)
252                        .collect(),
253                ),
254            ),
255            ("latency", latency_value(bridge.descriptor.latency())),
256        ],
257    )
258}
259
260fn bridge_table_view(bridges: &[DomainBridge]) -> Expr {
261    node(
262        "table",
263        vec![
264            ("lens", sym(PLACEMENT_BRIDGE_TABLE_VIEW_ID)),
265            ("role", sym("placement-bridge-table")),
266            (
267                "bridges",
268                Expr::List(bridges.iter().map(bridge_row).collect()),
269            ),
270        ],
271    )
272}
273
274fn bridge_row(bridge: &DomainBridge) -> Expr {
275    data_map(vec![
276        ("edge", uint(u64::from(bridge.edge.0))),
277        ("from", Expr::Symbol(bridge.from.as_symbol().clone())),
278        ("to", Expr::Symbol(bridge.to.as_symbol().clone())),
279        (
280            "from-site",
281            Expr::Symbol(bridge.from_site.as_symbol().clone()),
282        ),
283        ("to-site", Expr::Symbol(bridge.to_site.as_symbol().clone())),
284        (
285            "bridge-kind",
286            Expr::Symbol(bridge.descriptor.kind().symbol()),
287        ),
288        (
289            "bridge-name",
290            Expr::String(bridge.descriptor.name().to_owned()),
291        ),
292        (
293            "diagnostics",
294            Expr::List(
295                bridge
296                    .descriptor
297                    .diagnostics()
298                    .iter()
299                    .cloned()
300                    .map(Expr::Symbol)
301                    .collect(),
302            ),
303        ),
304        ("latency", latency_value(bridge.descriptor.latency())),
305    ])
306}
307
308fn latency_budget_view(latencies: &[PortLatency]) -> Expr {
309    node(
310        "table",
311        vec![
312            ("lens", sym(PLACEMENT_LATENCY_BUDGET_VIEW_ID)),
313            ("role", sym("placement-latency-budget")),
314            (
315                "latency",
316                Expr::List(latencies.iter().map(latency_row).collect()),
317            ),
318        ],
319    )
320}
321
322fn latency_row(latency: &PortLatency) -> Expr {
323    data_map(vec![
324        ("node", Expr::Symbol(latency.node.as_symbol().clone())),
325        ("site", Expr::Symbol(latency.site.as_symbol().clone())),
326        ("latency", latency_value(latency.latency)),
327        (
328            "latency-class",
329            Expr::Symbol(latency.latency_class.symbol()),
330        ),
331    ])
332}
333
334fn refusal_table_view(refusals: &[PlacementRefusal]) -> Expr {
335    node(
336        "table",
337        vec![
338            ("lens", sym(PLACEMENT_REFUSAL_TABLE_VIEW_ID)),
339            ("role", sym("placement-refusals")),
340            (
341                "refusals",
342                Expr::List(refusals.iter().map(refusal_row).collect()),
343            ),
344        ],
345    )
346}
347
348fn refusal_row(refusal: &PlacementRefusal) -> Expr {
349    data_map(vec![
350        ("node", Expr::Symbol(refusal.node.as_symbol().clone())),
351        ("site", Expr::Symbol(refusal.site.as_symbol().clone())),
352        (
353            "reason",
354            Expr::Symbol(refusal_reason_symbol(&refusal.reason)),
355        ),
356    ])
357}
358
359fn runtime_diagnostics_view(diagnostics: &[PlacementRuntimeDiagnostic]) -> Expr {
360    node(
361        "table",
362        vec![
363            ("lens", sym(PLACEMENT_RUNTIME_DIAGNOSTICS_VIEW_ID)),
364            ("role", sym("placement-runtime-diagnostics")),
365            (
366                "diagnostics",
367                Expr::List(diagnostics.iter().map(runtime_diagnostic_row).collect()),
368            ),
369        ],
370    )
371}
372
373fn runtime_diagnostic_row(diagnostic: &PlacementRuntimeDiagnostic) -> Expr {
374    data_map(vec![
375        ("source", Expr::Symbol(diagnostic.source().clone())),
376        ("diagnostic", Expr::Symbol(diagnostic.diagnostic().clone())),
377        ("count", uint(diagnostic.count())),
378    ])
379}
380
381fn browser_diagnostics_view(reports: &[BrowserPlacementReport]) -> Expr {
382    node(
383        "table",
384        vec![
385            ("lens", sym(PLACEMENT_BROWSER_DIAGNOSTICS_VIEW_ID)),
386            ("role", sym("placement-browser-diagnostics")),
387            (
388                "reports",
389                Expr::List(reports.iter().map(browser_report_row).collect()),
390            ),
391        ],
392    )
393}
394
395fn browser_report_row(report: &BrowserPlacementReport) -> Expr {
396    data_map(vec![
397        ("fragment", Expr::Symbol(report.fragment_id().clone())),
398        ("site", Expr::Symbol(report.site().clone())),
399        ("engine", Expr::Symbol(report.engine().id().clone())),
400        (
401            "lanes",
402            Expr::List(
403                report
404                    .lanes()
405                    .iter()
406                    .map(|lane| Expr::Symbol(lane.symbol()))
407                    .collect(),
408            ),
409        ),
410        (
411            "diagnostics",
412            Expr::List(
413                report
414                    .diagnostics()
415                    .iter()
416                    .cloned()
417                    .map(Expr::Symbol)
418                    .collect(),
419            ),
420        ),
421        ("outputs", uint(report.output_envelopes().len() as u64)),
422    ])
423}
424
425fn fault_timeline_view(faults: &[PlacementFaultFixture]) -> Expr {
426    node(
427        "timeline",
428        vec![
429            ("lens", sym(PLACEMENT_FAULT_TIMELINE_VIEW_ID)),
430            ("role", sym("placement-fault-timeline")),
431            ("lane", sym("placement-faults")),
432            (
433                "events",
434                Expr::List(
435                    faults
436                        .iter()
437                        .enumerate()
438                        .map(|(index, fault)| fault_event(index, fault))
439                        .collect(),
440                ),
441            ),
442        ],
443    )
444}
445
446fn fault_event(index: usize, fault: &PlacementFaultFixture) -> Expr {
447    data_map(vec![
448        ("at", uint(index as u64)),
449        ("fault", Expr::Symbol(fault.name().clone())),
450        ("response", Expr::Symbol(fault.response().clone())),
451        (
452            "diagnostics",
453            Expr::List(
454                fault
455                    .diagnostics()
456                    .iter()
457                    .map(runtime_diagnostic_row)
458                    .collect(),
459            ),
460        ),
461    ])
462}
463
464fn fault_fixture(name: &str, response: &str, count: u64) -> PlacementFaultFixture {
465    PlacementFaultFixture::new(
466        Symbol::qualified("placement/fault", name),
467        Symbol::qualified("placement/fault-response", response),
468        vec![PlacementRuntimeDiagnostic::new(
469            Symbol::qualified("placement/fault-source", name),
470            Symbol::qualified("placement/fault-diagnostic", name),
471            count,
472        )],
473    )
474}
475
476fn latency_value(latency: BridgeLatency) -> Expr {
477    data_map(vec![
478        ("frames", uint(latency.frame_count())),
479        ("packets", uint(u64::from(latency.packet_count()))),
480    ])
481}
482
483fn refusal_reason_symbol(reason: &PlacementRefusalReason) -> Symbol {
484    let name = match reason {
485        PlacementRefusalReason::UnknownSite => "unknown-site",
486        PlacementRefusalReason::RealtimePinViolation => "realtime-pin-violation",
487        PlacementRefusalReason::UnsupportedLatencyClass => "unsupported-latency-class",
488        #[allow(unreachable_patterns)]
489        other => fallback_refusal_reason(other),
490    };
491    Symbol::qualified("placement/refusal", name)
492}
493
494fn fallback_refusal_reason(reason: &PlacementRefusalReason) -> &'static str {
495    let debug = format!("{reason:?}");
496    if debug.starts_with("UnsupportedClockDomain") {
497        "unsupported-clock-domain"
498    } else if debug.starts_with("UnsupportedStreamPorts") {
499        "unsupported-stream-ports"
500    } else {
501        "incomparable-clock-domain"
502    }
503}