1use std::collections::BTreeMap;
19use std::fmt::Write as _;
20use std::sync::atomic::{AtomicBool, Ordering};
21
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24use shuyi_ui::perf::{
25 FrameRecord, HeapDiff, HeapSnapshot, PERF_FRAMES, PerfLine, PerfRing, Spread, TagDelta, TagRow,
26 bytes_text, spread,
27};
28
29pub const TOP_SPANS: usize = 5;
31
32pub const HEAP_ROWS: usize = 10;
35
36pub const TEXT_CHANGES: usize = 3;
38
39pub const TIMES: &str = "times are this machine's, from an unoptimized build unless it says \
41 otherwise: compare runs on the same machine";
42
43pub const RESET: &str = "the session was reset or started again: read from 0";
45
46pub const HEAP_UNCOUNTED: &str =
48 "the heap isn't counted: the game's tool program is a release build, or opted out";
49
50pub const HEAP_UNTAGGED: &str =
53 "the heap isn't counted by memory tag, so no heap snapshot was taken";
54
55pub const HEAP_THREADS: &str = "the heap counts the whole tool program: another thread ran beside \
58 the game, and its allocations count too";
59
60static OTHER_THREADS: AtomicBool = AtomicBool::new(false);
63
64pub fn note_other_thread() {
66 OTHER_THREADS.store(true, Ordering::Relaxed);
67}
68
69pub fn other_threads() -> bool {
72 OTHER_THREADS.load(Ordering::Relaxed)
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
77#[serde(deny_unknown_fields)]
78#[non_exhaustive]
80pub struct PerfRead {
81 pub records: Vec<FrameRecord>,
84 pub next: u64,
87 pub dropped: u64,
89 pub measured: bool,
91}
92
93impl PerfRead {
94 pub fn none(since: u64) -> Self {
97 Self {
98 records: Vec::new(),
99 next: since,
100 dropped: 0,
101 measured: false,
102 }
103 }
104
105 pub fn of(ring: &PerfRing, since: u64, max: u32) -> Self {
109 let next_seq = ring.next_seq();
110 let kept = u64::try_from(ring.records().len()).unwrap_or(u64::MAX);
111 let first = next_seq.saturating_sub(kept);
112 let measured = next_seq > 0;
113 if since > next_seq {
114 return Self {
115 records: Vec::new(),
116 next: 0,
117 dropped: 0,
118 measured,
119 };
120 }
121 let max = usize::try_from(max).unwrap_or(usize::MAX);
122 let records: Vec<FrameRecord> = ring.since(since).take(max).cloned().collect();
123 let start = since.max(first);
124 Self {
125 next: start + u64::try_from(records.len()).unwrap_or(u64::MAX),
126 dropped: first.saturating_sub(since),
127 records,
128 measured,
129 }
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
135#[serde(deny_unknown_fields)]
136#[non_exhaustive]
138pub struct SpanSummary {
139 pub name: String,
141 pub count: u32,
143 pub total_us: u64,
145 pub mean_us: u32,
147 pub worst_us: u32,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
153#[serde(deny_unknown_fields)]
154#[non_exhaustive]
156pub struct HeapSummary {
157 pub peak_bytes: u64,
159 pub live_bytes: u64,
161 pub allocs: Spread,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
167#[serde(deny_unknown_fields)]
168#[non_exhaustive]
170pub struct PerfSummary {
171 pub frames: u32,
173 pub dropped: u64,
175 pub frame: Spread,
177 pub update: Spread,
179 pub draw: Spread,
181 pub paint: Spread,
183 pub present: Spread,
185 pub spans: Vec<SpanSummary>,
188 pub heap: Option<HeapSummary>,
191 pub heap_tags: Vec<TagRow>,
194 pub heap_growth: Option<HeapDiff>,
198 pub file: Option<String>,
200}
201
202pub fn summary(records: &[FrameRecord]) -> Option<PerfSummary> {
205 let of = |part: fn(&FrameRecord) -> u32| spread(records.iter().map(part));
206 let mut spans: BTreeMap<&str, (u32, u64, u32)> = BTreeMap::new();
207 for span in records.iter().flat_map(|r| &r.spans) {
208 let (count, total, worst) = spans.entry(&span.name).or_default();
209 *count = count.saturating_add(1);
210 *total += u64::from(span.us);
211 *worst = (*worst).max(span.us);
212 }
213 let mut spans: Vec<SpanSummary> = spans
214 .into_iter()
215 .map(|(name, (count, total_us, worst_us))| SpanSummary {
216 name: name.to_owned(),
217 count,
218 total_us,
219 mean_us: u32::try_from(total_us / u64::from(count.max(1))).unwrap_or(u32::MAX),
220 worst_us,
221 })
222 .collect();
223 spans.sort_by_key(|span| std::cmp::Reverse(span.total_us));
225 spans.truncate(TOP_SPANS);
226 Some(PerfSummary {
227 frames: u32::try_from(records.len()).unwrap_or(u32::MAX),
228 dropped: 0,
229 frame: of(|r| r.times.frame_us)?,
230 update: of(|r| r.times.update_us)?,
231 draw: of(|r| r.times.draw_us)?,
232 paint: of(|r| r.times.paint_us)?,
233 present: of(|r| r.times.present_us)?,
234 spans,
235 heap: heap_summary(records),
236 heap_tags: Vec::new(),
237 heap_growth: None,
238 file: None,
239 })
240}
241
242pub fn add_heap(summary: &mut PerfSummary, first: &HeapSnapshot, last: &HeapSnapshot) {
246 summary.heap_tags = last.tags.iter().take(HEAP_ROWS).cloned().collect();
247 let mut growth = first.diff(last);
248 growth.tags.truncate(HEAP_ROWS);
249 summary.heap_growth = Some(growth);
250}
251
252pub fn heap_summary(records: &[FrameRecord]) -> Option<HeapSummary> {
255 let heaps: Vec<_> = records.iter().filter_map(|r| r.heap).collect();
256 let last = heaps.last()?;
257 Some(HeapSummary {
258 peak_bytes: heaps.iter().map(|h| h.peak_bytes).max().unwrap_or(0),
259 live_bytes: last.live_bytes,
260 allocs: spread(heaps.iter().map(|h| h.allocs))?,
261 })
262}
263
264pub fn heap_limits(records: &[FrameRecord], process: bool, other_threads: bool) -> Vec<String> {
268 if records.is_empty() {
269 Vec::new()
270 } else if records.iter().all(|r| r.heap.is_none()) {
271 vec![HEAP_UNCOUNTED.to_owned()]
272 } else if process && other_threads {
273 vec![HEAP_THREADS.to_owned()]
274 } else {
275 Vec::new()
276 }
277}
278
279pub fn lines<'a>(records: impl IntoIterator<Item = &'a FrameRecord>) -> String {
282 let mut out = String::new();
283 for record in records {
284 let line = serde_json::to_string(&PerfLine::Frame(record.clone())).unwrap_or_default();
286 out.push_str(&line);
287 out.push('\n');
288 }
289 out
290}
291
292pub fn heap_lines(first: &HeapSnapshot, last: &HeapSnapshot) -> String {
295 let lines = [
296 PerfLine::Snapshot(first.clone()),
297 PerfLine::Snapshot(last.clone()),
298 PerfLine::Diff(first.diff(last)),
299 ];
300 let mut out = String::new();
301 for line in lines {
302 out.push_str(&serde_json::to_string(&line).unwrap_or_default());
304 out.push('\n');
305 }
306 out
307}
308
309pub fn dropped_limit(dropped: u64) -> String {
311 format!(
312 "the trace keeps the last {PERF_FRAMES} frames: {dropped} earlier frame(s) were dropped"
313 )
314}
315
316pub fn unmeasured_limit(frames: u32) -> String {
318 if frames == 0 {
319 "nothing was measured: no frame ran".to_owned()
320 } else {
321 "nothing was measured: the game's tool program was built without debug tools".to_owned()
322 }
323}
324
325pub fn millis(us: u32) -> String {
328 let tenths = (u64::from(us) + 50) / 100;
329 format!("{}.{}", tenths / 10, tenths % 10)
330}
331
332fn signed_bytes(bytes: i64) -> String {
334 let sign = if bytes < 0 { '-' } else { '+' };
335 format!("{sign}{}", bytes_text(bytes.unsigned_abs()))
336}
337
338fn delta_text(delta: &TagDelta) -> String {
340 format!("{} {}", delta.name, signed_bytes(delta.live_bytes))
341}
342
343pub fn heap_text(diff: &HeapDiff) -> Option<String> {
348 if diff.live_bytes == 0 && diff.tags.is_empty() {
349 return None;
350 }
351 let size = bytes_text(diff.live_bytes.unsigned_abs());
352 let mut out = match diff.live_bytes.signum() {
353 1 => format!("heap grew {size}"),
354 -1 => format!("heap shrank {size}"),
355 _ => "heap held its size".to_owned(),
356 };
357 let _ = write!(out, " from frame {} to {}", diff.from, diff.to);
358 let changes: Vec<String> = diff
359 .tags
360 .iter()
361 .take(TEXT_CHANGES)
362 .map(delta_text)
363 .collect();
364 if !changes.is_empty() {
365 let _ = write!(out, ": {}", changes.join(", "));
366 }
367 Some(out)
368}
369
370pub fn text(summary: &PerfSummary) -> String {
374 let frame = &summary.frame;
375 let mut out = format!(
376 "perf: {} frames, frame p50 {} ms p99 {} ms worst {} ms",
377 summary.frames,
378 millis(frame.p50),
379 millis(frame.p99),
380 millis(frame.worst)
381 );
382 if let Some(span) = summary.spans.first() {
383 let _ = write!(out, "; span {} {} ms mean", span.name, millis(span.mean_us));
384 }
385 if let Some(heap) = &summary.heap {
386 let _ = write!(out, "; heap peak {}", bytes_text(heap.peak_bytes));
387 }
388 out.push('\n');
389 if let Some(line) = summary.heap_growth.as_ref().and_then(heap_text) {
390 out.push_str(&line);
391 out.push('\n');
392 }
393 if let Some(file) = &summary.file {
394 let _ = writeln!(out, "trace: {file}");
395 }
396 out
397}
398
399#[cfg(test)]
400mod tests;