moire_web/recording/
session.rs1use 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}