Skip to main content

kui_trace/
synth.rs

1//! Recordings made up, in the shape a session writes (backlog F250, ADR
2//! 0075 decision 12): for a reader's tests, a bench, and the inspector's
3//! spike, which needs ten minutes at 120 Hz — 72,000 frames, a million
4//! spans — that no Perf tab keeps.
5//!
6//! Each frame's gap holds the loop asleep and woken, a run of pointer moves
7//! or a key now and then, the app's handler with spans of its own, wakes
8//! and asked-for frames; the frame holds `build`, the app's spans nested a
9//! few deep under it, an extension's `fill` placing components — most
10//! replayed, some drawn fresh for a fact that moved — then `layout` and
11//! `emit`, counters, and a present. Runs of frames with no input (burns)
12//! come every few hundred frames with an animation holding them; a slow
13//! frame, a parse on a track of its own, gauges, marks and notes now and
14//! then. Spans are written as a recorder closes them, a child before its
15//! parent. The same `seed` makes the same recording.
16//!
17//! ```
18//! let t = kui_trace::synth::Synth { frames: 120, ..Default::default() }.trace();
19//! let tl = t.timeline();
20//! assert_eq!(tl.frames.len(), 120);
21//! assert!(tl.spans.len() >= 120 * 10);
22//! ```
23
24use std::io::{self, Write};
25
26use crate::codec::Writer;
27use crate::model::{Names, Trace};
28use crate::record::{LoopEvent, NameId, Origin, Record, Session, Span, Track};
29
30/// What to make.
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct Synth {
33    pub frames: u64,
34    /// About how many spans a frame closes, its gap's included.
35    pub spans_per_frame: u32,
36    pub seed: u64,
37    /// The display's refresh rate: the frame budget, and how far apart
38    /// frames come.
39    pub hz: f32,
40}
41
42impl Default for Synth {
43    fn default() -> Self {
44        Synth {
45            frames: 600,
46            spans_per_frame: 14,
47            seed: 1,
48            hz: 120.0,
49        }
50    }
51}
52
53/// xorshift64*: enough randomness for a made-up session, the same for a
54/// seed everywhere.
55struct Rng(u64);
56
57impl Rng {
58    fn next(&mut self) -> u64 {
59        let mut x = self.0;
60        x ^= x >> 12;
61        x ^= x << 25;
62        x ^= x >> 27;
63        self.0 = x;
64        x.wrapping_mul(0x2545_F491_4F6C_DD1D)
65    }
66
67    /// A fraction in `[0, 1)`.
68    fn f(&mut self) -> f64 {
69        (self.next() >> 11) as f64 / (1u64 << 53) as f64
70    }
71
72    /// An integer in `[lo, hi]`.
73    fn range(&mut self, lo: u64, hi: u64) -> u64 {
74        lo + self.next() % (hi - lo + 1)
75    }
76
77    fn chance(&mut self, p: f64) -> bool {
78        self.f() < p
79    }
80
81    fn pick<'a>(&mut self, of: &[&'a str]) -> &'a str {
82        of[self.next() as usize % of.len()]
83    }
84}
85
86const APP_SPANS: &[&str] = &[
87    "rows",
88    "row",
89    "toolbar",
90    "status",
91    "sidebar",
92    "editor",
93    "highlight",
94    "diff",
95    "query",
96    "format",
97];
98const COMPONENTS: &[&str] = &[
99    "kit/table",
100    "kit/tabs",
101    "kit/button",
102    "kit/line_chart",
103    "kit/tree",
104];
105const MOVED: &[&str] = &["hover", "scroll", "layout", "focused", "the clock", "theme"];
106const KEYS: &[&str] = &["key", "button", "wheel", "text"];
107
108/// Where records go: a writer, or a recording in memory.
109trait Sink {
110    fn put(&mut self, r: Record) -> io::Result<()>;
111}
112
113struct ToTrace<'a>(&'a mut Trace);
114
115impl Sink for ToTrace<'_> {
116    fn put(&mut self, r: Record) -> io::Result<()> {
117        self.0.push(r);
118        Ok(())
119    }
120}
121
122impl<W: Write> Sink for Writer<W> {
123    fn put(&mut self, r: Record) -> io::Result<()> {
124        self.write(&r)
125    }
126}
127
128/// The maker's state: names handed out, the clock, span ids.
129struct Gen<'a> {
130    sink: &'a mut dyn Sink,
131    names: Names,
132    rng: Rng,
133    t: u64,
134    next_id: u32,
135}
136
137impl Gen<'_> {
138    fn name(&mut self, s: &str) -> io::Result<NameId> {
139        let (id, new) = self.names.get(s);
140        if new {
141            self.sink.put(Record::Name {
142                id,
143                text: s.to_string(),
144            })?;
145        }
146        Ok(id)
147    }
148
149    fn id(&mut self) -> u32 {
150        self.next_id += 1;
151        self.next_id
152    }
153
154    #[allow(clippy::too_many_arguments)]
155    fn span(
156        &mut self,
157        id: u32,
158        parent: u32,
159        name: &str,
160        origin: Origin,
161        start: u64,
162        dur: u64,
163        node: u64,
164    ) -> io::Result<()> {
165        let name = self.name(name)?;
166        let cpu = Some(dur - dur / 8);
167        self.sink.put(Record::Span(Span {
168            track: Track::MAIN,
169            id,
170            parent,
171            name,
172            origin,
173            start,
174            dur,
175            cpu,
176            node,
177            cut: false,
178        }))
179    }
180
181    /// A tree of the app's spans from `start` lasting `dur`, at most
182    /// `budget` of them and `depth` deep, closed children first; how many
183    /// it made.
184    fn tree(
185        &mut self,
186        parent: u32,
187        start: u64,
188        dur: u64,
189        depth: u32,
190        budget: u32,
191    ) -> io::Result<u32> {
192        let id = self.id();
193        let name = self.rng.pick(APP_SPANS);
194        let node = self.rng.range(1, 400) * 0x1000_0001;
195        let mut made = 1;
196        if depth > 0 && budget > 1 && dur > 8 {
197            let kids = self.rng.range(1, 3).min((budget - 1) as u64) as u32;
198            let mut at = start + dur / 20;
199            let room = dur - dur / 10;
200            for k in 0..kids {
201                let share = room / kids as u64;
202                let len = (share as f64 * (0.4 + 0.5 * self.rng.f())) as u64;
203                let left = (budget - made) / (kids - k);
204                made += self.tree(id, at, len.max(1), depth - 1, left.max(1))?;
205                at += share;
206            }
207        }
208        self.span(id, parent, name, Origin::Host, start, dur, node)?;
209        Ok(made)
210    }
211}
212
213impl Synth {
214    /// The recording, in memory.
215    pub fn trace(&self) -> Trace {
216        let mut t = Trace::new();
217        self.run(&mut ToTrace(&mut t))
218            .expect("a recording in memory cannot fail to write");
219        t
220    }
221
222    /// The recording, streamed to `w` as a `.kuitrace`: one long session
223    /// in a few MB of memory, whatever its length.
224    pub fn write(&self, w: impl Write) -> io::Result<()> {
225        let mut w = Writer::new(io::BufWriter::new(w))?;
226        self.run(&mut w)?;
227        w.flush()
228    }
229
230    fn run(&self, sink: &mut dyn Sink) -> io::Result<()> {
231        let budget = (1e6 / self.hz.max(1.0) as f64) as u64;
232        sink.put(Record::Session(Session {
233            kui: "synth".into(),
234            app: "synth".into(),
235            platform: "synth".into(),
236            refresh_hz: self.hz,
237            budget_ms: budget as f32 / 1000.0,
238            scale: 2.0,
239            wall_ms: 1_761_000_000_000 + self.seed,
240        }))?;
241        let mut g = Gen {
242            sink,
243            names: Names::new(),
244            rng: Rng(self.seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1),
245            t: 0,
246            next_id: 0,
247        };
248        let parse = g.name("parse")?;
249        g.sink.put(Record::Track {
250            id: Track(2),
251            name: parse,
252        })?;
253        let per_frame = self.spans_per_frame.max(6);
254        let mut burn_left = 0u64;
255        for frame in 1..=self.frames {
256            if burn_left == 0 && g.rng.chance(1.0 / 400.0) {
257                burn_left = g.rng.range(35, 90);
258            }
259            let burning = burn_left > 0;
260            burn_left = burn_left.saturating_sub(1);
261            let mut spans = 0u32;
262
263            // The gap: asleep, woken, and what woke it.
264            let gap = if burning {
265                budget
266            } else {
267                budget + g.rng.range(0, budget * 6)
268            };
269            let woke = g.t + gap - gap / 6;
270            let sleep_until = Some(woke);
271            g.sink.put(Record::Loop {
272                t: g.t + 20,
273                event: LoopEvent::Sleep { until: sleep_until },
274            })?;
275            let input = !burning && g.rng.chance(0.55);
276            let by = if burning {
277                "redraw"
278            } else if input {
279                "input"
280            } else {
281                "timer"
282            };
283            let by = g.name(by)?;
284            g.sink.put(Record::Loop {
285                t: woke,
286                event: LoopEvent::Woke { by },
287            })?;
288            let mut causes = Vec::new();
289            let mut at = woke + 10;
290            if input {
291                let pointer = g.rng.chance(0.7);
292                let kind = if pointer {
293                    "pointerMove"
294                } else {
295                    g.rng.pick(KEYS)
296                };
297                let k = g.name(kind)?;
298                let count = if pointer {
299                    g.rng.range(1, 12) as u32
300                } else {
301                    1
302                };
303                g.sink.put(Record::Input {
304                    t: at,
305                    kind: k,
306                    os_t: Some(at.saturating_sub(g.rng.range(100, 900))),
307                    count,
308                    until: at + if count > 1 { 400 } else { 0 },
309                })?;
310                causes.push(k);
311                at += 450;
312                let ev = g.name(if pointer { "pointerMove" } else { "click" })?;
313                g.sink.put(Record::Event {
314                    t: at,
315                    kind: ev,
316                    node: g.rng.range(1, 400) * 0x1000_0001,
317                })?;
318                // The app's handler, a span or two inside it.
319                let h = g.id();
320                let len = g.rng.range(40, 600);
321                let kid = g.id();
322                g.span(kid, h, "update", Origin::Host, at + 5, len / 2, 0)?;
323                g.span(h, 0, "handler", Origin::Host, at, len, 0)?;
324                spans += 2;
325                at += len + 10;
326            }
327            if g.rng.chance(0.08) {
328                let w = g.name("parser")?;
329                g.sink.put(Record::Wake { t: at, name: w })?;
330                causes.push(g.name("wake: parser")?);
331            }
332            if burning {
333                causes.push(g.name("owed")?);
334            }
335            if g.rng.chance(0.03) {
336                let why = g.name("caret blink")?;
337                let line = g.name("src/editor.rs:88")?;
338                g.sink.put(Record::Request {
339                    t: at,
340                    why,
341                    at: line,
342                })?;
343                causes.push(g.name("request: caret blink")?);
344            }
345            if g.rng.chance(0.02) {
346                let what = if g.rng.chance(0.5) {
347                    "file opened"
348                } else {
349                    "command run"
350                };
351                let m = g.name(what)?;
352                g.sink.put(Record::Mark {
353                    t: at,
354                    track: Track::MAIN,
355                    name: m,
356                })?;
357            }
358
359            // The frame.
360            let begin = woke + gap / 6 - 1;
361            let begin = begin.max(at + 1);
362            g.sink.put(Record::FrameBegin {
363                t: begin,
364                frame,
365                causes,
366            })?;
367            if burning {
368                let what = g.name("transition")?;
369                let name = g.name("progress (width)")?;
370                g.sink.put(Record::Holder {
371                    frame,
372                    what,
373                    name,
374                    node: 0x2000_0002,
375                })?;
376            }
377            let slow = !burning && g.rng.chance(0.01);
378            let work = if slow {
379                budget * g.rng.range(2, 4)
380            } else {
381                (budget as f64 * (0.15 + 0.35 * g.rng.f())) as u64
382            };
383            let build_len = work * 6 / 10;
384            let layout_len = work * 25 / 100;
385            let emit_len = work.saturating_sub(build_len + layout_len + 30).max(1);
386            let build = g.id();
387            let b0 = begin + 10;
388            // The app's spans, then an extension's fill, inside build.
389            let app_room = build_len * 6 / 10;
390            let app_budget = per_frame.saturating_sub(spans + 5).max(1);
391            let mut made = 0;
392            let mut cursor = b0 + 5;
393            while made < app_budget {
394                let left = app_budget - made;
395                let len = (app_room / app_budget.max(1) as u64 * left.min(6) as u64).max(2);
396                made += g.tree(build, cursor, len, 3, left.min(6))?;
397                cursor += len + 2;
398                if cursor > b0 + app_room {
399                    break;
400                }
401            }
402            spans += made;
403            debug_assert!(spans <= per_frame + 8);
404            // A fill a component, as the runtime records one: the span,
405            // then its placement as it ends.
406            let fill_at = b0 + app_room + 20;
407            let fill_len = build_len.saturating_sub(app_room + 40).max(1);
408            let kit = g.name("kit")?;
409            let count = g.rng.range(2, 6);
410            let each = (fill_len / count).max(2);
411            for c in 0..count {
412                let kind = g.rng.pick(COMPONENTS);
413                let component = g.name(kind)?;
414                let fresh = g.rng.chance(if burning { 0.6 } else { 0.15 });
415                let (answer, why) = if !fresh {
416                    ("replayed", String::new())
417                } else if g.rng.chance(0.7) {
418                    let fact = g.rng.pick(MOVED);
419                    let why = match fact {
420                        "the clock" | "theme" => fact.to_string(),
421                        f => format!("{f} {:016x}", g.rng.range(1, 400) * 0x1000_0001),
422                    };
423                    ("reads", why)
424                } else {
425                    ("params", String::new())
426                };
427                let answer = g.name(answer)?;
428                let why = g.name(&why)?;
429                let fill = g.id();
430                let at = fill_at + c * each;
431                let len = each - 1;
432                g.span(fill, build, "fill kit", Origin::Extension(kit), at, len, 0)?;
433                g.sink.put(Record::Placement {
434                    t: at + len + 1,
435                    node: 0x3000_0000 + c,
436                    component,
437                    answer,
438                    why,
439                })?;
440            }
441            if g.rng.chance(0.01) {
442                g.sink.put(Record::Note {
443                    t: fill_at + 2,
444                    text: "editor moved, reads fs.stat".into(),
445                })?;
446            }
447            g.span(build, 0, "build", Origin::Kui, b0, build_len, 0)?;
448            let lay = g.id();
449            let l0 = b0 + build_len + 5;
450            g.span(lay, 0, "layout", Origin::Kui, l0, layout_len, 0)?;
451            let emit = g.id();
452            g.span(
453                emit,
454                0,
455                "emit",
456                Origin::Kui,
457                l0 + layout_len + 5,
458                emit_len,
459                0,
460            )?;
461            let end = begin + work;
462            for (name, v) in [
463                ("texts shaped", g.rng.range(0, 3) as i64),
464                ("glyphs rasterized", if g.rng.chance(0.05) { 26 } else { 0 }),
465                ("rows", g.rng.range(30, 60) as i64),
466            ] {
467                if v > 0 {
468                    let name = g.name(name)?;
469                    g.sink.put(Record::Counter {
470                        t: end,
471                        frame,
472                        name,
473                        value: v,
474                    })?;
475                }
476            }
477            g.sink.put(Record::FrameEnd {
478                t: end,
479                frame,
480                unchanged: Some(burning && g.rng.chance(0.5)),
481                nodes: g.rng.range(300, 900) as u32,
482            })?;
483            let render = g.rng.range(budget / 20, budget / 6);
484            let wait = budget.saturating_sub(work % budget + render);
485            g.sink.put(Record::Present {
486                t: end + render + wait,
487                frame,
488                render,
489                wait,
490            })?;
491            if frame % 30 == 0 {
492                let mem = g.name("footprint")?;
493                g.sink.put(Record::Gauge {
494                    t: end + 5,
495                    name: mem,
496                    value: 180e6 + g.rng.f() * 20e6,
497                })?;
498            }
499            if g.rng.chance(0.01) {
500                let id = g.id();
501                let len = g.rng.range(budget, budget * 8);
502                let start = end.saturating_sub(len);
503                let name = g.name("parse")?;
504                g.sink.put(Record::Span(Span {
505                    track: Track(2),
506                    id,
507                    parent: 0,
508                    name,
509                    origin: Origin::Host,
510                    start,
511                    dur: len,
512                    cpu: None,
513                    node: 0,
514                    cut: false,
515                }))?;
516            }
517            g.t = end + render + wait;
518        }
519        Ok(())
520    }
521}