1use 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#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct Synth {
33 pub frames: u64,
34 pub spans_per_frame: u32,
36 pub seed: u64,
37 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
53struct 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 fn f(&mut self) -> f64 {
69 (self.next() >> 11) as f64 / (1u64 << 53) as f64
70 }
71
72 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
108trait 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
128struct 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 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 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 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 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 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 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 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 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}