Skip to content

perf(companion): resting robots draw a fraction of the frames - #45

Merged
klNuno merged 4 commits into
boite/bots-mascots-models-perffrom
perf/idle-motion
Oct 14, 2026
Merged

klNuno merged 4 commits into
boite/bots-mascots-models-perffrom
perf/idle-motion

Conversation

@klNuno

@klNuno klNuno commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Cuts the frames the compositor draws for a robot at rest, without a change anyone would see. Follow-up to #43.

What I found first

A trace of Chrome's compositor (DrawFrame, BeginFrame) corrected the starting assumption. An endless animation that is still 93 % of its cycle (the 5 s blink) draws almost nothing: Chrome skips frames whose values did not change. It only keeps the compositor waking up on every vsync. What draws on every frame of the display is the continuous breathing: the box's breathe (scale 1.012 x 1.03), the Jelly's, Bubble's float, Capsule's rock, the Retro's scanline.

What changed

  • src/lib/companion/stepped.ts: slow loops whose motion is tiny are sampled STEP_HZ (20) times a second along their own eased curve and held between samples (steps(1, end)). Rhythm and amplitude stay the same. A loop qualifies only if no step moves any corner of the robot by more than 0.25 px at 128 px, the desktop's 64 px on a screen scaled to 200 %. stepped.test.ts checks that guard for every loop. Stepped: the box's breath (3, 4 and 6 s), the Jelly's breath (4 and 6 s) and the Capsule's doze. Not stepped because they move more: Capsule's rock (11 px), Bubble's float (6 px), the Retro scanline, hops, work motions and the sleeping z.
  • src/lib/companion/beats.ts: one timer for every robot starts the gestures that are still most of their cycle. Each runs only while it shows: the box blink (5 s), RobotFace's blink (4.6 s), the Retro antenna wiggle (4.2 s) and screen flicker (5 s). The rhythm is jittered by ±20 % around the same period, and robots start out of phase. Nothing starts while data-dormant, under reduced motion, or for an element off screen. The one-shot keyframes are the moving slice of the old loops, rescaled, with the same easing.
  • onscreen (same file): one IntersectionObserver marks robots scrolled or clipped out of view with data-offscreen, and bots.css pauses their animations.
  • will-change on the layers that blink or wiggle. Without it, every gesture created and dropped a layer and repainted it: the first try took the gallery from 1 to 14 ms/s of main thread.
  • scripts/perf-probe.ts --frames: traces the compositor per pose and counts frames drawn, compositor wakes and display swaps per second. New poses: one robot of each family awake and asleep (?view=mascots&family=<f>&mood=idle&shapes=1), and the pill and desktop for each family (skin=). The gallery gained the mood= and shapes= parameters it needs.

Measurements

Production build (perf.vite.config.ts), headless Chrome at 60 Hz on the AMD iGPU, 10 s per pose, before and after back to back. At 60 Hz a smoothly animating page draws about 60 frames a second; on a 240 Hz screen it would draw 240. A stepped breath draws about 20 at any refresh rate.

Pose Frames drawn/s Compositor wakes/s Main thread ms/s
closed 45.9 → 60.2 61.7 → 60.2 17.3 → 16.8
open-threads 27.2 → 60.0 60.3 → 60.1 11.1 → 9.1
desktop-idle 49.7 → 60.1 60.1 → 60.1 7.0 → 7.2
desktop-working 51.7 → 60.0 60.0 → 60.0 5.3 → 6.8
rest-box 55.6 → 22.9 60.0 → 60.0 1.0 → 1.2
asleep-box 52.8 → 58.3 60.0 → 60.0 0.9 → 1.2
closed-box 55.1 → 56.3 60.1 → 60.0 15.0 → 16.7
desktop-box 54.9 → 59.4 60.0 → 60.1 6.2 → 7.8
rest-jelly 53.7 → 24.4 60.1 → 60.0 0.9 → 2.0
asleep-jelly 56.4 → 58.8 60.1 → 60.1 1.0 → 1.2
closed-jelly 55.6 → 58.6 60.1 → 60.1 12.0 → 14.2
desktop-jelly 51.3 → 56.5 60.0 → 60.0 5.2 → 6.1
rest-bubble 54.1 → 58.4 60.0 → 60.0 1.0 → 2.2
asleep-bubble 53.6 → 56.5 60.1 → 60.0 0.9 → 1.2
closed-bubble 54.1 → 58.0 60.0 → 60.0 12.6 → 13.8
desktop-bubble 53.0 → 56.4 60.1 → 60.1 6.0 → 6.4
rest-capsule 59.7 → 57.8 60.1 → 60.1 1.1 → 2.2
asleep-capsule 55.0 → 58.5 60.0 → 60.1 1.0 → 1.2
closed-capsule 54.4 → 57.0 60.0 → 60.0 11.8 → 12.5
desktop-capsule 54.9 → 56.2 60.0 → 60.0 5.4 → 6.1
rest-retro 57.0 → 58.4 60.0 → 60.1 1.1 → 4.3
asleep-retro 55.1 → 55.8 60.1 → 60.1 1.0 → 1.2
closed-retro 59.9 → 54.6 60.0 → 60.1 13.8 → 13.4
desktop-retro 60.0 → 57.9 60.0 → 60.1 5.2 → 6.5
  • A resting box or Jelly, the companion's usual state on the desktop and in the pill when no agent works: 55.6 → 22.9 and 53.7 → 24.4 frames drawn per second. On a 240 Hz screen that is about 240 → 25.
  • In-app poses (closed, open-threads, desktop-*) have agents working in the fake core, so their robots hum, scan and pulse continuously. They stay near the display rate before and after. Their single runs swing between 27 and 60. Three repeat runs of closed and open-threads gave 51.6 to 57.1 on both builds.
  • Asleep robots keep drawing: the z drifts 13 px over 3 s, too far to step.
  • Compositor wakes stay at 60 per second wherever any animation runs, a stepped one included. Only a robot with nothing running at all would let the compositor sleep.

Not verified

  • Nothing ran on Windows/WebView2 or a real 240 Hz screen. Headless Chrome's counts stand in for WebView2's; DWM's cost of composing the transparent window is not measured.
  • Visual parity was checked on still captures, not by watching the motion. The stepped breath was reasoned from its sub-pixel step size, not seen at 240 Hz.

Checks

  • bun run check: 0 errors, 0 warnings (308 files)
  • bun run test: 68 files, 495 tests passed (new: stepped.test.ts, beats.test.ts)
  • bun run test:scripts: 0 fail; bun run check:manifest: accepted
  • No Rust touched.

Captures

Not attached: this machine has no image host. Before and after captures were opened and compared, and they match apart from animation phases: the full gallery (?view=mascots) and the open island for each family (pose=open&tab=threads&calm=1&skin=<family>). They are listed in the result file.

@klNuno
klNuno requested a review from ChrisPlayer as a code owner October 14, 2026 11:50
@klNuno
klNuno merged commit 04721c8 into boite/bots-mascots-models-perf Oct 14, 2026
3 checks passed
@klNuno
klNuno deleted the perf/idle-motion branch October 14, 2026 11:54
@ChrisPlayer ChrisPlayer mentioned this pull request Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant