1use alloc::borrow::ToOwned;
19use alloc::collections::BTreeMap;
20use alloc::format;
21use alloc::string::String;
22use alloc::vec::Vec;
23
24use core::sync::atomic::{AtomicBool, Ordering};
25
26use shuyi_assets::bundle::Watcher;
27use shuyi_ui::frame::onebit::Phase;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
32#[non_exhaustive]
33pub struct RunReport {
34 pub(crate) file: Option<String>,
35 pub(crate) every_seconds: u32,
36 pub(crate) slowest: usize,
37 pub(crate) phases: bool,
38 pub(crate) stack_bytes: Option<usize>,
40}
41
42impl Default for RunReport {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl RunReport {
49 pub const fn new() -> Self {
52 Self {
53 file: None,
54 every_seconds: 60,
55 slowest: 10,
56 phases: false,
57 stack_bytes: None,
58 }
59 }
60
61 #[must_use]
65 pub fn file(mut self, path: &str) -> Self {
66 self.file = Some(path.to_owned());
67 self
68 }
69
70 #[must_use]
72 pub const fn every_seconds(mut self, seconds: u32) -> Self {
73 self.every_seconds = if seconds == 0 { 1 } else { seconds };
74 self
75 }
76
77 #[must_use]
79 pub const fn slowest(mut self, count: usize) -> Self {
80 self.slowest = count;
81 self
82 }
83
84 #[must_use]
88 pub const fn phases(mut self, on: bool) -> Self {
89 self.phases = on;
90 self
91 }
92
93 #[doc(hidden)]
95 #[must_use]
96 pub const fn stack_bytes(mut self, bytes: usize) -> Self {
97 self.stack_bytes = Some(bytes);
98 self
99 }
100}
101
102pub(crate) fn grouped(n: u64) -> String {
104 let digits = format!("{n}");
105 let mut out = String::new();
106 for (i, digit) in digits.chars().enumerate() {
107 if i > 0 && (digits.len() - i) % 3 == 0 {
108 out.push(',');
109 }
110 out.push(digit);
111 }
112 out
113}
114
115fn ms_tenths(us: u64) -> String {
117 let tenths = us.saturating_add(50) / 100;
118 format!("{}.{}", grouped(tenths / 10), tenths % 10)
119}
120
121fn ms(us: u64) -> String {
123 grouped(us.saturating_add(500) / 1000)
124}
125
126#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
129#[non_exhaustive]
130pub struct Timing {
131 count: u32,
132 min_us: u64,
133 max_us: u64,
134 total_us: u64,
135}
136
137impl Timing {
138 pub const fn new() -> Self {
140 Self {
141 count: 0,
142 min_us: 0,
143 max_us: 0,
144 total_us: 0,
145 }
146 }
147
148 pub fn add(&mut self, us: u64) {
150 if self.count == 0 {
151 self.min_us = us;
152 self.max_us = us;
153 } else {
154 self.min_us = self.min_us.min(us);
155 self.max_us = self.max_us.max(us);
156 }
157 self.count = self.count.saturating_add(1);
158 self.total_us = self.total_us.saturating_add(us);
159 }
160
161 pub const fn count(&self) -> u32 {
163 self.count
164 }
165
166 pub const fn min_us(&self) -> u64 {
168 self.min_us
169 }
170
171 pub const fn max_us(&self) -> u64 {
173 self.max_us
174 }
175
176 pub fn avg_us(&self) -> u64 {
178 self.total_us / u64::from(self.count.max(1))
179 }
180
181 pub fn line(&self, name: &str) -> String {
184 if self.count == 0 {
185 return format!("{name}: none");
186 }
187 format!(
188 "{name}: min {} / avg {} / max {} ms, {} times",
189 ms_tenths(self.min_us),
190 ms_tenths(self.avg_us()),
191 ms_tenths(self.max_us),
192 grouped(u64::from(self.count))
193 )
194 }
195}
196
197#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
199pub(crate) struct Phases([Timing; Phase::ALL.len()]);
200
201impl Phases {
202 pub(crate) fn add(&mut self, phase: Phase, us: u64) {
204 if let Some(i) = Phase::ALL.iter().position(|&p| p == phase) {
205 self.0[i].add(us);
206 }
207 }
208
209 pub(crate) fn line(&self) -> String {
211 let each: Vec<String> = Phase::ALL
212 .iter()
213 .zip(&self.0)
214 .map(|(phase, t)| {
215 format!(
216 "{} {} / {}",
217 phase.name(),
218 ms_tenths(t.avg_us()),
219 ms_tenths(t.max_us)
220 )
221 })
222 .collect();
223 format!("Draw by phase, avg / max ms: {}", each.join(", "))
224 }
225}
226
227#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
229pub(crate) struct Measured {
230 pub(crate) draw: Timing,
232 pub(crate) phases: Option<Phases>,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq)]
238struct Read {
239 name: String,
241 found: Option<bool>,
243 at_us: u64,
245}
246
247#[derive(Debug, Clone)]
252pub(crate) struct Reads {
253 clock: fn() -> u64,
255 reads: Vec<Read>,
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260struct FileTime {
261 us: u64,
263 reads: u32,
265 found: Option<bool>,
266}
267
268impl Reads {
269 pub(crate) const fn new(clock: fn() -> u64) -> Self {
271 Self {
272 clock,
273 reads: Vec::new(),
274 }
275 }
276
277 fn add(&mut self, path: &str, found: bool) {
279 let at_us = (self.clock)();
280 self.reads.push(Read {
281 name: path.to_owned(),
282 found: Some(found),
283 at_us,
284 });
285 }
286
287 fn mark(&mut self, name: &str) {
289 let at_us = (self.clock)();
290 self.reads.push(Read {
291 name: format!("({name})"),
292 found: None,
293 at_us,
294 });
295 }
296
297 fn files(&self, end_us: u64) -> BTreeMap<&str, FileTime> {
299 let mut files: BTreeMap<&str, FileTime> = BTreeMap::new();
300 let ends = self.reads.iter().skip(1).map(|r| r.at_us).chain([end_us]);
301 for (read, end) in self.reads.iter().zip(ends) {
302 let file = files.entry(&read.name).or_insert(FileTime {
303 us: 0,
304 reads: 0,
305 found: read.found,
306 });
307 file.us += end.saturating_sub(read.at_us);
308 file.reads += 1;
309 }
310 files
311 }
312
313 fn by_time(&self, end_us: u64) -> Vec<(&str, FileTime)> {
315 let mut files: Vec<(&str, FileTime)> = self.files(end_us).into_iter().collect();
316 files.sort_by(|a, b| b.1.us.cmp(&a.1.us).then(a.0.cmp(b.0)));
317 files
318 }
319
320 pub(crate) fn summary(&self, end_us: u64, slowest: usize) -> Vec<String> {
323 let files = self.by_time(end_us);
324 let missing: Vec<&str> = files
325 .iter()
326 .filter(|(_, f)| f.found == Some(false))
327 .map(|(name, _)| *name)
328 .collect();
329 let reads = self.reads.iter().filter(|r| r.found.is_some()).count();
330 let read = files.iter().filter(|(_, f)| f.found.is_some()).count();
331 let mut lines = Vec::from([format!(
332 "Bundle: {} reads of {} files, {} not in the bundle",
333 grouped(reads as u64),
334 grouped(read as u64),
335 grouped(missing.len() as u64)
336 )]);
337 if !missing.is_empty() {
338 lines.push(format!("Not in the bundle: {}", missing.join(", ")));
339 }
340 let names: Vec<String> = files
341 .iter()
342 .take(slowest)
343 .map(|(name, f)| format!("{name} {}", ms(f.us)))
344 .collect();
345 if !names.is_empty() {
346 lines.push(format!(
347 "Slowest reads, ms to the next read: {}",
348 names.join(", ")
349 ));
350 }
351 lines
352 }
353
354 pub(crate) fn each(&self, end_us: u64) -> Vec<String> {
357 self.by_time(end_us)
358 .into_iter()
359 .map(|(name, f)| {
360 let reads = if f.reads > 1 {
361 format!(", {} reads", grouped(u64::from(f.reads)))
362 } else {
363 String::new()
364 };
365 let missing = if f.found == Some(false) {
366 ", not in the bundle"
367 } else {
368 ""
369 };
370 format!(" {name}: {} ms{reads}{missing}", ms_tenths(f.us))
371 })
372 .collect()
373 }
374}
375
376#[cfg(test)]
380pub(crate) fn with_reads<R>(f: impl FnOnce(&mut Option<Reads>) -> R) -> R {
381 std::thread_local! {
382 static READS: core::cell::RefCell<Option<Reads>> = const { core::cell::RefCell::new(None) };
383 }
384 READS.with_borrow_mut(f)
385}
386
387#[cfg(all(not(test), shuyi_playdate_sdk))]
389pub(crate) use crate::device::with_reads;
390
391#[cfg(all(not(test), not(shuyi_playdate_sdk)))]
393pub(crate) fn with_reads<R>(f: impl FnOnce(&mut Option<Reads>) -> R) -> R {
394 f(&mut None)
395}
396
397static WATCHING: AtomicBool = AtomicBool::new(false);
399
400pub(crate) fn watch_bundle(watcher: Watcher) -> Result<(), Watcher> {
404 watch_once(shuyi_assets::bundle::watch, &WATCHING, watcher)
405}
406
407fn watch_once(
410 set: fn(Watcher) -> Result<(), Watcher>,
411 watching: &AtomicBool,
412 watcher: Watcher,
413) -> Result<(), Watcher> {
414 match set(watcher) {
415 Ok(()) => {
416 watching.store(true, Ordering::Relaxed);
417 Ok(())
418 }
419 Err(_) if watching.load(Ordering::Relaxed) => Ok(()),
420 Err(other) => Err(other),
421 }
422}
423
424pub(crate) fn noted(path: &str, found: bool) {
427 with_reads(|reads| {
428 if let Some(reads) = reads {
429 reads.add(path, found);
430 }
431 });
432}
433
434pub fn mark(name: &str) {
438 with_reads(|reads| {
439 if let Some(reads) = reads {
440 reads.mark(name);
441 }
442 });
443}
444
445pub(crate) fn stack_line(used: Option<usize>, size: Option<usize>) -> String {
447 match (used, size) {
448 (Some(used), Some(size)) => format!(
449 "Game stack used: {} of {} bytes",
450 grouped(used as u64),
451 grouped(size as u64)
452 ),
453 (Some(used), None) => format!("Game stack used: {} bytes", grouped(used as u64)),
454 (None, _) => "Game stack: measured on the device only".to_owned(),
455 }
456}
457
458pub(crate) fn shown_line(what: &str, us: u64) -> String {
460 format!("{what} shown {} ms after start", ms(us))
461}
462
463pub(crate) fn load_line(us: u64) -> String {
465 format!("Game data loaded in {} ms", ms(us))
466}
467
468pub(crate) const REFUSED: &str = "Bundle reads: not timed, as another watcher was set first";
471
472#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
474pub(crate) struct Stretch {
475 pub(crate) frame: Timing,
477}
478
479#[derive(Debug, Clone, Copy)]
481pub(crate) struct Now<'a> {
482 pub(crate) screen: Option<&'a str>,
484 pub(crate) ms: u64,
486 pub(crate) stack_used: Option<usize>,
487 pub(crate) music: Option<&'a str>,
489 pub(crate) underran: bool,
491}
492
493impl Stretch {
494 pub(crate) fn lines(
496 &self,
497 now: &Now<'_>,
498 measured: &Measured,
499 report: &RunReport,
500 ) -> Vec<String> {
501 let frames = u64::from(self.frame.count());
502 let tenths = frames * 10_000 / now.ms.max(1);
503 let screen = now.screen.unwrap_or("no screen");
504 let music = match (now.music, now.underran) {
505 (Some(cue), true) => format!("Music: {cue}, and a file player ran out of data"),
506 (Some(cue), false) => format!("Music: {cue}, no file player ran out of data"),
507 (None, true) => "Music: none, and a file player ran out of data".to_owned(),
508 (None, false) => "Music: none".to_owned(),
509 };
510 let mut lines = Vec::from([
511 format!(
512 "On {screen} for {} seconds: {}.{} frames a second",
513 grouped((now.ms + 500) / 1000),
514 grouped(tenths / 10),
515 tenths % 10
516 ),
517 self.frame.line("App::frame"),
518 measured.draw.line("Draw and flush"),
519 ]);
520 if let Some(phases) = &measured.phases {
521 lines.push(phases.line());
522 }
523 lines.push(stack_line(now.stack_used, report.stack_bytes));
524 lines.push(music);
525 lines
526 }
527}
528
529#[cfg(test)]
530mod tests;