Skip to content

mobile: native-feeling frame pacing on the phone - #26

Closed
ymote wants to merge 6 commits into
fix/hosted-appcard-mobilefrom
perf/mobile-shell
Closed

ymote wants to merge 6 commits into
fix/hosted-appcard-mobilefrom
perf/mobile-shell

Conversation

@ymote

@ymote ymote commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Makes the standalone phone shell draw only when something moves, and measures it on the OnePlus 6T (Snapdragon 845, 1080×2340, 60 Hz) with both SurfaceFlinger (what the display got) and makepad's own frame monitor (what the main thread did). Built on #24 (fix/hosted-appcard-mobile @ fedd802).

How it was measured

  • Baseline = perf/mobile-shell-baseline @ 7b253a8: mobile: the hosted AppCard stays in its client slot and its card survives the light theme #24 plus the instrument commit only (below), no fixes.
  • Perf = this branch @ 57a3134: the same instrument plus the four fix commits.
  • Same 13 scenarios on each build, gestures injected with adb shell input (scratchpad/perf_scenarios2.sh):
    • SurfaceFlinger: dumpsys SurfaceFlinger --latency intervals between actual present times.
    • Frame monitor: the app's [perf] lines, plus one PerfGraph screenshot per scenario.

Table 1 — SurfaceFlinger (display), baseline vs perf

scenario baseline fps baseline p50/p95/max ms baseline >16.7 / >33 perf fps perf p50/p95/max ms perf >16.7 / >33
idle-home 56.9 16.6 / 17.3 / 66.5 22/454 · 3/454 56.2 16.6 / 33.1 / 66.4 24/448 · 5/448
open-app(tap AppCard icon) 24.3 34.6 / 66.7 / 515.9 49/96 · 48/96 27.3 16.7 / 66.6 / 549.3 46/108 · 43/108
idle-app-screen 17.5 50.2 / 66.8 / 83.3 138/138 · 138/138 18.6 49.9 / 66.9 / 116.7 147/147 · 146/147
swipe-up-home 42.8 16.6 / 66.4 / 83.2 32/170 · 25/170 40.6 16.6 / 66.4 / 83.3 36/161 · 30/161
shade-pull+close 40.1 16.6 / 50.1 / 99.8 61/199 · 34/199 41.1 16.7 / 50.0 / 83.1 66/204 · 27/204
page-swipe(right then back) 56.1 16.6 / 32.7 / 66.4 16/279 · 4/279 55.3 16.6 / 16.8 / 116.5 9/275 · 7/275
recents(swipe-up-hold) 38.7 16.7 / 49.5 / 199.7 82/192 · 10/192 39.9 16.7 / 33.6 / 182.8 81/198 · 7/198
recents-idle 30.4 33.3 / 66.6 / 116.9 94/120 · 8/120 33.7 33.3 / 49.9 / 99.8 92/133 · 10/133
recents-to-home 46.1 16.6 / 33.5 / 182.9 44/183 · 5/183 48.6 16.6 / 33.5 / 99.7 36/193 · 6/193
island-demo(triple-tap clock) 53.6 16.6 / 33.3 / 116.5 26/320 · 6/320 52.9 16.6 / 33.3 / 116.0 22/316 · 9/316
group-open(tap Work) 46.1 16.6 / 33.5 / 99.8 41/183 · 6/183 47.3 16.6 / 33.5 / 116.3 38/188 · 6/188
group-idle 41.8 16.6 / 33.7 / 149.4 51/166 · 7/166 44.1 16.6 / 49.9 / 83.2 46/175 · 11/175
group-close(tap scrim) 51.1 16.6 / 33.5 / 133.5 17/152 · 4/152 53.7 16.6 / 33.3 / 66.6 18/160 · 1/160

CPU after idle-home: baseline 78.0 % · perf 43.0 %
CPU after idle-app-screen: baseline 41.0 % · perf 34.0 %

Table 2 — makepad frame monitor ([perf]), baseline vs perf

Gap = main-thread paint-to-paint interval (p50 / p95 / max ms, with (>16.7·>33) counts). ms/frame columns: event · home · module · glass · overlay · shade · groups. Asked by: a=animation g=gesture x=action e=external (nothing in the shell asked).

scenario baseline scenes/s baseline repaints/s baseline gap baseline ms/frame baseline asked by perf scenes/s perf repaints/s perf gap perf ms/frame perf asked by
idle-home 57.0 57.0 16.5 / 26.2 / 81.9 (102·15) 4.48 · 4.76 · 0.00 · 0.02 · 0.03 · 0.00 · 0.00 a513 g0 x0 e0 1.0 56.3 0.0 / 0.0 / 0.0 (0·0) 20.61 · 18.75 · 0.00 · 0.00 · 0.12 · 0.00 · 0.00 a0 g0 x0 e10
open-app(tap AppCard icon) 26.3 33.1 16.6 / 264.6 / 264.6 (26·18) 6.10 · 4.62 · 0.78 · 0.02 · 0.03 · 0.00 · 0.00 a177 g0 x0 e7 2.6 30.4 0.0 / 348.6 / 348.6 (11·11) 26.00 · 6.57 · 8.21 · 0.00 · 0.11 · 0.00 · 0.00 a7 g0 x0 e11
idle-app-screen 1.0 17.3 0.0 / 0.0 / 0.0 (0·0) 49.08 · 1.31 · 0.18 · 0.03 · 0.16 · 0.00 · 0.00 a0 g0 x0 e12 1.0 18.5 0.0 / 0.0 / 0.0 (0·0) 26.19 · 0.03 · 0.23 · 0.00 · 0.22 · 0.00 · 0.00 a0 g0 x0 e11
swipe-up-home 34.8 40.2 17.3 / 76.4 / 94.5 (57·11) 5.05 · 4.46 · 0.01 · 0.02 · 0.10 · 0.00 · 0.00 a203 g3 x0 e3 2.9 35.0 0.0 / 478.0 / 478.0 (15·7) 15.43 · 6.81 · 0.12 · 0.04 · 0.54 · 0.00 · 0.00 a11 g3 x1 e8
shade-pull+close 43.3 43.3 16.8 / 66.9 / 102.4 (97·55) 5.32 · 4.96 · 0.00 · 0.02 · 0.04 · 0.40 · 0.00 a298 g5 x0 e0 6.1 46.1 17.3 / 380.5 / 380.5 (28·20) 9.30 · 5.70 · 0.00 · 0.04 · 0.06 · 1.83 · 0.00 a18 g4 x8 e19
page-swipe(right then back) 55.2 55.2 16.5 / 28.2 / 103.1 (81·10) 6.34 · 1.39 · 0.00 · 0.02 · 0.04 · 0.00 · 0.00 a319 g12 x0 e0 4.0 55.1 16.6 / 262.4 / 262.4 (10·5) 18.14 · 8.52 · 0.00 · 0.00 · 0.06 · 0.00 · 0.00 a1 g9 x8 e10
recents(swipe-up-hold) 37.9 37.9 29.0 / 69.9 / 144.8 (199·18) 9.34 · 2.02 · 0.00 · 0.05 · 0.47 · 0.00 · 0.07 a193 g110 x0 e0 8.7 43.0 27.6 / 92.8 / 490.9 (69·13) 15.34 · 1.53 · 0.00 · 0.09 · 0.84 · 0.00 · 0.24 a1 g67 x0 e10
recents-idle 30.7 30.7 29.6 / 96.9 / 115.2 (210·13) 11.44 · 4.12 · 0.00 · 0.08 · 0.74 · 0.00 · 0.03 a215 g0 x0 e0 1.2 33.2 26.1 / 26.1 / 26.1 (1·0) 40.74 · 9.00 · 0.00 · 0.14 · 1.89 · 0.00 · 0.07 a0 g0 x0 e7
recents-to-home 43.3 43.3 16.8 / 32.2 / 142.4 (154·12) 6.65 · 4.07 · 0.00 · 0.04 · 0.24 · 0.00 · 0.01 a345 g6 x0 e0 3.2 45.9 0.0 / 93.0 / 371.0 (20·4) 26.39 · 5.89 · 0.00 · 0.06 · 0.61 · 0.00 · 0.02 a15 g3 x0 e11
island-demo(triple-tap clock) 52.5 52.5 16.6 / 34.0 / 115.1 (99·16) 4.81 · 4.65 · 0.00 · 0.02 · 0.30 · 0.00 · 0.00 a425 g0 x0 e0 4.4 52.7 16.3 / 75.0 / 166.6 (5·4) 19.10 · 7.33 · 0.00 · 0.00 · 1.21 · 0.00 · 0.00 a20 g0 x6 e5
group-open(tap Work) 47.4 47.4 20.7 / 26.0 / 124.5 (248·16) 5.50 · 4.49 · 0.00 · 0.03 · 0.39 · 0.00 · 0.50 a422 g0 x0 e0 26.0 46.5 20.4 / 44.0 / 112.5 (82·7) 4.83 · 4.88 · 0.00 · 0.01 · 0.42 · 0.00 · 0.50 a153 g0 x2 e1
group-idle 41.2 41.2 21.1 / 73.6 / 109.3 (240·11) 5.85 · 4.52 · 0.00 · 0.03 · 0.39 · 0.00 · 0.95 a247 g0 x0 e0 1.0 44.7 0.0 / 0.0 / 0.0 (0·0) 71.17 · 16.11 · 0.00 · 0.09 · 1.12 · 0.00 · 2.60 a0 g0 x6 e0
group-close(tap scrim) 49.0 49.0 20.3 / 30.7 / 110.6 (89·8) 5.00 · 4.34 · 0.00 · 0.03 · 0.37 · 0.00 · 0.32 a250 g0 x0 e0 4.5 52.5 15.3 / 307.7 / 343.4 (4·3) 19.24 · 8.53 · 0.00 · 0.03 · 0.47 · 0.00 · 0.68 a20 g0 x6 e1

scenes/s = phone scenes the shell drew; repaints/s = window repaints the renderer did (census).

How to read Table 2's ms/frame columns. They are per shell frame. At 1 scene/s every event of that second lands on that one frame, so a larger number there is not a slower frame.

What each commit does

  1. Instrument: the shell on makepad's frame monitor, with a PerfGraph overlay (no behaviour change).
    • Switched on by --test-action perf:on, OCTOSENSE_PERF=1, or three quick taps on the status-bar battery icon.
    • App channels for the shell's surfaces (home, module, glass, overlay, shade, groups) sit next to the platform's event channel. The phone scene calls frame_boundary, which the Android backend never does.
    • PerfGraph is drawn topmost, above the shade, only while the monitor is on.
    • A [perf] line every 2 s gives the gap p50/p95/max, missed frames, per-channel ms/frame, and what asked for the frames (animation, gesture, action, external).
    • The line ends with a renderer census: repaints against scenes drawn, which passes the latest repaint painted, events by kind and timer id, the backend's repaint flags, the retirement-debt term, and any drawn shader that reads the pass time.
  2. The launcher's app list is answered from memory for a second. launcher::apps() read two settings files and stat'ed every app binary on each call, and the home page called it several times per frame.
  3. The island keeps the frame loop only for deadlines within a frame. A demo's 25-minute countdown held the phone at 60 fps; farther deadlines now wake through the shell's 1 s tick.
  4. The shell draws on demand, and composes only for glass.
    • The phone frame loop used to re-arm whenever the wallpaper was visible, just to drift it. It now runs only while something animates or a finger is down, and the 1 s tick wakes the clock, island timers, the open shade and starting tiles.
    • PhoneState::scene_plan routes a frame through the backdrop compositor only when a frosted surface samples it. It skips the wallpaper shader and the home page under a settled app.
  5. The phone keeps the desktop's full-screen caches out of its frame.
    • On phone styles the desktop wallpaper CachedView is hidden.
    • WmScene renders straight into the window (set_optimize, because View only reads texture_caching when a script is applied).
    • Desktop styles keep the cache and their crossfade.
    • Each repaint the renderer forces (see below) is now one window pass instead of three full-screen passes.

Found, not fixed here (upstream, makepad fork)

What happens. The renderer keeps repainting the idle window at display rate.

Evidence. On every sample of an idle home page the census reads retirement-debt: allocations, with demo_time_repaint set and no dirty or requested passes. The platform repaints about 55 times a second while the shell draws one scene a second (repaints=113 scenes=2).

Where it happens. In makepad's GL backend, retire_free_items keeps returning true because the retained-upload allocation ledger (RetainedAllocationBudget::has_pending_retirements) never settles. On every render that sets demo_time_repaint (opengl.rs:409), and compute_pass_repaint_order repaints every pass recorded in the latest redraw.

What was ruled out. The worker pool is healthy (heavy-lane wait 0.26 ms). Events, timers, widgets and shaders are not involved: dropping the only per-frame timer before the app, and drawing one flat quad, both left the repaints unchanged.

Consequence. The display still gets ~55 presents/s on idle screens on both builds. This PR lowers what each of those repaints costs; the repaints themselves need the ledger fixed upstream.

Tests

cargo test 264 passed · cargo test --features mobile-only 261 passed.

New tests:

  • the scene plan;
  • the island's far deadlines;
  • the app-list memo;
  • the frame monitor's report line, census and battery triple-tap.

Visual check: the home, shade and Recents screenshots match between the builds. The frosted backdrop under the shade and Recents samples the scene upside down on both builds; that is pre-existing (GL snapshot orientation) and not changed here.

🤖 Generated with Claude Code

ymote and others added 6 commits September 14, 2026 22:47
Instrument only, no behaviour change. `--test-action perf:on`,
`OCTOSENSE_PERF=1`, or three quick taps on the status bar's battery icon
switch on `Cx::perf_monitor`:

- the shell's surfaces are app channels next to the platform's own:
  `home` (wallpaper, home page, tiles), `module` (hosted apps and tiles
  drawing into their captures), `glass` (backdrop snapshots, blur
  pyramids, the compositor's finish), `overlay` (status bar, island,
  keyboard, navigation), `shade`, `groups`;
- the Android backend never closes the monitor's frames, so the phone
  scene calls `frame_boundary` at the start of every draw;
- `PerfGraph` is drawn topmost (above the shade) while the monitor is on,
  on the frames the shell draws anyway — it schedules none of its own;
- every 2 s the shell's tick logs a `[perf]` line read back from the
  monitor's ring: frames, p50/p95/max gap, missed 16.7/33 ms frames,
  idle pauses, per-channel ms per frame and what asked for the frames,
  then a census of what the renderer did without the shell: repaints
  (the newest `painted_serial`) against phone scenes drawn, the attached
  passes the latest repaint painted, the events the app saw by kind and
  timer id, the backend's repaint flags (`demo_time_repaint`, dirty and
  requested passes, pending draw-item retirements), the retirement-debt
  term holding them, and any drawn shader that reads the pass time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`launcher::apps()` read `wm/launcher.hides` and `wm/apps.splash` from
disk and stat'ed every app's binary on each call, and the phone shell
asked for it several times per frame (home layout, pager sync, tiles,
groups, launch origins). It now answers from a one-second memo;
`apps_uncached()` is the fresh read. A change to the settings shows
within a second.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rame

`IslandState::step` reported itself active while any auto-finish or
countdown was pending, so the demo's 25-minute countdown held the phone
at 60 fps for as long as it ran. A deadline within a frame is stepped on
the frame loop; a farther one wakes through the shell's 1 s tick
(`needs_step`), with a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured on the OnePlus 6T (perf-before.md): an idle home page presented
54 frames/s at 75 % CPU, Recents 30/s, the group window 42/s; every
transition had 33–100 ms frames.

- The phone frame loop re-armed whenever the wallpaper was visible, only
  to drift the wallpaper. It now runs while something animates or a
  finger is down; the wallpaper's phase advances with those frames and
  holds still otherwise. The shell's 1 s tick (`phone_tick`) wakes what
  changes on its own: the status-bar clock, an island timer, the open
  shade's relative times, a tile client that is still starting.
- `PhoneState::scene_plan` decides per frame what the scene needs: the
  backdrop compositor (a full-screen off-screen scene pass, its copy and
  a blur pyramid per glass) only when a frosted surface samples the
  scene — the overview, the shade, a group window, iOS's dock and
  keyboard — and neither the wallpaper shader nor the home page under a
  settled, opaque app. With a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rame

On the OnePlus 6T the renderer repaints the window at display rate while
the shell is idle: makepad's retained-upload ledger reports retirement
debt (`allocations`) on every render, the GL backend answers with
`demo_time_repaint`, and every pass recorded in the latest redraw is
painted again — 55 presents/s on an idle home page with the shell drawing
one scene a second (the monitor's census: `repaints=113 scenes=2`,
`retirement-debt: allocations`). That is upstream; what the shell
decides is how much each of those repaints costs. It was three
full-screen passes: the window, the `WmScene` framebuffer cache the whole
desk renders into, and the desktop wallpaper's `CachedView` inside it.

On a phone style the desktop wallpaper is hidden (the phone paints its
own) and the scene renders straight into the window: `WmScene::set_caching`
switches the view through `set_optimize`, since `View` only reads
`texture_caching` when a script is applied. Desktop styles keep the cache
and their crossfade. The census now shows the window pass alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nst perf

SurfaceFlinger presents and the frame monitor's `[perf]` census for
thirteen scenarios on the monitor-only baseline and on this branch, what
the fixes changed, and the upstream repaint loop they do not reach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ymote

ymote commented Sep 27, 2026

Copy link
Copy Markdown
Contributor Author

Closing as superseded: every commit in this stack (#22–#28) is already on OctoSense-ROM main, where the phone shell continued. Per ADR 0001 the phone layer moves into the shared crates/shell from that copy when OctoSense-Desktop and OctoSense-ROM are merged into one OctoSense repository, so nothing here is lost. #25's octos-app pin is obsolete: AppCard now comes from OctoSense-System-Apps on octos main. The branches are kept.

🤖 Generated with Claude Code

@ymote ymote closed this Sep 27, 2026
ymote added a commit that referenced this pull request Sep 28, 2026
… real-kernel check

The manifest's top-level research object is App Hub #26's shape; the os.*
fallback stays until the shells' App Hub pin has #26. The host-tool
round trip also runs on the host-managed server (host-token WebSocket).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chrislearn pushed a commit to chrislearn/OctoSense that referenced this pull request Sep 29, 2026
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
chrislearn pushed a commit to chrislearn/OctoSense that referenced this pull request Sep 29, 2026
…runtime patch

Makepad main now carries both halves of patches/runtime/
makepad-isolate-policy.patch: the isolate policy (makepad OctoSense-org#22) and the
product fixes (OctoSense-org#26: theme-reload resolver and host widgets, retained eval
errors, OpenHarmony capture). So Home locks Octoscript-Makepad 36ad1f22
with an empty runtime-patches lock, which setup now accepts.

AppCard's octos-app and the Camera source move to AppCard main 1cf78120.
Mail stays on eaf3d145 until its native crate reaches AppCard main.
InstanceHandles.windows is left unsupported: on a phone, modules use modals.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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