Skip to content

perf: phone-shell transitions — main-thread wakes, icon tessellation, shade and drawer fill rate - #28

Closed
ymote wants to merge 9 commits into
perf/android-idle-repaintfrom
perf/transition-main-thread
Closed

ymote wants to merge 9 commits into
perf/android-idle-repaintfrom
perf/transition-main-thread

Conversation

@ymote

@ymote ymote commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Draft: the OnePlus 6 (Android 15) numbers are still to come; see the placeholders below.

Phone-shell transition performance on the OnePlus 6T (Snapdragon 845 / Adreno 630, Android 11, 1080×2340 @ 60 Hz), profiled with root simpleperf --call-graph dwarf and atrace. Stacked on #27 (perf/android-idle-repaint). The makepad pin moves to OctoSense-org/makepad#7 (perf/android-main-thread).

What the profile found

Render-thread samples on PR #27's build, OnePlus 6T, Android 11:

scenario App event dispatch hosted AppCard draw_phone_scene AppIconDraw → render_svg render_view
shade 34 % 22 % 33 % 27 % → 23 % 20 %
open-app 56 % 35 % 16 % 12 % → 11 % 15 %
swipe home 49 % 33 % 16 % 14 % → 12 % 18 %
page swipe 52 % 34 % 21 % 17 % → 15 % 12 %
Recents hold 34 % 23 % 18 % 13 % → 11 % 33 %
Recents → Home 50 % 32 % 24 % 20 % → 17 % 12 %
group open 50 % 34 % 24 % 21 % → 18 % 15 %

The hosted AppCard's share. It came from Event::Signal and Event::Timer: a worker signal once per animation frame, plus a 125 Hz run-view timer. Each event ran the AppCard's update_connection_indicator script eval.

After the fixes (same phone):

  • AppIconDraw 0.8–1.8 %
  • render_svg ≤ 0.6 %
  • glTexImage2D 0 %
  • the module path 5–12 %

Scheduling:

  • The render thread ran at nice 0 and spent 21–28 % of its animation time on the little cores. It is now at nice −10 on cpus 4–7 (fork).
  • Run-queue waits were short, and there was no lock contention.
  • eglSwapBuffers/queueBuffer took 22–73 ms in slow frames, which is GPU back-pressure.

The shade A/B. CPU draw stays at 4–5.5 ms in every variant. The swap wait falls from 7–21 ms to 1.5–3.7 ms once the stacked full-screen blends are removed, and that variant reaches 60 fps. The remaining cost is fill rate, not GL driver overhead, so a Vulkan path would not remove it.

Commits

commit what
6419f53 wm: a run view ticks its child only while a process is attached. A 125 Hz timer ran through the whole app, the hosted AppCard included, on a phone with no child processes.
2a60bac, e8f60b0, efc153c deps: makepad pin to perf/android-main-thread, same www alias routing (one makepad lineage, no ad8f372).
415e3e6 mobile: the Android app drawer hides the wallpaper, including under glass.
0b68c89 mobile: the shade's sheet is the compositor's final glass. This removes a second full-screen scene segment on every frame of the pull.
5fa3dab mobile: the shade is one glass layer. Its tint is folded in exactly, the dim is drawn only below the sheet, and its glyphs are warmed once before the first pull.
2eca43c mobile: under Android's drawer only the system-bar strips are filled.
242eb8c docs: the fourth column in docs/perf-mobile-shell.md.

The pin commits track the fork head: aa21709, then e596207, then 73dfc62. The last adds the script VM's probe lookups passing NoTrap (OctoSense-org/makepad#7).

Before / after (SurfaceFlinger, monitor off, two fresh launches per build)

Cells: burst fps · p95 ms · intervals > 33 ms / burst intervals · worst first frame (ms).

OnePlus 6T, Android 11 (bf0a4730)

scenario before run 1 before run 2 after run 1 after run 2
open-app-tap-AppCard-icon 41.4 · 49.9 · 4/20 · 49.9 30.1 · 50.0 · 5/21 · 16.6 40.1 · 66.5 · 4/20 · 49.9 33.4 · 166.3 · 4/20 · 16.6
swipe-up-home 43.7 · 33.3 · 0/24 · 33.3 42.1 · 33.3 · 1/21 · 49.9 45.6 · 33.3 · 0/22 · 16.6 41.3 · 33.3 · 1/22 · 49.9
shade-pull-close 33.7 · 49.9 · 9/46 · 66.5 33.9 · 49.9 · 8/48 · 83.2 45.1 · 49.9 · 3/33 · 49.9 45.7 · 49.9 · 2/35 · 33.3
page-swipe-right-then-back 41.8 · 66.5 · 2/32 · 199.6 35.1 · 133.1 · 2/28 · 133.1 37.1 · 83.0 · 2/29 · 199.7 47.0 · 33.3 · 1/25 · 33.3
recents-swipe-up-hold 34.9 · 33.3 · 1/75 · 149.7 34.5 · 49.9 · 4/74 · 83.2 38.8 · 33.5 · 0/84 · 33.5 36.3 · 33.3 · 1/84 · 149.6
recents-to-home 30.8 · 66.5 · 2/21 · 199.6 41.5 · 66.3 · 1/20 · 66.3 41.5 · 66.5 · 1/20 · 66.5 45.1 · 33.3 · 0/24 · 16.6
island-demo-triple-tap-clock 36.7 · 16.8 · 1/22 · 249.5 49.0 · 49.9 · 2/22 · 66.5 40.7 · 16.8 · 1/23 · 199.6 42.7 · 16.6 · 1/22 · 166.3
group-open-tap-Work 53.6 · 33.3 · 1/115 · 49.9 52.4 · 33.4 · 2/115 · 49.8 52.3 · 33.4 · 2/100 · 33.1 53.9 · 33.3 · 1/130 · 49.9
group-close-tap-scrim 48.1 · 33.3 · 0/16 · 33.3 36.1 · 99.8 · 2/15 · 50.0 48.1 · 49.9 · 1/16 · 49.9 48.1 · 50.0 · 1/16 · 50.0
  • Shade: 34 → 45–46 fps; frames over 33 ms 8–9 → 2–3; first frame 67–83 → 33–50 ms.
  • Recents → Home: 31–42 → 42–45 fps.
  • Recents hold: 35 → 36–39 fps. It starts from the drawer; the drawer fill commit was measured through an A/B switch only (38–40 → 48–49 fps), because this phone disconnected before a harness run on it.

OnePlus 6, Android 15 (cfb7c9e3)

These numbers come from a different phone than the 6T tables above and are not comparable with them. It has the same Snapdragon 845 / Adreno 630, but a 1080×2280 screen, density 450, and gesture navigation. Before is PR #27's build and after is this branch, on the same phone, monitor off, with harness coordinates recalibrated to its screen.

OP6_TABLE

First frame on a fresh launch: OP6_FIRST. Attribution run with the monitor on: OP6_MON.

Shade wait with the monitor on: OnePlus 6T: mean 7–21 ms (worst 57–85) before the shade fixes; 4.9–16 ms (worst 36–47) after; 1.5–3.7 ms with the sheet glass, dim and content switched off. CPU draw stays at 4–5.5 ms throughout, so this is GPU fill rate, not GL driver overhead. OnePlus 6: OP6_WAIT.

Attribution run with the monitor on: see Table 6 in the doc.

Not met

  • No transition reaches ≥ 55 fps with p95 ≤ 20 ms in the 6T harness. Group open is closest at 52–54 fps with p95 33 ms. Recents over Home reaches 52–56 fps. The remaining cost is GPU fill in the glass and full-screen layers.
  • First frames of 133–250 ms remain on open-app, page swipe and the island.
    • Open-app spends 213 ms in the AppCard's script VM.
    • It then spends 190 ms inflating a compressed font asset on the render thread.
  • Candidate fixes:
    • store fonts uncompressed in the APK (cargo-makepad's aapt add), or preload them off the main thread;
    • probe misses in the script VM, handled in the fork.

Tests

  • cargo test: 265 passed.
  • cargo test --features mobile-only: 262 passed.
  • A macOS desktop run draws in both the desktop and the phone style (--test-action capture).
  • Device: OnePlus 6T (Android 11): two fresh-launch harness runs per build plus an attribution run. OnePlus 6 (Android 15): OP6_RUNS.

Not for merge until reviewed.

🤖 Generated with Claude Code

ymote and others added 9 commits September 15, 2026 00:07
Every `MpRunView` started a 125 Hz interval when it was created, whether or
not a child process was attached. The timer's `Event::Timer` went through the
whole app on every pass of the event loop — the hosted AppCard module's widget
tree included — and on the phone, which hosts no child processes, it was the
most frequent event the app handled (about 94 per second, idle or animating,
against 60 next-frames).

The interval now starts in `set_target` when a process attaches and stops when
it detaches; the tick's work (bootstrap resends, `StudioToApp::Tick`) already
did nothing without a target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
aa21709 on top of f1db9eb (OctoSense-org/makepad#6):
- a worker's UI wake no longer paints on Android: `SignalToUI` sent the
  Choreographer's own `RenderLoop` message, so every worker signal drew and
  swapped an extra frame between vsync beats (one per animation frame here);
- Android retires draw storage without raising the UI signal, which reached
  the whole app, the hosted AppCard included, once or twice per frame;
- `AppIconDraw` keeps tessellated icons per device-size bucket instead of
  re-tessellating an icon drawn at two sizes on every draw;
- the render thread runs at nice -10 on the fast cluster.

Same routing as the previous pin: every crate of the OctoSense-org/makepad URL
resolves through the www host alias at this rev, so `octos-app`'s ad8f372
manifest and the workspace share one makepad lineage (one makepad-widgets in
Cargo.lock, no ad8f372 left).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The drawer is an opaque full-screen sheet with no slide, so the wallpaper
under it is never visible, yet the scene plan drew it on every frame, and with
glass up (a home swipe that starts on the drawer blurs it into Recents) a
second time into the backdrop scene. The wallpaper is the most expensive
full-screen fill the phone draws: a Recents hold over the drawer presented at
39 fps (GPU-ready p50 43 ms) against 55 fps (13 ms) over Home on the OnePlus
6T, and the harness reaches Recents from the drawer after its page swipe.

The plan leaves the wallpaper out on Android's drawer. The iOS App Library is
translucent and keeps it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The shade asked the backdrop compositor for a mid-scene checkpoint after all
scene content. That opened a second segment whose only content was a
full-screen copy of the first: one more full-resolution scene pass plus a copy
on every frame of the pull. The sheet samples the finished scene, which is
what `finish`'s final glass snapshots with no copy; the upside-down GL
snapshot that kept the shade off it was fixed in the fork (f1db9eb).

With the keyboard up (its own final glass) the shade keeps the checkpoint.
The census during a pull goes from two `gauss_scene` passes to one.

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

An A/B of the shade's parts on the OnePlus 6T (monitor off, two pulls each)
put the cost in fill rate, not in draw calls: CPU `draw` stayed at 4-5 ms in
every variant while the swap wait fell from 7-21 ms to 1.5 ms with the sheet
glass, dim and content removed (60 fps). The sheet glass was the largest
single part.

- The sheet was a glass at the overview tint plus a 55-62 % tint rect over
  it: two full-width blends. The tint is now folded into a dedicated
  `shade_glass`, exactly: mix(mix(T, g, 0.2), c, a) = mix(T, g', 1 - 0.8 (1 - a)),
  with the rim and inner shadow scaled by (1 - a) as the tint layer covered
  them.
- The full-screen dim is drawn only below the sheet: the sheet's glass fills
  at alpha 1 from a snapshot taken before the overlay, so dim under it never
  reached the screen.
- The first pull after launch spent ~30 ms of a 55 ms frame packing glyphs into
  the atlas under `draw_overlay`. Both pages are now drawn once from a seeded
  copy of the shade, off-screen and transparent, on the first idle frame; the
  live shade still seeds its fixtures on its first real open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two fork commits on top of aa21709:
- the render thread's boost runs after `Event::Startup`, whose task-pool warm
  set it back to nice 0;
- an integer glass blur level reads one mip level and the compositor builds
  ceil(level) mips (the shade and a settled Recents sit at 3.0).

Same www-alias routing: one makepad lineage in Cargo.lock, no ad8f372.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With the drawer open the phone scene drew a full-screen flat fill (the
system-bar colour, standing in for the wallpaper) under the drawer's own
full-screen sheet, both through the SDF chrome quad. A home swipe that starts
on the drawer composes both into Recents' blur, and the frame missed its vsync:
an A/B on the OnePlus 6T (Recents hold from the drawer, monitor off, two runs
each) ran 38-40 fps with both, 42-44 without either one, 48-49 without both
(GPU-ready p50 42 -> 33 ms); the drawer's icons, labels and search pill moved
nothing.

The scene now fills only the strips outside the safe area when the Android
drawer covers the rest, and the drawer's sheet is a flat fill rather than the
chrome quad. The look is unchanged: the drawer is opaque.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
simpleperf call graphs and atrace per transition on the OnePlus 6T: the
render thread's hotspots (icon re-tessellation, the hosted AppCard handling a
worker signal and a 125 Hz timer per frame), its scheduling (nice 0, a quarter
of its time on the little cores), the shade A/B that puts the remaining cost in
GPU fill rate rather than GL driver overhead, the Recents hold's dependence on
starting from the drawer, the fixes with their files, before/after SurfaceFlinger
tables (two fresh launches per build, monitor off), the attribution run, and
what is still short of 55 fps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One fork commit on top of e596207: the script VM's probe lookups (`+:` and a
shader's scope-object field) pass NoTrap, so their expected misses no longer
build a "Did you mean" edit-distance list (part of a hosted AppCard's first
open). Same www-alias routing: one makepad lineage, no ad8f372.

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
chrislearn pushed a commit to chrislearn/OctoSense that referenced this pull request Sep 29, 2026
chrislearn pushed a commit to chrislearn/OctoSense that referenced this pull request Sep 29, 2026
…e993d4c

Home: App Hub 3e993d4c, System-Apps c0f2199c (octos main)
chrislearn pushed a commit to chrislearn/OctoSense that referenced this pull request Sep 29, 2026
OctoSense-System-Apps c0f2199c and App Hub 3e993d4c (pinned by OctoSense-org#28) carry
the AI providers app, octosense-llm-service and octosense-llm-config, and
the `llm` capability; the Mail and llm services name Home's App Hub rev, so
the graph keeps one App Hub. Home links the llm crates under `app-hub`, and
system-apps.json selects ai-providers.

src/llm_host.rs registers `llm` at startup with the AppCard kernel's octos
home as the core dir and, on Android, a camera QR scanner over
cx.show_qr_scanner() (NativeQrScanned / NativeQrCancelled routed from
Home's events; a scan the service did not ask for, such as AppCard's
composer's, is left alone) and an image picker: QrImagePickActivity, an
invisible activity in a task of its own that runs the system picker and
hands the bytes over in a private cache file on the qr.image.result
packet. With AppCard linked (--features app-appcard, opt-in since OctoSense-org#27), a
change to the provider set restarts the AppCard core; without it no change
hook is registered.

System-Apps' AppCard now takes octos from octos main, which no longer
vendors nix; Home's nix patch stays at the 18fcd3f1 copy octos-app itself
patches in, and the comment and READMEs say so.

Co-Authored-By: Claude Opus 5.5 <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