Skip to main content

proof_stream/
rerun_sink.rs

1//! `RerunSink` — streams proof events to a Rerun viewer via a background thread.
2//!
3//! Events are dispatched through a bounded crossbeam channel (capacity 8192) so
4//! the prover hot path never blocks waiting for Rerun I/O. Events are silently
5//! dropped if the background thread falls behind.
6
7#[cfg(feature = "rerun")]
8mod inner {
9    use std::path::PathBuf;
10    use std::sync::atomic::{AtomicBool, Ordering};
11    use std::sync::Arc;
12    use std::thread::{self, JoinHandle};
13    use std::time::Duration;
14
15    use crossbeam::channel::{self, Receiver, Sender};
16    use rerun::{RecordingStream, RecordingStreamBuilder};
17
18    use crate::events::{LayerKind, LogLevel, ProofEvent};
19    use crate::sink::ProofEventSink;
20
21    // ── Rerun entity path helpers ─────────────────────────────────────────────
22
23    fn layer_node_path(layer_idx: usize) -> String {
24        format!("gkr/walk/layer_{layer_idx}/node")
25    }
26
27    fn layer_poly_path(layer_idx: usize) -> String {
28        format!("gkr/walk/layer_{layer_idx}/poly")
29    }
30
31    fn layer_claim_path(layer_idx: usize) -> String {
32        format!("gkr/walk/layer_{layer_idx}/claim")
33    }
34
35    fn inference_output_path(layer_idx: usize) -> String {
36        format!("inference/layer_{layer_idx}/output")
37    }
38
39    fn inference_mean_path(layer_idx: usize) -> String {
40        format!("inference/stats/layer_{layer_idx}/mean")
41    }
42
43    fn inference_std_path(layer_idx: usize) -> String {
44        format!("inference/stats/layer_{layer_idx}/std")
45    }
46
47    fn inference_attn_path(layer_idx: usize, head_idx: usize) -> String {
48        format!("inference/layer_{layer_idx}/attn_head_{head_idx}")
49    }
50
51    fn gpu_util_path(device_id: usize) -> String {
52        format!("gpu/device_{device_id}/util")
53    }
54
55    fn gpu_free_path(device_id: usize) -> String {
56        format!("gpu/device_{device_id}/free_gb")
57    }
58
59    // ── Position / color helpers ──────────────────────────────────────────────
60
61    fn kind_color(kind: LayerKind) -> rerun::Color {
62        let [r, g, b] = kind.color_rgb();
63        rerun::Color::from_rgb(r, g, b)
64    }
65
66    /// Vertical offset for each layer kind so the 3D view shows circuit "height".
67    fn kind_y(kind: LayerKind) -> f32 {
68        match kind {
69            LayerKind::MatMul => 0.0,
70            LayerKind::Activation => 1.8,
71            LayerKind::LayerNorm | LayerKind::RMSNorm => -1.2,
72            LayerKind::Attention => 3.0,
73            LayerKind::Add => 0.8,
74            LayerKind::Mul => 1.2,
75            LayerKind::Embedding => -2.5,
76            LayerKind::Dequantize | LayerKind::Quantize => -1.8,
77            _ => 0.0,
78        }
79    }
80
81    /// X spacing between consecutive layers.
82    const X_STEP: f32 = 3.0;
83
84    fn layer_x(layer_idx: usize) -> f32 {
85        layer_idx as f32 * X_STEP
86    }
87
88    /// 3D position for a layer node — helix in YZ so nodes spiral in depth.
89    /// Y = sin(i * 0.9) * 1.8,  Z = cos(i * 0.9) * 1.8
90    fn helix_pos(layer_idx: usize) -> [f32; 3] {
91        let angle = layer_idx as f32 * 0.9_f32;
92        let r = 1.8_f32;
93        [layer_x(layer_idx), angle.sin() * r, angle.cos() * r]
94    }
95
96    // ── Connection config ─────────────────────────────────────────────────────
97
98    /// Where the Rerun SDK should send data.
99    pub enum RerunConnection {
100        /// Stream to a running `rerun` viewer over TCP.
101        Tcp { addr: String },
102        /// Write a `.rrd` file for later replay.
103        File { path: PathBuf },
104        /// Spawn a viewer subprocess and stream to it.
105        Spawn,
106    }
107
108    impl RerunConnection {
109        /// Parse from a user-facing string:
110        /// - `"spawn"` → `Spawn`
111        /// - `"file:<path>"` → `File`
112        /// - anything else → `Tcp`
113        pub fn from_str(s: &str) -> Self {
114            if s == "spawn" {
115                RerunConnection::Spawn
116            } else if let Some(path) = s.strip_prefix("file:") {
117                RerunConnection::File {
118                    path: PathBuf::from(path),
119                }
120            } else {
121                RerunConnection::Tcp { addr: s.to_owned() }
122            }
123        }
124    }
125
126    // ── Background worker ─────────────────────────────────────────────────────
127
128    enum Msg {
129        Event(ProofEvent),
130        Flush,
131        Stop,
132    }
133
134    fn build_recording_stream(
135        conn: RerunConnection,
136        app_id: &str,
137    ) -> Result<RecordingStream, Box<dyn std::error::Error + Send + Sync>> {
138        let builder = RecordingStreamBuilder::new(app_id);
139        match conn {
140            RerunConnection::Tcp { addr } => {
141                // Normalize to rerun+http://host:port/proxy URL
142                let addr_str = addr
143                    .strip_prefix("tcp://")
144                    .unwrap_or(addr.as_str());
145                // If bare "host:port", wrap in gRPC proxy URL
146                let url = if addr_str.starts_with("rerun+") {
147                    addr_str.to_owned()
148                } else {
149                    format!("rerun+http://{addr_str}/proxy")
150                };
151                let stream = builder.connect_grpc_opts(url)?;
152                Ok(stream)
153            }
154            RerunConnection::File { path } => {
155                let stream = builder.save(path)?;
156                Ok(stream)
157            }
158            RerunConnection::Spawn => {
159                let stream = builder.spawn()?;
160                Ok(stream)
161            }
162        }
163    }
164
165    fn dispatch_event(rec: &RecordingStream, event: &ProofEvent) {
166        match event {
167            ProofEvent::CircuitCompiled { nodes, .. } => {
168                let positions: Vec<[f32; 3]> = nodes
169                    .iter()
170                    .map(|n| [layer_x(n.layer_idx), kind_y(n.kind), 0.0])
171                    .collect();
172                let colors: Vec<rerun::Color> = nodes.iter().map(|n| kind_color(n.kind)).collect();
173                let radii: Vec<rerun::Radius> = nodes
174                    .iter()
175                    .map(|n| {
176                        rerun::Radius::new_ui_points(4.0 + (n.trace_cost as f32).sqrt() * 0.4)
177                    })
178                    .collect();
179                let labels: Vec<String> = nodes
180                    .iter()
181                    .map(|n| format!("{:?} [{}]", n.kind, n.layer_idx))
182                    .collect();
183                let _ = rec.log_static(
184                    "circuit/nodes",
185                    &rerun::Points3D::new(positions.clone())
186                        .with_colors(colors)
187                        .with_radii(radii)
188                        .with_labels(labels),
189                );
190
191                // DAG edges: one LineStrip per node with predecessors
192                let mut edge_strips: Vec<Vec<[f32; 3]>> = Vec::new();
193                for n in nodes {
194                    let to = [layer_x(n.layer_idx), kind_y(n.kind), 0.0];
195                    for &from_idx in &n.input_layers {
196                        if let Some(m) = nodes.iter().find(|m| m.layer_idx == from_idx) {
197                            edge_strips.push(vec![
198                                [layer_x(m.layer_idx), kind_y(m.kind), 0.0],
199                                to,
200                            ]);
201                        }
202                    }
203                }
204                if !edge_strips.is_empty() {
205                    let _ = rec.log_static(
206                        "circuit/edges",
207                        &rerun::LineStrips3D::new(edge_strips)
208                            .with_colors(vec![rerun::Color::from_unmultiplied_rgba(
209                                0x44, 0x88, 0xcc, 0xaa,
210                            )])
211                            .with_radii(vec![rerun::Radius::new_ui_points(1.2)]),
212                    );
213                }
214            }
215
216            ProofEvent::LayerActivation {
217                layer_idx,
218                output_sample,
219                stats,
220                ..
221            } => {
222                let vals: Vec<f32> = output_sample
223                    .iter()
224                    .map(|&v| v as f32 / 0x7fff_ffff_u32 as f32)
225                    .collect();
226                let n = vals.len();
227                let _ = rec.log(
228                    inference_output_path(*layer_idx).as_str(),
229                    &rerun::Tensor::new(rerun::TensorData::new(
230                        vec![n as u64],
231                        rerun::TensorBuffer::F32(vals.into()),
232                    )),
233                );
234                let _ = rec.log(
235                    inference_mean_path(*layer_idx).as_str(),
236                    &rerun::Scalars::single(stats.mean as f64),
237                );
238                let _ = rec.log(
239                    inference_std_path(*layer_idx).as_str(),
240                    &rerun::Scalars::single(stats.std_dev as f64),
241                );
242            }
243
244            ProofEvent::AttentionHeatmap {
245                layer_idx,
246                head_idx,
247                scores,
248                ..
249            } => {
250                let n = (scores.len() as f64).sqrt() as usize;
251                let _ = rec.log(
252                    inference_attn_path(*layer_idx, *head_idx).as_str(),
253                    &rerun::Tensor::new(rerun::TensorData::new(
254                        vec![n as u64, n as u64],
255                        rerun::TensorBuffer::F32(scores.clone().into()),
256                    )),
257                );
258            }
259
260            ProofEvent::ProofStart {
261                num_layers,
262                input_shape,
263                output_shape,
264                backend,
265                model_name,
266            } => {
267                // Pre-layout layer nodes on a helix so the 3D view has real depth.
268                // LayerStart will update each node to its type color.
269                let n = *num_layers;
270                let positions: Vec<[f32; 3]> = (0..n).map(helix_pos).collect();
271                let dim = rerun::Color::from_unmultiplied_rgba(72, 74, 92, 200);
272                let labels: Vec<String> = (0..n).map(|i| format!("L{i}")).collect();
273                let _ = rec.log_static(
274                    "circuit/nodes",
275                    &rerun::Points3D::new(positions.clone())
276                        .with_colors(vec![dim; n])
277                        .with_radii(vec![rerun::Radius::new_ui_points(5.0); n])
278                        .with_labels(labels),
279                );
280                // Helix backbone rail connecting placeholder nodes
281                let _ = rec.log_static(
282                    "circuit/backbone",
283                    &rerun::LineStrips3D::new(vec![positions])
284                        .with_colors(vec![rerun::Color::from_unmultiplied_rgba(
285                            60, 62, 90, 120,
286                        )])
287                        .with_radii(vec![rerun::Radius::new_ui_points(1.0)]),
288                );
289
290                let _ = rec.log("proof/progress", &rerun::Scalars::single(0.0_f64));
291                let _ = rec.log(
292                    "logs/proof",
293                    &rerun::TextLog::new(format!(
294                        "ProofStart: {backend}, {n} layers, \
295                         input={input_shape:?} output={output_shape:?}, model={model_name:?}"
296                    )),
297                );
298            }
299
300            ProofEvent::LayerStart {
301                layer_idx,
302                kind,
303                claim_value_approx,
304                gpu_device,
305                trace_cost,
306                ..
307            } => {
308                let [hx, hy, hz] = helix_pos(*layer_idx);
309                let ky = kind_y(*kind);
310                let [r, g, b] = kind.color_rgb();
311                let radius =
312                    rerun::Radius::new_ui_points(6.0 + (*trace_cost as f32).sqrt() * 0.2);
313
314                // Activate node — type color at helix position + kind elevation
315                let _ = rec.log(
316                    layer_node_path(*layer_idx).as_str(),
317                    &rerun::Points3D::new(vec![[hx, hy + ky, hz]])
318                        .with_colors(vec![rerun::Color::from_rgb(r, g, b)])
319                        .with_radii(vec![radius])
320                        .with_labels(vec![format!("{kind:?} [{layer_idx}]")]),
321                );
322                // Cyan scanning cursor above the active node
323                let _ = rec.log(
324                    "gkr/walk/cursor",
325                    &rerun::Points3D::new(vec![[hx, hy + ky + 0.7, hz]])
326                        .with_colors(vec![rerun::Color::from_rgb(0x00, 0xe5, 0xff)])
327                        .with_radii(vec![rerun::Radius::new_ui_points(6.0)])
328                        .with_labels(vec![format!("proving {kind:?}...")]),
329                );
330                let _ = rec.log("gkr/claim", &rerun::Scalars::single(*claim_value_approx as f64));
331                let _ = rec.log(
332                    "logs/gkr",
333                    &rerun::TextLog::new(format!(
334                        "-> [{layer_idx}] {kind:?}  claim={claim_value_approx:.6}  gpu={gpu_device:?}"
335                    )),
336                );
337            }
338
339            ProofEvent::SumcheckRound {
340                layer_idx,
341                round,
342                total_rounds,
343                poly_deg2,
344                claim_value_approx,
345                ..
346            } => {
347                // Update per-round claim scalar
348                let _ = rec.log(
349                    layer_claim_path(*layer_idx).as_str(),
350                    &rerun::Scalars::single(*claim_value_approx as f64),
351                );
352
353                // Claim reduction tower: one vertical bar per sumcheck round,
354                // shrinking as the claim compresses toward the input evaluation.
355                // Bars fan out along X from the node; tallest = round 0 (full claim).
356                {
357                    let [hx, hy, hz] = helix_pos(*layer_idx);
358                    let frac = *round as f32 / (*total_rounds).max(1) as f32;
359
360                    // Bar height = normalised claim value (clamped to [0.1, 4.0])
361                    let bar_h = (claim_value_approx.abs() * 4.0).clamp(0.1, 4.0);
362
363                    // Fan bars out along X centred on the node; most recent at centre
364                    let spread = *total_rounds as f32 * 0.38;
365                    let x_off = (frac - 0.5) * spread;
366
367                    // Vertical bar: base → top
368                    let base = [hx + x_off, hy, hz];
369                    let top  = [hx + x_off, hy + bar_h, hz];
370
371                    // A small horizontal cap at the top to read claim level
372                    let cap_w = 0.18_f32;
373                    let cap_l = [hx + x_off - cap_w, hy + bar_h, hz];
374                    let cap_r = [hx + x_off + cap_w, hy + bar_h, hz];
375
376                    // Colour: blue (round 0, tall) → red (last round, short)
377                    let r_col = (frac * 210.0) as u8 + 45;
378                    let b_col = ((1.0 - frac) * 210.0) as u8 + 45;
379                    let color = rerun::Color::from_rgb(r_col, 0x55, b_col);
380
381                    let _ = rec.log(
382                        format!("gkr/walk/layer_{layer_idx}/bar_{round}").as_str(),
383                        &rerun::LineStrips3D::new(vec![
384                            vec![base, top],       // vertical bar
385                            vec![cap_l, cap_r],    // top cap
386                        ])
387                        .with_colors(vec![color])
388                        .with_radii(vec![rerun::Radius::new_ui_points(2.2)]),
389                    );
390                }
391
392                if *round == 0 || round + 1 == *total_rounds {
393                    let _ = rec.log(
394                        "logs/gkr",
395                        &rerun::TextLog::new(format!(
396                            "  [{layer_idx}] round {}/{total_rounds}  claim={claim_value_approx:.6}",
397                            round + 1
398                        )),
399                    );
400                }
401            }
402
403            ProofEvent::LayerEnd {
404                layer_idx,
405                kind,
406                final_claim_value_approx,
407                duration_ms,
408                ..
409            } => {
410                let [hx, hy, hz] = helix_pos(*layer_idx);
411                // Mark node proved — vivid green, settled at helix position
412                let _ = rec.log(
413                    layer_node_path(*layer_idx).as_str(),
414                    &rerun::Points3D::new(vec![[hx, hy, hz]])
415                        .with_colors(vec![rerun::Color::from_rgb(0x00, 0xe6, 0x76)])
416                        .with_radii(vec![rerun::Radius::new_ui_points(6.0)])
417                        .with_labels(vec![format!("{kind:?}")]),
418                );
419                // Grow the proof-progress trail along the helix
420                let trail: Vec<[f32; 3]> = (0..=*layer_idx).map(helix_pos).collect();
421                let _ = rec.log(
422                    "gkr/walk/progress_trail",
423                    &rerun::LineStrips3D::new(vec![trail])
424                        .with_colors(vec![rerun::Color::from_unmultiplied_rgba(
425                            0x00, 0xe6, 0x76, 0xcc,
426                        )])
427                        .with_radii(vec![rerun::Radius::new_ui_points(2.5)]),
428                );
429                let _ = rec.log(
430                    "logs/gkr",
431                    &rerun::TextLog::new(format!(
432                        "[{layer_idx}] proved {kind:?} in {duration_ms}ms  claim={final_claim_value_approx:.6}"
433                    )),
434                );
435            }
436
437            ProofEvent::ProofComplete {
438                duration_ms,
439                num_layer_proofs,
440                weight_binding_mode,
441                ..
442            } => {
443                let _ = rec.log("proof/progress", &rerun::Scalars::single(1.0_f64));
444                // Dismiss the scanning cursor (move far off-canvas)
445                let _ = rec.log(
446                    "gkr/walk/cursor",
447                    &rerun::Points3D::new(vec![[0.0_f32, -30.0, -30.0]])
448                        .with_colors(vec![rerun::Color::from_unmultiplied_rgba(0, 0, 0, 0)])
449                        .with_radii(vec![rerun::Radius::new_ui_points(0.1)]),
450                );
451                let _ = rec.log(
452                    "logs/proof",
453                    &rerun::TextLog::new(format!(
454                        "PROOF COMPLETE  {duration_ms}ms  {num_layer_proofs} layers  {weight_binding_mode}"
455                    )),
456                );
457            }
458
459            ProofEvent::WeightOpeningStart {
460                weight_node_id,
461                eval_point_len,
462            } => {
463                let _ = rec.log(
464                    "logs/weights",
465                    &rerun::TextLog::new(format!(
466                        "WeightOpening node={weight_node_id}, eval_dim={eval_point_len}"
467                    )),
468                );
469            }
470
471            ProofEvent::WeightOpeningEnd {
472                weight_node_id,
473                duration_ms,
474                commitment_hex,
475            } => {
476                let _ = rec.log(
477                    "logs/weights",
478                    &rerun::TextLog::new(format!(
479                        "WeightOpening done node={weight_node_id}, {duration_ms}ms, commit={commitment_hex}"
480                    )),
481                );
482            }
483
484            ProofEvent::AggregatedBindingStart {
485                num_claims,
486                num_matrices,
487            } => {
488                let _ = rec.log(
489                    "logs/proof",
490                    &rerun::TextLog::new(format!(
491                        "AggregatedBinding: {num_claims} claims, {num_matrices} matrices"
492                    )),
493                );
494            }
495
496            ProofEvent::AggregatedBindingEnd {
497                duration_ms,
498                estimated_calldata_felts,
499            } => {
500                let _ = rec.log("proof/binding_ms", &rerun::Scalars::single(*duration_ms as f64));
501                let _ = rec.log(
502                    "logs/proof",
503                    &rerun::TextLog::new(format!(
504                        "AggregatedBinding done: {duration_ms}ms, ~{estimated_calldata_felts} felts"
505                    )),
506                );
507            }
508
509            ProofEvent::GpuStatus {
510                devices,
511                matmul_done,
512                matmul_total,
513                layers_done,
514                layers_total,
515            } => {
516                let positions: Vec<[f32; 3]> = devices
517                    .iter()
518                    .enumerate()
519                    .map(|(i, _)| [i as f32 * 2.0, 0.0, 2.0])
520                    .collect();
521                let colors: Vec<rerun::Color> = devices
522                    .iter()
523                    .map(|d| {
524                        let r = (d.utilization * 255.0) as u8;
525                        let g = ((1.0 - d.utilization) * 200.0) as u8;
526                        rerun::Color::from_rgb(r, g, 0x40)
527                    })
528                    .collect();
529                let _ = rec.log(
530                    "gpu/cluster",
531                    &rerun::Points3D::new(positions)
532                        .with_colors(colors)
533                        .with_radii(vec![rerun::Radius::new_ui_points(10.0)]),
534                );
535                for d in devices {
536                    let _ = rec.log(
537                        gpu_util_path(d.device_id).as_str(),
538                        &rerun::Scalars::single(d.utilization as f64),
539                    );
540                    if let Some(free) = d.free_memory_bytes {
541                        let _ = rec.log(
542                            gpu_free_path(d.device_id).as_str(),
543                            &rerun::Scalars::single(free as f64 / 1e9),
544                        );
545                    }
546                }
547                if *layers_total > 0 {
548                    let _ = rec.log(
549                        "proof/progress",
550                        &rerun::Scalars::single(*layers_done as f64 / *layers_total as f64),
551                    );
552                }
553                let _ = rec.log(
554                    "logs/proof",
555                    &rerun::TextLog::new(format!(
556                        "GPU: matmul {matmul_done}/{matmul_total}, layers {layers_done}/{layers_total}"
557                    )),
558                );
559            }
560
561            ProofEvent::StarkProofStart {
562                num_activation_layers,
563                num_add_layers,
564                num_layernorm_layers,
565            } => {
566                let _ = rec.log(
567                    "logs/stark",
568                    &rerun::TextLog::new(format!(
569                        "StarkProofStart: activations={num_activation_layers}, add={num_add_layers}, layernorm={num_layernorm_layers}"
570                    )),
571                );
572            }
573
574            ProofEvent::StarkProofEnd { duration_ms } => {
575                let _ = rec.log(
576                    "logs/stark",
577                    &rerun::TextLog::new(format!("StarkProofEnd: {duration_ms}ms")),
578                );
579            }
580
581            ProofEvent::Log { level, message } => {
582                let level_str = match level {
583                    LogLevel::Trace | LogLevel::Debug => rerun::TextLogLevel::TRACE,
584                    LogLevel::Info => rerun::TextLogLevel::INFO,
585                    LogLevel::Warn => rerun::TextLogLevel::WARN,
586                    LogLevel::Error => rerun::TextLogLevel::ERROR,
587                };
588                let _ = rec.log(
589                    "logs/proof",
590                    &rerun::TextLog::new(message.as_str()).with_level(level_str),
591                );
592            }
593        }
594    }
595
596    fn background_worker(
597        rx: Receiver<Msg>,
598        conn: RerunConnection,
599        app_id: String,
600        ready: Arc<AtomicBool>,
601    ) {
602        let rec = match build_recording_stream(conn, &app_id) {
603            Ok(r) => r,
604            Err(e) => {
605                eprintln!("[proof-stream] Failed to connect to Rerun: {e}");
606                ready.store(true, Ordering::SeqCst);
607                return;
608            }
609        };
610
611        // Send blueprint before any data
612        crate::blueprint::send_blueprint(&rec);
613
614        ready.store(true, Ordering::SeqCst);
615
616        for msg in &rx {
617            match msg {
618                Msg::Event(event) => dispatch_event(&rec, &event),
619                Msg::Flush => {
620                    let _ = rec.flush_blocking();
621                }
622                Msg::Stop => break,
623            }
624        }
625
626        let _ = rec.flush_blocking();
627    }
628
629    // ── Public sink ───────────────────────────────────────────────────────────
630
631    /// A `ProofEventSink` that forwards events to a Rerun viewer.
632    pub struct RerunSink {
633        tx: Sender<Msg>,
634        _handle: Option<JoinHandle<()>>,
635    }
636
637    impl RerunSink {
638        /// Connect to Rerun and start the background dispatch thread.
639        pub fn connect(
640            conn: RerunConnection,
641            app_id: impl Into<String>,
642        ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
643            let (tx, rx) = channel::bounded::<Msg>(8192);
644            let app_id = app_id.into();
645            let ready = Arc::new(AtomicBool::new(false));
646            let ready2 = Arc::clone(&ready);
647
648            let handle = thread::Builder::new()
649                .name("proof-stream-rerun".into())
650                .spawn(move || background_worker(rx, conn, app_id, ready2))?;
651
652            // Wait (briefly) for the connection to be established
653            let deadline = std::time::Instant::now() + Duration::from_secs(10);
654            while !ready.load(Ordering::SeqCst) && std::time::Instant::now() < deadline {
655                thread::sleep(Duration::from_millis(20));
656            }
657
658            Ok(Self {
659                tx,
660                _handle: Some(handle),
661            })
662        }
663    }
664
665    impl ProofEventSink for RerunSink {
666        #[inline]
667        fn emit(&self, event: ProofEvent) {
668            let _ = self.tx.try_send(Msg::Event(event));
669        }
670
671        fn flush(&self) {
672            let _ = self.tx.send(Msg::Flush);
673        }
674    }
675
676    impl Drop for RerunSink {
677        fn drop(&mut self) {
678            let _ = self.tx.send(Msg::Stop);
679            if let Some(h) = self._handle.take() {
680                let _ = h.join();
681            }
682        }
683    }
684}
685
686// ── Re-exports ────────────────────────────────────────────────────────────────
687
688#[cfg(feature = "rerun")]
689pub use inner::{RerunConnection, RerunSink};
690
691// ── Stubs when feature is disabled ────────────────────────────────────────────
692
693#[cfg(not(feature = "rerun"))]
694pub struct RerunSink;
695
696#[cfg(not(feature = "rerun"))]
697pub enum RerunConnection {
698    Tcp { addr: String },
699    File { path: std::path::PathBuf },
700    Spawn,
701}
702
703#[cfg(not(feature = "rerun"))]
704impl RerunConnection {
705    pub fn from_str(s: &str) -> Self {
706        if s == "spawn" {
707            RerunConnection::Spawn
708        } else if let Some(path) = s.strip_prefix("file:") {
709            RerunConnection::File {
710                path: std::path::PathBuf::from(path),
711            }
712        } else {
713            RerunConnection::Tcp { addr: s.to_owned() }
714        }
715    }
716}