Skip to main content

moire_web/recording/
session.rs

1use std::sync::Arc;
2
3use moire_types::{
4    FrameSummary, RecordingImportBody, RecordingSessionInfo, RecordingSessionStatus, SessionId,
5};
6use tokio::sync::Notify;
7
8#[derive(Clone)]
9pub struct StoredFrame {
10    pub frame_index: u32,
11    pub captured_at_unix_ms: i64,
12    pub process_count: u32,
13    pub capture_duration_ms: f64,
14    pub json: String,
15}
16
17pub struct RecordingState {
18    pub session_id: SessionId,
19    pub interval_ms: u32,
20    pub started_at_unix_ms: i64,
21    pub stopped_at_unix_ms: Option<i64>,
22    pub frames: Vec<StoredFrame>,
23    pub max_frames: u32,
24    pub max_memory_bytes: u64,
25    pub overflowed: bool,
26    pub total_frames_captured: u32,
27    pub approx_memory_bytes: u64,
28    pub total_capture_ms: f64,
29    pub max_capture_ms: f64,
30    pub stop_signal: Arc<Notify>,
31}
32
33pub fn push_frame(
34    recording: &mut RecordingState,
35    captured_at_unix_ms: i64,
36    process_count: u32,
37    capture_duration_ms: f64,
38    json: String,
39) {
40    if recording.frames.len() as u32 >= recording.max_frames {
41        recording.overflowed = true;
42        let dropped = recording.frames.remove(0);
43        recording.approx_memory_bytes = recording
44            .approx_memory_bytes
45            .saturating_sub(dropped.json.len() as u64);
46    }
47    let frame_index = recording.total_frames_captured;
48    recording.total_frames_captured += 1;
49    recording.total_capture_ms += capture_duration_ms;
50    if capture_duration_ms > recording.max_capture_ms {
51        recording.max_capture_ms = capture_duration_ms;
52    }
53    let json_len = json.len() as u64;
54    recording.frames.push(StoredFrame {
55        frame_index,
56        captured_at_unix_ms,
57        process_count,
58        capture_duration_ms,
59        json,
60    });
61    recording.approx_memory_bytes += json_len;
62    while recording.approx_memory_bytes > recording.max_memory_bytes && !recording.frames.is_empty()
63    {
64        recording.overflowed = true;
65        let dropped = recording.frames.remove(0);
66        recording.approx_memory_bytes = recording
67            .approx_memory_bytes
68            .saturating_sub(dropped.json.len() as u64);
69    }
70}
71
72pub fn frame_json_by_index(recording: &RecordingState, frame_index: u32) -> Option<&str> {
73    if recording.frames.is_empty() {
74        return None;
75    }
76    let first_index = recording.frames[0].frame_index;
77    if frame_index < first_index {
78        return None;
79    }
80    let vec_index = (frame_index - first_index) as usize;
81    recording
82        .frames
83        .get(vec_index)
84        .map(|frame| frame.json.as_str())
85}
86
87pub fn recording_session_info(rec: &RecordingState) -> RecordingSessionInfo {
88    let status = if rec.stopped_at_unix_ms.is_none() {
89        RecordingSessionStatus::Recording
90    } else {
91        RecordingSessionStatus::Stopped
92    };
93    let avg_capture_ms = if rec.total_frames_captured > 0 {
94        rec.total_capture_ms / rec.total_frames_captured as f64
95    } else {
96        0.0
97    };
98    let frames = rec
99        .frames
100        .iter()
101        .map(|frame| FrameSummary {
102            frame_index: frame.frame_index,
103            captured_at_unix_ms: frame.captured_at_unix_ms,
104            process_count: frame.process_count,
105            capture_duration_ms: frame.capture_duration_ms,
106        })
107        .collect();
108    RecordingSessionInfo {
109        session_id: rec.session_id.clone(),
110        status,
111        interval_ms: rec.interval_ms,
112        started_at_unix_ms: rec.started_at_unix_ms,
113        stopped_at_unix_ms: rec.stopped_at_unix_ms,
114        frame_count: rec.frames.len() as u32,
115        max_frames: rec.max_frames,
116        max_memory_bytes: rec.max_memory_bytes,
117        overflowed: rec.overflowed,
118        approx_memory_bytes: rec.approx_memory_bytes,
119        avg_capture_ms,
120        max_capture_ms: rec.max_capture_ms,
121        total_capture_ms: rec.total_capture_ms,
122        frames,
123    }
124}
125
126pub fn build_imported_frames(import: &RecordingImportBody) -> Result<Vec<StoredFrame>, String> {
127    let summary_by_index: std::collections::HashMap<u32, &FrameSummary> = import
128        .session
129        .frames
130        .iter()
131        .map(|frame| (frame.frame_index, frame))
132        .collect();
133
134    let mut frames: Vec<StoredFrame> = Vec::with_capacity(import.frames.len());
135    for frame in &import.frames {
136        let json = facet_json::to_string(&frame.snapshot).map_err(|error| {
137            format!(
138                "failed to re-serialize frame {}: {error}",
139                frame.frame_index
140            )
141        })?;
142        let summary = summary_by_index.get(&frame.frame_index);
143        let captured_at_unix_ms = summary.map_or(0, |entry| entry.captured_at_unix_ms);
144        let process_count = summary.map_or(0, |entry| entry.process_count);
145        let capture_duration_ms = summary.map_or(0.0, |entry| entry.capture_duration_ms);
146        frames.push(StoredFrame {
147            frame_index: frame.frame_index,
148            captured_at_unix_ms,
149            process_count,
150            capture_duration_ms,
151            json,
152        });
153    }
154    frames.sort_by_key(|frame| frame.frame_index);
155    Ok(frames)
156}
157
158pub fn export_frame_rows(frames: &[StoredFrame]) -> Vec<String> {
159    frames
160        .iter()
161        .map(|frame| {
162            format!(
163                r#"{{"frame_index":{},"snapshot":{}}}"#,
164                frame.frame_index, frame.json
165            )
166        })
167        .collect()
168}