Measure the Pi profile on the Pi, and keep only what helped
The first cut of ?pi=1 switched off the two things that look most expensive - the backdrop-filter glass blur and the decorative CSS animation loops - and made the device slower, not faster: 53.5% of a core idle became 118%. Measured on musicdolphin, sum of the Firefox process tree, idle on the browse screen, 15s average: full app 53.5% + ambient canvas at half resolution 25.0% <- the whole win + 30fps cap on top of that 25.0% (no idle change) + decorative CSS animation loops off 24.6% (noise; left on) + backdrop-filter glass blur off 118.1% <- 5x worse The blur is what promotes each glass panel to its own compositing layer. Without it the animated canvas and the whole album grid above it collapse into one layer and every canvas frame repaints all of it. The most expensive-looking CSS in the app is what was keeping the rest of it cheap. SHOW_GLASS_BLUR and SHOW_DECORATIVE_ANIMATIONS therefore go back to unconditionally on, in both the music app and the typing game, with the numbers written down next to them so the next person does not repeat this. What is left is one real change - paint a quarter of the pixels - plus three caps that only bite while something is playing and so are not in the table above: 30fps on the canvas, 60 bubbles alive at once, and ten progress-bar re-renders a second. Those three are unmeasured; measuring them means playing audio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,21 +2,36 @@
|
||||
*
|
||||
* The device this exists for is musicdolphin: a Pi 4 driving a 1920x1080 monitor
|
||||
* through a Firefox kiosk window. The GPU is fine there (`V3D 4.2`, accelerated), but
|
||||
* Firefox on Linux composites in the parent process, and the two things this app does
|
||||
* every frame - repaint a full-screen canvas, and blur a live backdrop copy behind
|
||||
* every card - were between them eating about 70% of a core with nothing happening on
|
||||
* screen. Everything below is aimed at those two costs and nothing else.
|
||||
* Firefox on Linux rasterizes 2D canvas in the content process, and the ambient canvas
|
||||
* repaints the full screen every frame. Idle - nothing playing, nothing moving but the
|
||||
* gradient - that was half a core in the content process alone.
|
||||
*
|
||||
* The rule the profile follows: *make the expensive things cheaper before switching
|
||||
* any of them off*. The ambient canvas stays - it is the app's whole look - it just
|
||||
* paints a quarter of the pixels half as often, which on a soft gradient behind
|
||||
* content is close to invisible. Only the effects with no cheap version (a real
|
||||
* backdrop blur, the decorative animation loops) actually go.
|
||||
* Everything here was measured on the device rather than reasoned about, and the
|
||||
* reasoning lost. Sum of the Firefox process tree, idle on the browse screen, 15s
|
||||
* average:
|
||||
*
|
||||
* full app 53.5%
|
||||
* + ambient canvas at half resolution 25.0% <- the whole win
|
||||
* + 30fps cap on top of that 25.0% (no idle change)
|
||||
* + decorative CSS animation loops switched off 24.6% (no change; left on)
|
||||
* + backdrop-filter glass blur switched off 118.1% <- 5x WORSE
|
||||
*
|
||||
* That last row is why this module is so much smaller than it started out. On this
|
||||
* device `backdrop-filter` is load-bearing *for performance*: it promotes each glass
|
||||
* panel to its own compositing layer, so a canvas frame underneath repaints only the
|
||||
* canvas. Take the blur away and the canvas and everything stacked above it collapse
|
||||
* into one layer, and every single frame repaints the whole page - all of the album
|
||||
* grid included. The most expensive-looking CSS in the app is the thing keeping the
|
||||
* rest of it cheap. Do not "optimize" it away without re-running the numbers above.
|
||||
*
|
||||
* So the profile is one real change - paint a quarter of the pixels - plus three caps
|
||||
* that only bite while something is playing (bubbles spawn, curves are sampled per
|
||||
* frame, the progress bar re-renders) and that are therefore not in the table above.
|
||||
*
|
||||
* Read once at module load, from the URL and nowhere else: no persistence, no
|
||||
* auto-detection. The kiosk points Firefox at `http://localhost:8080/?pi=1` (see the
|
||||
* `pi_kiosk_url` host var in the ansible repo) and a laptop opening the same page gets
|
||||
* the full-fat version, which is what makes "is this the Pi profile or the hardware?"
|
||||
* the full-fat version, which is what makes "is this the profile or the hardware?"
|
||||
* answerable by editing the address bar.
|
||||
*/
|
||||
|
||||
@@ -33,17 +48,18 @@ function readLowPowerFlag(): boolean {
|
||||
export interface AmbienceQuality {
|
||||
/** Multiplies the canvas backing store relative to its on-screen size, before
|
||||
* `devicePixelRatio`. At 0.5 the loop fills a quarter of the pixels and the browser
|
||||
* scales the result up; on a blurred-by-nature gradient with soft translucent
|
||||
* bubbles over it, that upscale is hard to see and the fill cost is the whole
|
||||
* point - `fillRect` over the gradient is proportional to pixels and happens every
|
||||
* single frame. */
|
||||
* scales the result back up. This is the one change that moved the number: 53.5% ->
|
||||
* 25.0% idle. On a soft gradient with translucent circles drifting over it, the
|
||||
* upscale is not something you can point at on the screen. */
|
||||
resolutionScale: number;
|
||||
/** Hard ceiling on either backing-store axis. Also a guard against a runaway
|
||||
* measurement hitting the browser's canvas allocation limit. */
|
||||
maxBackingStorePx: number;
|
||||
/** Frames per second the loop will paint. 0 means "whatever the display offers".
|
||||
* The gradient eases over ~2s and the bubbles drift; neither has anything to say
|
||||
* at 60fps that it can't say at 30. */
|
||||
* Worth nothing at idle once the resolution is halved - see the table above - and
|
||||
* kept only for the playing case, where each frame also samples the mood curve and
|
||||
* moves every bubble. The gradient eases over ~2s; nothing here has anything to say
|
||||
* at 60fps it cannot say at 30. */
|
||||
maxFps: number;
|
||||
/** Ceiling on bubbles alive at once. 0 means unlimited. Spawn rate is driven by the
|
||||
* music, so a loud track on a big screen can otherwise pile up more circles per
|
||||
@@ -59,10 +75,12 @@ export const AMBIENCE_QUALITY: AmbienceQuality = LOW_POWER
|
||||
* render. 0 means "every animation frame", which is what it has always done.
|
||||
*
|
||||
* `usePlaybackClock` exists to stop the progress bar stepping twice a second, and it
|
||||
* does that with a `setState` per animation frame - so `PlayView` and `PlayerBar`
|
||||
* each re-render sixty times a second for the whole length of a track. A progress bar
|
||||
* a thousand-odd pixels wide advances one pixel every few *hundred* milliseconds on a
|
||||
* does that with a `setState` per animation frame - so `PlayView` and `PlayerBar` each
|
||||
* re-render sixty times a second for the whole length of a track. A progress bar a
|
||||
* thousand-odd pixels wide advances one pixel every few *hundred* milliseconds on a
|
||||
* three-minute track, and the time label beside it only has one-second resolution, so
|
||||
* ten updates a second is already more than either can show.
|
||||
*
|
||||
* Only bites while something is playing, so it is not in the idle table above.
|
||||
*/
|
||||
export const PLAYBACK_CLOCK_FPS = LOW_POWER ? 10 : 0;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import { LOW_POWER } from "./lowPower";
|
||||
import type { Group } from "./search";
|
||||
import type { UiState } from "./keyboard";
|
||||
|
||||
@@ -35,13 +34,14 @@ export const ROW_SPACING = 25;
|
||||
* container, not per card, so cost stays flat regardless of grid size. */
|
||||
export const ANIMATE_VIEW_TRANSITIONS = true;
|
||||
|
||||
/** Frost the glass panels/cards/rows with a real backdrop blur. `backdrop-filter` is
|
||||
* one of the most GPU-expensive CSS effects in the app - a live backdrop copy+blur
|
||||
* per element, repeated for every album card in the unvirtualized grid - and unlike
|
||||
* the canvas there is no cheaper version of it to fall back to, so `?pi=1` switches
|
||||
* it off outright: a plain translucent background, no blur. This is the one visible
|
||||
* cost of the Pi profile. */
|
||||
export const SHOW_GLASS_BLUR = !LOW_POWER;
|
||||
/** Frost the glass panels/cards/rows with a real backdrop blur.
|
||||
*
|
||||
* The obvious thing to cut on weak hardware, and on the Pi that is exactly wrong: it
|
||||
* measured five times *worse* with the blur off (118% of a core against 25%). The blur
|
||||
* is what promotes each panel to its own compositing layer; without it the animated
|
||||
* canvas and the whole album grid above it share one layer and every canvas frame
|
||||
* repaints all of it. See the table in lib/lowPower.ts before touching this. */
|
||||
export const SHOW_GLASS_BLUR = true;
|
||||
|
||||
/** Render the play view's animated canvas background (gradient + bubbles, both
|
||||
* redrawn every frame at 60fps). Off falls back to `.stage`'s own static CSS
|
||||
@@ -53,10 +53,10 @@ export const SHOW_GLASS_BLUR = !LOW_POWER;
|
||||
export const SHOW_AMBIENCE = true;
|
||||
|
||||
/** Run the purely decorative CSS animation loops: the room page's bubble field and
|
||||
* the dolphin mascot's bob/swim. Individually cheap (opacity/transform only), but
|
||||
* a couple of dozen elements animating forever still means a repaint every frame on
|
||||
* a machine compositing in software, and they carry no information. */
|
||||
export const SHOW_DECORATIVE_ANIMATIONS = !LOW_POWER;
|
||||
* the dolphin mascot's bob/swim. Left on even under `?pi=1`: switching them off on the
|
||||
* Pi moved the idle figure by 0.4 percentage points, which is noise, and they are the
|
||||
* kind of detail this app is for. */
|
||||
export const SHOW_DECORATIVE_ANIMATIONS = true;
|
||||
|
||||
// ------------------------------------------------------------------ colors --
|
||||
|
||||
|
||||
@@ -4,24 +4,21 @@
|
||||
* through components. The hue lives in styles/app.css because CSS is where it is used;
|
||||
* it is repeated here only for the canvas, which cannot read a custom property. */
|
||||
|
||||
import { LOW_POWER } from "../lowPower";
|
||||
|
||||
/** The turquoise lagoon. Music is 210, Hörbücher 55, "Mein Zimmer" 300. */
|
||||
export const HUE = 175;
|
||||
|
||||
/** Frost the glass panels with a real backdrop blur. Expensive on weak GPUs, and the
|
||||
* on-screen keyboard frosts every single key - off under `?pi=1`, like the music app's
|
||||
* matching toggle. */
|
||||
export const SHOW_GLASS_BLUR = !LOW_POWER;
|
||||
/** Frost the glass panels with a real backdrop blur. Looks like the first thing to cut
|
||||
* on a weak GPU; on the Pi it measured five times worse with it off, because the blur is
|
||||
* what gives each panel its own compositing layer. Same finding as the music app's
|
||||
* matching toggle - see the table in ../lowPower.ts. */
|
||||
export const SHOW_GLASS_BLUR = true;
|
||||
|
||||
/** The decorative rising bubbles behind everything. A dozen elements animating forever,
|
||||
* carrying no information, so `?pi=1` drops them. */
|
||||
export const SHOW_BUBBLES = !LOW_POWER;
|
||||
/** The decorative rising bubbles behind everything. A dozen elements on a CSS transform
|
||||
* loop; measured as noise on the Pi, so they stay. */
|
||||
export const SHOW_BUBBLES = true;
|
||||
|
||||
/** The earned pets swimming behind every screen. The reward that is always in view - off
|
||||
* only to rule it out when chasing a performance problem. Kept on even under `?pi=1`: a
|
||||
* handful of images moved by writing `transform`, and taking away what she earned to save
|
||||
* a few frames is the wrong trade. */
|
||||
* only to rule it out when chasing a performance problem. */
|
||||
export const SHOW_AQUARIUM_CREATURES = true;
|
||||
|
||||
/** Fade screens in on entry. */
|
||||
|
||||
Reference in New Issue
Block a user