shuyi_platform/link.rs
1//! The live link (ADR-0047): how a tool reaches a build that is already
2//! running, in a debug build with the backend's `link` feature. The wire is
3//! JSON lines, one UTF-8 JSON object per line: the client sends a
4//! [`Request`] per line, and the build answers each with an [`Answer`], in
5//! order. The first line must be a [`Request::Hello`] with the build's
6//! token, or the connection is closed.
7//!
8//! This module touches no device. A backend carries the lines (TCP on
9//! loopback in `shuyi-app`) and the [`Server`] turns each into its answer
10//! over the [`App`] it runs: it reads the screens, the frame as text, the
11//! window's picture (taken by the backend), the log, and the frames'
12//! records with a heap snapshot (taken by the backend), and its only input
13//! is raw device events the backend adds after the devices' own in
14//! [`Platform::poll_input`](crate::Platform::poll_input), so they go where a
15//! player's go. It changes nothing any other way.
16//!
17//! `shuyi attach` is the client, and shares these types with the build, so
18//! the two ends can't drift; [`VERSION`] says which wire they speak.
19
20pub mod base64;
21
22use crate::alloc_prelude::*;
23use alloc::collections::VecDeque;
24use alloc::format;
25
26use serde::{Deserialize, Serialize};
27use shuyi_ui::input::PadKind;
28use shuyi_ui::log::LogRing;
29use shuyi_ui::perf::{FrameRecord, HeapSnapshot, PERF_FRAMES, PerfRing};
30use shuyi_ui::{GameData, RawInputEvent};
31
32use crate::app::App;
33use crate::script::{self, Planned, Step};
34
35/// The link's version: a client and a build speak the same, or the build
36/// closes the link after its hello.
37pub const VERSION: u32 = 3;
38
39/// The longest request line, in bytes: a longer one closes the link.
40pub const MAX_LINE: usize = 64 * 1024;
41
42/// The most bytes of answers a client may leave unread: past it, the
43/// backend drops the client.
44pub const MAX_UNSENT: usize = 64 * 1024 * 1024;
45
46/// The longest [`Step::Wait`] of a play, in seconds: half an hour, played
47/// as the build's frames at 60 a second ([`script::wait_frames`]).
48pub const MAX_WAIT: f32 = 1_800.0;
49
50/// How many records a [`Request::Perf`] answers when it doesn't say.
51pub const PERF_MAX: u32 = 120;
52
53/// A line a client sends, tagged by `op`.
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55#[serde(tag = "op", rename_all = "snake_case", deny_unknown_fields)]
56#[non_exhaustive]
57pub enum Request {
58 /// The first line: the build's token, and the link's version the
59 /// client speaks.
60 Hello {
61 /// The token the build logged when it started listening.
62 token: String,
63 /// The client's [`VERSION`].
64 link: u32,
65 },
66 /// Where the build is: [`Status`].
67 Status,
68 /// The frame as text, in the snapshot format.
69 Frame,
70 /// The window's picture, as a PNG, after the next frame is shown.
71 Screenshot,
72 /// Steps played as a player's input, frame by frame: answered when
73 /// the last has run. A wait's seconds are the build's frames at 60 a
74 /// second ([`script::wait_frames`]); a crank isn't played over the
75 /// link yet.
76 Play {
77 /// The steps, in order.
78 steps: Vec<Step>,
79 },
80 /// The log's kept lines numbered `since` or more.
81 Log {
82 /// The first line's number: 0, the default, for every kept line;
83 /// the last answer's `next` for the lines since.
84 #[serde(default)]
85 since: u64,
86 },
87 /// The frames' records numbered `since` or more ([`App::perf`]), the
88 /// first `max` of them, and a heap snapshot after this frame if asked.
89 Perf {
90 /// The first record's number: 0, the default, for every kept
91 /// record; the last answer's `next` for the records since.
92 #[serde(default)]
93 since: u64,
94 /// The most records to answer, 1 to [`PERF_FRAMES`];
95 /// [`PERF_MAX`] by default.
96 #[serde(default = "perf_max")]
97 max: u32,
98 /// Whether the backend takes a heap snapshot after this frame
99 /// ([`App::heap_snapshot`]) and answers it with the records.
100 #[serde(default)]
101 snapshot: bool,
102 },
103}
104
105/// [`Request::Perf`]'s `max` when it doesn't say.
106fn perf_max() -> u32 {
107 PERF_MAX
108}
109
110impl Request {
111 /// The request as one line, with no line break.
112 pub fn to_line(&self) -> String {
113 // An enum of strings and numbers: serializing can't fail.
114 serde_json::to_string(self).unwrap_or_default()
115 }
116}
117
118/// Which part of its life the game is in.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121#[non_exhaustive]
122pub enum Phase {
123 /// Its loading card shows.
124 Loading,
125 /// It runs.
126 Running,
127 /// Its load failed: the errors show.
128 Failed,
129}
130
131/// Where a running build is.
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(deny_unknown_fields)]
134// An answer, read field by field: it may grow (ADR-0039).
135#[non_exhaustive]
136pub struct Status {
137 /// The frames run: [`App::frame_number`].
138 pub frames: u32,
139 /// Loading, running or failed.
140 pub phase: Phase,
141 /// Why the game didn't load, if it failed.
142 pub errors: Option<String>,
143 /// The open screens, bottom first.
144 pub screens: Vec<String>,
145 /// The top screen's name; empty with none open.
146 pub top_screen: String,
147 /// Whether the system has suspended the game.
148 pub suspended: bool,
149 /// Whether its window has the player's attention.
150 pub focused: bool,
151 /// Whether the game has quit.
152 pub quit: bool,
153}
154
155impl Status {
156 /// Where `app` is now.
157 pub fn of<G: GameData>(app: &App<G>) -> Self {
158 let phase = if app.is_loading() {
159 Phase::Loading
160 } else if app.errors().is_some() {
161 Phase::Failed
162 } else {
163 Phase::Running
164 };
165 let screens: Vec<String> = app
166 .game()
167 .map(|game| game.screens().into_iter().map(str::to_owned).collect())
168 .unwrap_or_default();
169 Self {
170 frames: app.frame_number(),
171 phase,
172 errors: app.errors().map(str::to_owned),
173 top_screen: screens.last().cloned().unwrap_or_default(),
174 screens,
175 suspended: app.is_suspended(),
176 focused: app.is_focused(),
177 quit: app.has_quit(),
178 }
179 }
180}
181
182/// The window's picture.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(deny_unknown_fields)]
185#[non_exhaustive]
186pub struct Screenshot {
187 /// The PNG file, in [`base64`].
188 pub png: String,
189 /// Its width in pixels.
190 pub width: u32,
191 /// Its height in pixels.
192 pub height: u32,
193 /// What it doesn't show: the loading card or the errors instead of
194 /// the game.
195 pub limit: Option<String>,
196}
197
198/// What a [`Request::Play`] did.
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(deny_unknown_fields)]
201#[non_exhaustive]
202pub struct Played {
203 /// The frames its steps took.
204 pub frames: u32,
205 /// Whether the steps waited for the game to load.
206 pub waited_loading: bool,
207 /// Whether the steps waited while the game was suspended.
208 pub waited_suspended: bool,
209}
210
211/// One line of the log, with its number in the ring.
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(deny_unknown_fields)]
214#[non_exhaustive]
215pub struct LogEntry {
216 /// Its number: lines are numbered from 0 as they are logged.
217 pub seq: u64,
218 /// The frame it was given in.
219 pub frame: u32,
220 /// How serious it is: `error`, `warn`, `info` or `debug`.
221 pub level: String,
222 /// What it is about (`assets`, `platform`, `game`, …).
223 pub category: String,
224 /// What it says.
225 pub text: String,
226}
227
228/// The log's lines a [`Request::Log`] asked for.
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(deny_unknown_fields)]
231#[non_exhaustive]
232pub struct LogAnswer {
233 /// The kept lines numbered `since` or more, oldest first.
234 pub lines: Vec<LogEntry>,
235 /// The number the next line gets: the next request's `since`.
236 pub next: u64,
237 /// How many lines numbered `since` or more the ring no longer keeps.
238 pub dropped: u64,
239}
240
241/// The frames' records a [`Request::Perf`] asked for, counted as the
242/// contract's `PerfRead` counts them.
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(deny_unknown_fields)]
245#[non_exhaustive]
246pub struct PerfAnswer {
247 /// The kept records numbered `since` or more, oldest first, the first
248 /// `max` of them.
249 pub records: Vec<FrameRecord>,
250 /// The number to read from next time: the one after the last record
251 /// answered; 0 for a `since` past the ring's next number.
252 pub next: u64,
253 /// How many records numbered `since` or more the ring no longer keeps.
254 pub dropped: u64,
255 /// Whether any frame was measured.
256 pub measured: bool,
257 /// The frames run: [`App::frame_number`].
258 pub frames: u32,
259 /// The heap snapshot taken after the frame, if one was asked for and
260 /// the build takes one (a debug build that counts the heap by tag).
261 pub snapshot: Option<HeapSnapshot>,
262}
263
264/// A line the build sends, tagged by `answer`.
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266#[serde(tag = "answer", rename_all = "snake_case", deny_unknown_fields)]
267#[non_exhaustive]
268pub enum Answer {
269 /// The hello's answer: the build's link version (a client of another
270 /// is closed after it) and its game, the window's title.
271 Hello {
272 /// The build's [`VERSION`].
273 link: u32,
274 /// The game: the window's title.
275 game: String,
276 },
277 /// [`Request::Status`]'s.
278 Status(Status),
279 /// [`Request::Frame`]'s: the frame in the snapshot format, empty
280 /// while the game isn't running, with a limit saying why.
281 Frame {
282 /// The frame as text.
283 frame: String,
284 /// Why there is none.
285 limit: Option<String>,
286 },
287 /// [`Request::Screenshot`]'s.
288 Screenshot(Screenshot),
289 /// [`Request::Play`]'s, once its last frame has run.
290 Played(Played),
291 /// [`Request::Log`]'s.
292 Log(LogAnswer),
293 /// [`Request::Perf`]'s.
294 Perf(PerfAnswer),
295 /// A request that couldn't be answered: why.
296 Error {
297 /// What was wrong.
298 error: String,
299 },
300 /// Another client holds the link: this one is closed after this line.
301 Busy,
302}
303
304impl Answer {
305 /// The answer as one line, with no line break.
306 pub fn to_line(&self) -> String {
307 // Strings, numbers and lists: serializing can't fail.
308 serde_json::to_string(self).unwrap_or_default()
309 }
310
311 /// The answer `line` holds.
312 pub fn from_line(line: &str) -> Result<Self, String> {
313 serde_json::from_str(line.trim_end()).map_err(|e| format!("not a link answer: {e}"))
314 }
315
316 fn error(error: impl Into<String>) -> String {
317 Self::Error {
318 error: error.into(),
319 }
320 .to_line()
321 }
322}
323
324/// What the [`Server`] made of a line.
325#[derive(Debug, Clone, PartialEq, Eq)]
326#[non_exhaustive]
327pub enum Handled {
328 /// Its answer, to send now.
329 Answer(String),
330 /// Its answer comes from [`Server::after_frame`]: a play's when its
331 /// last frame has run, or any request's behind a play or a picture.
332 Later,
333 /// The backend answers it after this frame is shown, with
334 /// [`Server::screenshot`].
335 Screenshot,
336 /// The backend answers it after this frame, with a heap snapshot
337 /// ([`App::heap_snapshot`]) given to [`Server::snapshot`].
338 Snapshot,
339 /// Close the connection, after sending this line if there is one,
340 /// and read nothing more: the first line wasn't a hello with the
341 /// token, a line was longer than [`MAX_LINE`], or the client speaks
342 /// another link version (the line is the hello's answer, saying the
343 /// build's).
344 Close(Option<String>),
345}
346
347/// A play being played.
348#[derive(Debug)]
349struct Play {
350 steps: Vec<Step>,
351 /// Its frames, once its actions are looked up (the game has loaded).
352 frames: Option<VecDeque<Planned>>,
353 /// The frames given so far.
354 run: u32,
355 waited_loading: bool,
356 waited_suspended: bool,
357}
358
359/// What the server is busy with: later requests wait for it.
360#[derive(Debug)]
361enum Busy {
362 Idle,
363 Play(Play),
364 /// A picture the backend takes after this frame is shown.
365 Screenshot {
366 limit: Option<String>,
367 },
368 /// A heap snapshot the backend takes after this frame, answered with
369 /// the records this asks for.
370 Snapshot {
371 since: u64,
372 max: u32,
373 },
374}
375
376/// The build's end of the link, for one client: it turns the client's
377/// lines into answers over the [`App`] the backend runs, and gives the
378/// backend a play's input frame by frame. The backend makes one per
379/// connection.
380///
381/// Each frame the backend calls [`handle`](Self::handle) for each line
382/// read and sends the answers; [`input`](Self::input) before
383/// [`App::frame`], adding what it gives after the devices' input; then
384/// [`after_frame`](Self::after_frame), sending what it gives; if
385/// [`wants_screenshot`](Self::wants_screenshot), reads the window and
386/// sends [`screenshot`](Self::screenshot)'s line; and if
387/// [`wants_snapshot`](Self::wants_snapshot), takes a heap snapshot and
388/// sends [`snapshot`](Self::snapshot)'s.
389#[derive(Debug)]
390pub struct Server {
391 token: String,
392 game: String,
393 hello: bool,
394 busy: Busy,
395 /// The request lines behind a play or a picture, in order.
396 waiting: VecDeque<String>,
397 /// The answers [`after_frame`](Self::after_frame) gives.
398 due: Vec<String>,
399}
400
401impl Server {
402 /// The server of a build whose token is `token`, of the game `game`
403 /// (the window's title).
404 pub fn new(token: String, game: String) -> Self {
405 Self {
406 token,
407 game,
408 hello: false,
409 busy: Busy::Idle,
410 waiting: VecDeque::new(),
411 due: Vec::new(),
412 }
413 }
414
415 /// What `line` (without its line break) asks of `app`.
416 pub fn handle<G: GameData>(&mut self, line: &str, app: &App<G>) -> Handled {
417 if line.len() > MAX_LINE {
418 return Handled::Close(None);
419 }
420 if !self.hello {
421 return self.hello(line);
422 }
423 if !matches!(self.busy, Busy::Idle) || !self.waiting.is_empty() {
424 self.waiting.push_back(line.to_owned());
425 return Handled::Later;
426 }
427 self.request(line, app)
428 }
429
430 /// The first line: a hello with the token and this link's version.
431 fn hello(&mut self, line: &str) -> Handled {
432 let Ok(Request::Hello { token, link }) = serde_json::from_str::<Request>(line) else {
433 return Handled::Close(None);
434 };
435 if token != self.token {
436 return Handled::Close(None);
437 }
438 let answer = Answer::Hello {
439 link: VERSION,
440 game: self.game.clone(),
441 }
442 .to_line();
443 if link != VERSION {
444 return Handled::Close(Some(answer));
445 }
446 self.hello = true;
447 Handled::Answer(answer)
448 }
449
450 /// A request after the hello, with nothing before it waiting.
451 fn request<G: GameData>(&mut self, line: &str, app: &App<G>) -> Handled {
452 let request = match serde_json::from_str::<Request>(line) {
453 Ok(request) => request,
454 Err(e) => return Handled::Answer(Answer::error(format!("not a request: {e}"))),
455 };
456 let answer = match request {
457 Request::Hello { .. } => Answer::error("the link has said hello already"),
458 Request::Status => Answer::Status(Status::of(app)).to_line(),
459 Request::Frame => frame(app).to_line(),
460 Request::Log { since } => Answer::Log(log(app.log(), since)).to_line(),
461 Request::Perf {
462 since,
463 max,
464 snapshot,
465 } => {
466 let most = u32::try_from(PERF_FRAMES).unwrap_or(u32::MAX);
467 if !(1..=most).contains(&max) {
468 return Handled::Answer(Answer::error(format!("max: 1 to {most}, not {max}")));
469 }
470 if snapshot {
471 self.busy = Busy::Snapshot { since, max };
472 return Handled::Snapshot;
473 }
474 Answer::Perf(perf(app, since, max, None)).to_line()
475 }
476 Request::Screenshot => {
477 self.busy = Busy::Screenshot {
478 limit: shot_limit(app),
479 };
480 return Handled::Screenshot;
481 }
482 Request::Play { steps } => {
483 if let Err(e) = check_play(&steps) {
484 return Handled::Answer(Answer::error(e));
485 }
486 // A play of no frames (nothing, or waits of none) is done.
487 let none =
488 |step: &Step| matches!(step, Step::Wait(s) if script::wait_frames(*s) == 0);
489 if steps.iter().all(none) {
490 Answer::Played(Played {
491 frames: 0,
492 waited_loading: false,
493 waited_suspended: false,
494 })
495 .to_line()
496 } else {
497 self.busy = Busy::Play(Play {
498 steps,
499 frames: None,
500 run: 0,
501 waited_loading: false,
502 waited_suspended: false,
503 });
504 return Handled::Later;
505 }
506 }
507 };
508 Handled::Answer(answer)
509 }
510
511 /// This frame's input from the play being played, if one is: the next
512 /// frame of its steps, put in `out` after what is there. While the
513 /// game loads or is suspended the steps wait. A play whose game failed
514 /// to load, or one of whose actions can't be pressed, ends with an
515 /// error, having played nothing more.
516 pub fn input<G: GameData>(&mut self, app: &App<G>, out: &mut Vec<RawInputEvent>) {
517 let Busy::Play(play) = &mut self.busy else {
518 return;
519 };
520 if let Some(errors) = app.errors() {
521 let error = format!("the game didn't load, so the steps can't be played: {errors}");
522 self.end_play(Answer::error(error));
523 return;
524 }
525 if app.is_loading() {
526 play.waited_loading = true;
527 return;
528 }
529 if app.is_suspended() {
530 play.waited_suspended = true;
531 return;
532 }
533 let Some(game) = app.game() else {
534 return;
535 };
536 if play.frames.is_none() {
537 match script::frames(&play.steps, game.input().keymap(), PadKind::default()) {
538 Ok(frames) if frames.is_empty() => {
539 let played = Played {
540 frames: 0,
541 waited_loading: play.waited_loading,
542 waited_suspended: play.waited_suspended,
543 };
544 self.end_play(Answer::Played(played).to_line());
545 return;
546 }
547 Ok(frames) => play.frames = Some(frames.into()),
548 Err(e) => {
549 self.end_play(Answer::error(e));
550 return;
551 }
552 }
553 }
554 let frames = play.frames.get_or_insert_with(VecDeque::new);
555 match frames.front_mut() {
556 Some(Planned::Events(events)) => {
557 out.append(events);
558 frames.pop_front();
559 }
560 Some(Planned::Idle(n)) => {
561 *n = n.saturating_sub(1);
562 if *n == 0 {
563 frames.pop_front();
564 }
565 }
566 None => {}
567 }
568 play.run = play.run.saturating_add(1);
569 }
570
571 /// Ends the play with `answer`, due after this frame.
572 fn end_play(&mut self, answer: String) {
573 self.busy = Busy::Idle;
574 self.due.push(answer);
575 }
576
577 /// The answers due now that `app`'s frame has run: a play's whose last
578 /// frame it was, then those of the requests that waited behind it, up
579 /// to the next play or picture.
580 pub fn after_frame<G: GameData>(&mut self, app: &App<G>) -> Vec<String> {
581 match &mut self.busy {
582 Busy::Play(play) if play.frames.as_ref().is_some_and(VecDeque::is_empty) => {
583 let answer = Answer::Played(Played {
584 frames: play.run,
585 waited_loading: play.waited_loading,
586 waited_suspended: play.waited_suspended,
587 });
588 self.end_play(answer.to_line());
589 }
590 Busy::Screenshot { limit } => *limit = shot_limit(app),
591 Busy::Idle | Busy::Play(_) | Busy::Snapshot { .. } => {}
592 }
593 while matches!(self.busy, Busy::Idle)
594 && let Some(line) = self.waiting.pop_front()
595 {
596 match self.request(&line, app) {
597 Handled::Answer(answer) => self.due.push(answer),
598 Handled::Later | Handled::Screenshot | Handled::Snapshot | Handled::Close(_) => {}
599 }
600 }
601 core::mem::take(&mut self.due)
602 }
603
604 /// Whether the backend should read the window after this frame and
605 /// answer with [`screenshot`](Self::screenshot).
606 pub fn wants_screenshot(&self) -> bool {
607 matches!(self.busy, Busy::Screenshot { .. })
608 }
609
610 /// The answer to a [`Request::Screenshot`]: `png`, the window's
611 /// picture of `width` × `height` pixels. Requests behind it are
612 /// answered from the next [`after_frame`](Self::after_frame).
613 pub fn screenshot(&mut self, png: &[u8], width: u32, height: u32) -> String {
614 let limit = match core::mem::replace(&mut self.busy, Busy::Idle) {
615 Busy::Screenshot { limit } => limit,
616 other => {
617 self.busy = other;
618 None
619 }
620 };
621 Answer::Screenshot(Screenshot {
622 png: base64::encode(png),
623 width,
624 height,
625 limit,
626 })
627 .to_line()
628 }
629
630 /// Whether the backend should take a heap snapshot after this frame
631 /// and answer with [`snapshot`](Self::snapshot).
632 pub fn wants_snapshot(&self) -> bool {
633 matches!(self.busy, Busy::Snapshot { .. })
634 }
635
636 /// The answer to a [`Request::Perf`] that asked for a snapshot:
637 /// `app`'s records it asked for, and `snapshot`, the backend's
638 /// ([`App::heap_snapshot`]; `None` where it takes none). Requests
639 /// behind it are answered from the next
640 /// [`after_frame`](Self::after_frame).
641 pub fn snapshot<G: GameData>(
642 &mut self,
643 app: &App<G>,
644 snapshot: Option<HeapSnapshot>,
645 ) -> String {
646 let (since, max) = match core::mem::replace(&mut self.busy, Busy::Idle) {
647 Busy::Snapshot { since, max } => (since, max),
648 other => {
649 self.busy = other;
650 (app.perf().next_seq(), PERF_MAX)
651 }
652 };
653 Answer::Perf(perf(app, since, max, snapshot)).to_line()
654 }
655}
656
657/// The frame `app` shows, as text.
658fn frame<G: GameData>(app: &App<G>) -> Answer {
659 match app.game() {
660 Some(game) => Answer::Frame {
661 frame: game.buffer().to_snapshot(&game.ctx().palette),
662 limit: None,
663 },
664 None => Answer::Frame {
665 frame: String::new(),
666 limit: Some(not_running(app)),
667 },
668 }
669}
670
671/// Why `app` shows no game: its loading card, or the errors of its load.
672fn not_running<G: GameData>(app: &App<G>) -> String {
673 match app.errors() {
674 Some(errors) => format!("the build's game didn't load: {errors}"),
675 None => "the build shows its loading card".to_owned(),
676 }
677}
678
679/// What a picture of `app`'s window doesn't show.
680fn shot_limit<G: GameData>(app: &App<G>) -> Option<String> {
681 app.game()
682 .is_none()
683 .then(|| format!("{}: the picture shows that, not the game", not_running(app)))
684}
685
686/// `ring`'s kept lines numbered `since` or more, each with its number.
687fn log(ring: &LogRing, since: u64) -> LogAnswer {
688 let next = ring.next_seq();
689 let kept = u64::try_from(ring.lines().len()).unwrap_or(u64::MAX);
690 let first = next.saturating_sub(kept);
691 let lines = (since.max(first)..)
692 .zip(ring.since(since))
693 .map(|(seq, line)| LogEntry {
694 seq,
695 frame: line.frame,
696 level: line.level.name().to_owned(),
697 category: line.category.name().to_owned(),
698 text: line.text.clone(),
699 })
700 .collect();
701 LogAnswer {
702 lines,
703 next,
704 dropped: first.saturating_sub(since),
705 }
706}
707
708/// `app`'s records numbered `since` or more, the first `max`, with
709/// `snapshot`.
710fn perf<G: GameData>(
711 app: &App<G>,
712 since: u64,
713 max: u32,
714 snapshot: Option<HeapSnapshot>,
715) -> PerfAnswer {
716 let mut answer = perf_read(app.perf(), since, max);
717 answer.frames = app.frame_number();
718 answer.snapshot = snapshot;
719 answer
720}
721
722/// The first `max` records of `ring` numbered `since` or more, as the
723/// contract's `PerfRead::of` reads them: a `since` past the ring's next
724/// number (a build started again) reads nothing, with `next` 0. No frames
725/// and no snapshot.
726fn perf_read(ring: &PerfRing, since: u64, max: u32) -> PerfAnswer {
727 let next_seq = ring.next_seq();
728 let kept = u64::try_from(ring.records().len()).unwrap_or(u64::MAX);
729 let first = next_seq.saturating_sub(kept);
730 let mut answer = PerfAnswer {
731 records: Vec::new(),
732 next: 0,
733 dropped: 0,
734 measured: next_seq > 0,
735 frames: 0,
736 snapshot: None,
737 };
738 if since > next_seq {
739 return answer;
740 }
741 let max = usize::try_from(max).unwrap_or(usize::MAX);
742 answer.records = ring.since(since).take(max).cloned().collect();
743 answer.next = since.max(first) + u64::try_from(answer.records.len()).unwrap_or(u64::MAX);
744 answer.dropped = first.saturating_sub(since);
745 answer
746}
747
748/// Whether a play's `steps` can be played: each step
749/// ([`Step::check`](script::Step::check)), waits of at most [`MAX_WAIT`]
750/// seconds, and no crank or debug step; an error names the step. An action's name is
751/// looked up when the play starts.
752pub fn check_play(steps: &[Step]) -> Result<(), String> {
753 for (i, step) in steps.iter().enumerate() {
754 step.check().map_err(|e| format!("step {}: {e}", i + 1))?;
755 if matches!(step, Step::Crank(_)) {
756 return Err(format!("step {}: crank: not over the live link yet", i + 1));
757 }
758 if matches!(step, Step::Debug(_)) {
759 return Err(format!("step {}: debug: not over the live link yet", i + 1));
760 }
761 }
762 script::check_waits(steps, MAX_WAIT)
763}
764
765#[cfg(test)]
766mod tests;