Benchmark resultsΒΆ
The latest available Android measurements for each comparison are collected here. Use the performance guide to choose what to try on your screen, and the benchmark runbook to reproduce Haze's workloads. Sections use different devices and builds: compare configurations within a table, not timings across sections.
Reading the resultsΒΆ
- CPU frame duration measures UI-thread and RenderThread cost, not GPU shader duration.
- Frame overrun measures how far a frame finishes past its deadline. Negative values mean spare time; more negative is better.
- P90 describes the slower end of the measured frames. Negative P90 overrun does not mean every frame met its deadline.
These are measurements of specific sample workloads, not performance budgets for other apps. Each section records its aggregation method and measurement conditions.
Performance modesΒΆ
These are the latest source-backed results for Quality, Balanced, and Performance with
stable and continuously changing source input. They used a Pixel 8a running Android 17 (full SDK
37.2) at 60 Hz, with fixed-performance mode enabled and normal Android CPU scheduling.
Values are P90 CPU frame duration / P90 frame overrun, in milliseconds, computed from the frames of two passes of eight measured iterations per method, about 2,880 frames per cell. The second pass reversed the method order. The P90s are of the pooled frames, not averages of per-pass P90s. Other style choices stay fixed within each sample.
BlurΒΆ
| Workload | Quality | Balanced | Performance |
|---|---|---|---|
| Stable source | 4.84 / -8.97 | 4.84 / -8.89 | 4.82 / -8.97 |
| Continuously changing source | 5.24 / -6.14 | 5.37 / -6.85 | 5.41 / -7.21 |
GlassΒΆ
| Workload | Quality | Balanced | Performance |
|---|---|---|---|
| Stable source | 4.45 / -10.18 | 4.45 / -10.14 | 4.37 / -10.24 |
| Continuously changing source | 4.80 / -6.65 | 4.60 / -7.86 | 4.64 / -8.96 |
All configurations had spare time at P90. For changing-input Glass, Balanced left about
7.9 ms of deadline margin, compared with 6.7 ms for Quality. Performance used roughly
9% less peak GPU memory than Quality in both workloads. This compares each method's
memoryGpuMaxKb median over the 16 iterations: 93,456 KB versus 103,120 KB for the stable
source, and 93,248 KB versus 103,224 KB for the changing source. CPU frame duration did not
follow one mode ordering across workloads.
These results are not a universal mode ranking
Differences of a few tenths of a millisecond in CPU P90 are within the spread between the
two passes, which reached 0.5 ms (Blur stable Balanced: 5.04 ms in the first pass and
4.54 ms in the second). The order of the modes was not consistent across workloads.
CPU placement was not examined, and fixed-performance mode does not fix it. CPU frame
duration also does not measure GPU shader cost. Check appearance on your screen and
repeat with reversed run order before choosing an override.
Measurement details
The benchmarkRelease APKs were built from commit 193c0774f (2.1.0-SNAPSHOT). The
sample APK SHA-256 was
c78023ad93f68d391686ed46ba210a18b22c285f752b8b7e6d0dba3d9faa66bb and the benchmark APK
403817b0eebc783331814a04c54bab00274fca67b568c48d253944144eaabc6e; they were hashed after
the second pass from the build outputs, and the installed copies were not read back. The Android
build was CP41.260831.019. The device was on AC power at 80% charge, thermal status 0 before
every method, and a battery temperature at or below 30.0Β°C before each invocation. Brightness
was not recorded.
The first pass ran Blur stable, Blur changing, Glass stable, then Glass changing, each in Quality, Balanced, then Performance order. The second pass ran the exact reverse. Each method ran in its own invocation, which produced 192 measured iterations and 192 Perfetto traces across both passes. Method identity, XML success, eight metric runs, and eight distinct non-empty trace references were verified for every archive. Slower completed iterations were retained. The automation dry run was skipped; the archive verification stood in for it.
The Google app was temporarily disabled to avoid a Chromium trace-producer delay, then re-enabled and read back afterward. Fixed-performance mode was disabled and the sample process stopped. The sample ran under the default CPU scheduling; no affinity mask was applied.
Blur rows other than Quality did not record GPU memory. Raw JSON, XML, benchmark messages and
traces are retained locally under
internal/benchmark/build/benchmark-results/modes-2.1.0-vs-2.0.1/; pass1-2.1.0/ and
pass2-2.1.0/ hold the results above. The same directory holds an equivalent two-pass run of
the 2.0.1 tag, which is not used here.
See ADR-0004 and ADR-0005 for adaptive-policy decisions, and ADR-0006 for the public terminology.
Backdrop versus SourcesΒΆ
Native Android Backdrop reads pixels behind the effect from the window, instead of using
captured hazeSource content. See Backdrop guidance for
its practical benefits, fallback requirements, and experimental feature flag.
This comparison used Quality on a Pixel 8a running Android 17 (full SDK 37.2) at 60 Hz,
with fixed-performance mode enabled and normal CPU scheduling. Each method ran eight
iterations in each of two passes, reversing both method and workload order.
Values are the arithmetic mean of the two per-pass P90s, not the P90 of pooled frames.
| Quality workload | Sources CPU P90 (ms) | Backdrop CPU P90 (ms) | Backdrop CPU difference | Sources overrun P90 (ms) | Backdrop overrun P90 (ms) |
|---|---|---|---|---|---|
| Blur, stable input | 4.59 | 5.07 | 10% higher | -7.75 | -2.44 |
| Blur, changing input | 4.59 | 5.12 | 12% higher | -5.40 | -2.21 |
| Glass, stable input | 3.54 | 5.18 | 46% higher | -10.69 | -0.48 |
| Glass, changing input | 3.46 | 4.24 | 23% higher | -1.84 | -0.48 |
| Nine Glass nodes, changing input | 3.94 | 4.74 | 20% higher | -6.85 | -5.18 |
Backdrop had higher CPU P90 in both passes for changing-input Blur and all Glass workloads. All rows retained negative P90 overrun, but Backdrop left less deadline margin. Stable Blur's ranking reversed between passes, so its average does not establish a consistent difference.
Stable input is not an idle screen
These workloads continuously invalidate the effect while leaving source pixels unchanged. They compare retained source output with repeated native Backdrop composition. Use the changing-input rows as the closer reference for scrolling or animated content. These results do not establish that Backdrop is slower on every screen.
Measurement details
The benchmarkRelease build used commit f06cd64a; the Android build was
CP41.260814.003.B1. The device was on AC power at 100% charge. The first pass started
at thermal status 0 and battery temperature 26.0Β°C; after cooling, the reverse pass
started at thermal status 0 and 28.1Β°C. Brightness was unchanged and not recorded.
Ten methods, eight iterations each, and two passes produced 160 measured iterations and 160 Perfetto traces. The first pass ran Sources then Backdrop for each workload. One interrupted second-pass attempt was replaced; no completed comparison was discarded. The Google app was force-stopped to prevent its Chromium trace producer from adding a known collection delay. Table values and percentage differences were calculated before rounding.
Native Backdrop trace counters were present with zero source records, confirming that
the native rows did not measure source fallback. Backdrop also used less peak GPU memory:
the recorded difference was approximately 6β10 MB, based on the mean of each pass's
median memoryGpuMaxKb value.
Perfetto showed scheduler variation: the mean per-iteration RenderThread time share on the little CPU cluster ranged from 0.9% to 49.6% across method executions. Stable Glass remained slower with comparable placement in the first pass and more favourable Backdrop placement in the reverse pass. This supports a narrower conclusion: native Backdrop was not a CPU performance win for this Glass scene on this device.
Raw JSON, benchmark messages, and traces are retained locally under
internal/benchmark/build/pixel8a-2026-09-12-full-backdrop-2pass/.
Glass fixed qualityΒΆ
This comparison isolates six qualityFraction settings for one 280 dp Γ 180 dp Regular
Glass surface using HazeInput.Sources. It used a Pixel 6 running Android 17 (full SDK 37.1)
at 60 Hz, with fixed-performance mode enabled.
Controlled CPU placement
The sample's main thread and RenderThread were restricted to the middle CPU cluster. These are controlled quality comparisons, not expected timings under normal Android scheduling. Do not combine them with the Pixel 8a mode results above.
The table shows changing-input results. Each value is the arithmetic mean of two per-pass P90s, calculated before rounding, not the P90 of pooled frames.
qualityFraction |
CPU frame duration: mean per-pass P90 (ms) | Frame overrun: mean per-pass P90 (ms) |
|---|---|---|
0 |
3.17 | -10.95 |
0.25 |
3.12 | -10.17 |
1f / 3f |
3.11 | -9.77 |
0.5 (Balanced) |
3.17 | -9.11 |
0.75 |
4.87 | -6.34 |
1 (Quality) |
4.74 | -5.53 |
For this scene, Fixed(0.75f) left 2.77 ms less deadline margin than Balanced.
No measured frame missed its deadline. These results do not establish the right setting
for Web, Clear Glass, larger or multiple surfaces, or higher refresh rates. Check both
appearance and frame timing on the actual screen under normal scheduling.
Measurement details
The sweep used a release build. The sample process requested affinity to CPUs 4β5; every Perfetto trace confirmed that main and RenderThread executed only on those CPUs during measurement. Other Android processes remained unrestricted.
Six levels ran with stable and changing input, first in ascending quality order, then descending with workload order reversed. Eight iterations per case produced 192 iterations and 34,753 measured frames. No completed timing iteration was discarded. Runs started at thermal status 0 and a battery temperature at most 30Β°C. Brightness was fixed during measurement; cooldowns dimmed the screen and disabled fixed-performance mode.
The largest changing-input CPU P90 difference between passes was 0.12 ms. Stable-input CPU P90 ranged from 2.60 to 2.72 ms across all levels and both passes.
An earlier normal-scheduling experiment showed CPU rankings sensitive to slower-core placement, including a reversed ranking when two levels were repeated. That experiment remains in git history. This controlled sweep also fixed brightness, so differences between the experiments cannot be attributed to CPU affinity alone.
Glass containersΒΆ
These comparisons measure hazeGlassContainer against independent Glass surfaces on a Pixel 8a
running Android 17 (full SDK 37.2) at 60 Hz, with fixed-performance mode enabled and normal CPU
scheduling. Every row uses HazePerformanceMode.Default,
HazeInput.Sources and the Regular style, and runs eight measured iterations per pass. The steady
and animated comparisons use a continuously changing source; the lazy-scroll and first-appearance
rows use a stationary source. Each per-pass value pools the frames of that pass's eight iterations;
the tables show the arithmetic mean of the two per-pass values. Peak GPU memory uses each
pass's median memoryGpuMaxKb. A second pass ran every method in the reverse order of the first.
Two members: merged against independentΒΆ
The independent row draws two Regular Glass surfaces, each about 134 Γ 180 dp and 12 dp apart,
separately. The container row wraps the same surfaces in hazeGlassContainer(mergeDistance =
32.dp), so they merge into one cluster.
| Row | CPU frame duration P50 (ms) | CPU frame duration P90 (ms) | Frame overrun P90 (ms) | Peak GPU memory (KB) |
|---|---|---|---|---|
| Independent surfaces | 2.70 | 4.62 | -7.18 | 99,476 |
| One container cluster | 2.64 | 4.70 | -2.54 | 113,264 |
HWUI's drawLayer [graphicsLayer] slice shows what each row drew: 2.00β2.01 layers per frame for
the independent surfaces and 1.00β1.01 for the cluster, which draws one layer that covers both
members and the bridge between them.
CPU frame duration P90 was 0.08 ms higher for the container, within the 0.13 ms spread between the independent row's passes. The container row finished about 4.6 ms closer to the frame deadline at P90 and used 13,788 KB (about 14%) more peak GPU memory. CPU frame duration does not measure GPU shader cost. These runs did not measure GPU timings, so they do not show why the container finished closer to its deadline. Neither row missed its deadline at P90.
Animated and resting container workloadsΒΆ
Each row below changes one variable from the 12 dp container row above, except the three-member group and join and leave. The group has a different member count. Join and leave uses 60 Γ 120 dp members, against about 134 Γ 180 dp for the others, and varies their count from two to four. They share its Glass settings and changing source.
| Workload | What changes | CPU frame duration P90 (ms) | Per-pass CPU P90 (ms) | Frame overrun P90 (ms) | Peak GPU memory (KB) |
|---|---|---|---|---|---|
| Resting gap | Two members rest 24 dp apart, between half and the full merge distance | 4.70 | 4.76, 4.65 | -1.17 | 113,264 |
| Group | Three members 48 dp apart, beyond the merge distance, share one groupKey |
4.58 | 4.62, 4.55 | -2.36 | 111,000 |
| Press | The first member is pressed and released three times | 6.19 | 5.79, 6.59 | -1.60 | 113,484 |
| Join and leave | Two to four 60 Γ 120 dp members; one joins or leaves every half second | 5.30 | 5.61, 5.00 | -2.15 | 117,102 |
The 12 dp container row's CPU frame duration P90 was 4.70 ms (4.69 and 4.72 ms per pass), with -2.54 ms frame overrun P90. Every row drew about one Glass layer per frame (0.99β1.02).
- A resting gap between half and the full merge distance matched the 12 dp pair's CPU frame duration and peak GPU memory, but finished about 1.4 ms closer to the deadline at P90. The two passes agreed within 0.01 ms.
- The three-member group matched the two-member container within pass spread.
- Pressing a merged member raised CPU frame duration P90 by about 1.5 ms. The join and leave workload raised it by about 0.6 ms, and its P99 was 10.1 ms against 7.1 ms. Both rows were above the container row in both passes, but their passes differed by 0.8 ms and 0.6 ms, so treat the size as approximate.
- The join and leave workload used 3,838 KB (about 3%) more peak GPU memory than the two-member container. Its member size and count differ from the control, so this compares workloads and does not isolate the cost of membership changes.
CPU placement differed in these two rows. The main thread spent 42β71% of its running time on the little cluster in the press and join-and-leave rows, against 81β86% in the other two-member rows, so part of their CPU difference may come from where the threads ran rather than from the workload.
Lazy container scrollΒΆ
This comparison scrolls a five-column LazyVerticalGrid with 40β45 visible members and a 12 dp gap
over a stationary source. The container row wraps the grid in
hazeGlassContainer(mergeDistance = 16.dp, scrollState = gridState), so adjacent members join
clusters of at most four members. The independent row draws the same members as separate Glass
surfaces, with no container.
| Row | CPU frame duration P50 (ms) | CPU frame duration P90 (ms) | CPU frame duration P99 (ms) | Frame overrun P50 (ms) | Frame overrun P90 (ms) | Peak GPU memory (KB) |
|---|---|---|---|---|---|---|
| Independent members | 16.5 | 25.3 | 31.7 | 7.8 | 17.2 | 159,230 |
| Container | 24.7 | 32.6 | 36.9 | 16.6 | 25.1 | 179,448 |
The container ranked higher in CPU frame duration P90 in both passes, at 33.3 and 31.9 ms against 22.1 and 28.5 ms for the independent members. The gap was 11.2 ms in the first pass and 3.4 ms in the second, so treat the 7.3 ms mean as approximate. Peak GPU memory was about 13% higher.
Mean main-thread running time inside the measured window was about 1,440 ms for the container and 1,180 ms for the independent members, a 22% increase. RenderThread running time was similar, at about 2,020 ms and 2,040 ms. Both rows ran the RenderThread mostly (79β84%) on the big core and the main thread almost entirely on the middle and big cores (36β43% on the big core). Placement was not controlled.
Neither row met the 60 Hz deadline: the median frame overran in both. These runs did not measure GPU timings, so they do not show where the remaining frame time goes. The result describes this deliberately dense scene, not a typical container.
First appearanceΒΆ
The cold-initialization rows attach the Glass at the start of the measured window. The control attaches one independent surface, and the container row attaches a two-member container, so the member counts differ.
| Row | CPU frame duration P50 (ms) | CPU frame duration P90 (ms) | Frame overrun P90 (ms) | Peak GPU memory (KB) |
|---|---|---|---|---|
| One independent surface | 4.64 | 8.02 | -6.00 | 95,028 |
| Two-member container | 6.31 | 9.95 | -1.79 | 107,791 |
Each pass recorded only 10 frames across its eight iterations, so these percentiles come from 20 frames per row and are indicative only. The container had a CPU frame duration P50 about 1.7 ms higher and about 4.2 ms less deadline margin at P90. Neither row missed its deadline at P90.
These results describe a few two- and three-member layouts and one dense grid on one device. They do not establish the cost of other cluster sizes, other styles, or higher refresh rates. Measure clustered screens against independent surfaces before adopting a container.
Measurement details
The benchmarkRelease APKs were built from commit a4803f6a1 (2.1.0-SNAPSHOT). The sample
APK SHA-256 was 3936fc8d6608d53a980662f965faf1cbf5cd8b330dcf76b7846712ccb8f061d5 and the
benchmark APK SHA-256 was
ff92fbf3289f15c5ddf42ff32da3ef5c1e9b44f5e80da987ca6106c8e1aeaf2f. The Android build was
CP41.260831.019. The device was on AC power and reported thermal status 0 throughout.
The second pass ran the first pass's ten methods in exactly the opposite order. Each method ran eight measured iterations in its own Gradle invocation, which produced 160 iterations and 160 Perfetto traces. No completed iteration was discarded. The per-frame layer gates passed on the two-member, resting-gap, group and press rows. The Google app was temporarily disabled to prevent its Chromium trace producer from delaying collection.
CPU placement was not controlled. In the steady two-member rows the main thread ran 81β86% on the little cluster. The RenderThread ran 11β23% on the big core in every container row and 27β31% in the independent row, with similar total running time (352β371 ms). Two lazy-scroll traces had no scheduler data and are excluded from the running-time means.
Raw JSON, traces and device readbacks are retained locally under
internal/benchmark/build/benchmark-results/container-2.1.9TQlOX/.
Blur noise and tint shader reuseΒΆ
On Android 13 and newer, Blur combines nonzero noise with one SrcOver color tint in a single
runtime shader. Animating radius, tint, or noise creates a new effect for almost every frame.
This comparison measured constructing that shader for each new effect, then compiling it once
per process and reusing it for every effect.
It used a Pixel 8a running Android 17 (full SDK 37.2) at 60 Hz, with fixed-performance mode enabled and normal CPU scheduling. Each workload animates one property for three seconds over a constant 240 Γ 180 dp source-backed area, split across one or three nodes.
Values are the arithmetic mean of two per-pass P90s, not the P90 of pooled frames. Constructions and their elapsed time are per three-second measured window.
| Workload | Constructions before β after | Construction time before (ms) | CPU P90 before β after (ms) | Overrun P90 before β after (ms) |
|---|---|---|---|---|
| Tint, 1 node | 127 β 0 | 51.1 | 5.11 β 5.29 | -7.46 β -7.70 |
| Tint, 3 nodes | 381 β 0 | 99.4 | 5.71 β 4.75 | -6.22 β -7.07 |
| Radius, 1 node | 180 β 0 | 70.1 | 5.53 β 5.01 | -7.78 β -8.31 |
| Radius, 3 nodes | 540 β 0 | 134.2 | 6.61 β 5.75 | -7.03 β -7.77 |
| Noise, 1 node | 180 β 0 | 77.6 | 6.66 β 6.26 | -6.56 β -7.00 |
| Noise, 3 nodes | 540 β 0 | 132.5 | 5.97 β 5.38 | -6.01 β -6.30 |
Reuse removed every warm construction. Before, each new effect constructed the shader on the main thread, taking roughly 0.25β0.45 ms each. A fresh process still constructs it once on first use. Stable styles constructed nothing in either build.
CPU frame-duration changes are inconclusive. The two passes of one build differed by up to 1.47 ms in CPU P90, more than any before/after difference in the table. Three-node rows also ran with different CPU placement in each build, in both run orders, so their P90 differences cannot be attributed to shader reuse alone. Neither build missed its deadline at P90 in these workloads.
Measurement details
The benchmarkRelease builds used commits 0f81d5f4 (before) and 92168671 (after) with
an identical benchmark APK; the Android build was CP41.260831.007.A3. The device was on
AC power at 90% charge. Every method started at thermal status 0, with the big CPU at most
34Β°C and the battery at most 28.5Β°C. Brightness was unchanged and not recorded.
Ten methods (the six rows above, stable one- and three-node controls, and one- and three-node process-cold launches) ran eight measured iterations in four passes: before then after, then after then before with method order reversed. This produced 320 iterations and 320 Perfetto traces. No completed iteration was discarded. The Google app was temporarily disabled to prevent its Chromium trace producer from delaying collection.
Construction counts come from a HazeRuntimeShader.construct section nested in
HazeBlur.combinedNoiseTint in the sample process. Its duration measures CPU-side
construction, not backend or GPU shader compilation; GPU timings were not analyzed.
Without reuse, three-node main-thread and RenderThread time ran about 97% on the middle CPU cluster. With reuse, more main-thread time ran on the little cluster and 22β30% of RenderThread time ran on the big core. Main-thread running time still fell by 58β90 ms per window for three nodes. Process-cold launches exceeded their deadline at P90 in both builds; their mean CPU P90 differed by less than 1 ms, passes differed by up to 3.49 ms, and the ranking varied between passes.
Raw JSON, benchmark messages, traces, frozen APKs, and per-method device readbacks are
retained locally under internal/benchmark/build/benchmark-results/noise-tint.eO1A65/.
Input sharingΒΆ
This comparison measures HazeInputSharing on two nested
hazeSource modifiers. An outer host source fills the screen and contains an inner feed source:
a LazyColumn of full-width cards with artwork, text and translucent chips, where every third
card animates like a playing video and every fourth card is faded. Four 40β44 dp controls in a
header sample the feed, and two 40 dp controls at the bottom sample the host. Every control sits
outside both captured subtrees. The video rows animate the video cards; the scroll rows also
scroll the feed at a fixed speed. Each row draws Glass or Blur controls.
Within a row the content, geometry, animation, styles and duration are identical. Only the two
sharing policies change: neither source shares (Never/Never), only the feed shares
(Auto/Never), or both share (Auto/Auto). The Pixel 8a ran Android 17 at 60 Hz with
fixed-performance mode enabled and normal CPU scheduling.
Each value is the arithmetic mean of a forward and a reverse pass of eight measured
iterations per method. Within a pass, CPU P90 and frame overrun P90 come from the pooled frames of
its eight iterations, so the means are not percentiles of all frames. Peak GPU memory is the median
of the per-iteration memoryGpuMaxKb, in MiB. Replays per frame, feed then host, come from the replay oracle in the measurement
details.
| Workload | Controls | Sharing | Replays per frame | CPU P90 (ms) | Overrun P90 (ms) | DrawFrames avg (ms) |
Peak GPU (MiB) |
|---|---|---|---|---|---|---|---|
| Video | Glass | Neither | 7, 3 | 7.05 | -4.77 | 3.56 | 115.6 |
| Video | Glass | Feed only | 1, 3 | 6.62 | -5.61 | 3.29 | 148.7 |
| Video | Glass | Both | 1, 1 | 6.73 | -5.15 | 3.37 | 181.5 |
| Video | Blur | Neither | 7, 3 | 6.84 | -5.63 | 3.40 | 113.1 |
| Video | Blur | Feed only | 1, 3 | 6.69 | -6.33 | 3.17 | 146.0 |
| Video | Blur | Both | 1, 1 | 6.61 | -5.87 | 3.30 | 177.9 |
| Scroll | Glass | Neither | 7, 3 | 8.18 | -3.55 | 3.99 | 142.2 |
| Scroll | Glass | Feed only | 1, 3 | 7.68 | -4.53 | 3.73 | 173.1 |
| Scroll | Glass | Both | 1, 1 | 7.99 | -4.04 | 3.87 | 198.0 |
| Scroll | Blur | Neither | 7, 3 | 7.80 | -4.72 | 3.83 | 141.8 |
| Scroll | Blur | Feed only | 1, 3 | 7.41 | -5.52 | 3.60 | 174.7 |
| Scroll | Blur | Both | 1, 1 | 7.67 | -4.78 | 3.75 | 199.5 |
The replay counts matched the intended topology in every one of the 192 measured iterations. With only the feed sharing, the feed's display list was replayed once per frame instead of seven times. With both sharing, the host's replay drew the feed's texture, so the feed was still replayed once and the host once.
Feed-only sharing had the lowest DrawFrames average and frame overrun in all four rows, and the
lowest CPU P90 in three. Against Neither, it lowered CPU P90 by 0.15β0.50 ms (2β6%) and the
DrawFrames average by 0.23β0.27 ms (6β8%). Sharing both sources also beat Neither in every
row, but by less than feed-only sharing, except for CPU P90 in the Blur video row, where it was
0.08 ms lower than feed-only, inside the pass-to-pass variation. In the scroll rows, Both was
only 0.13β0.19 ms below Neither on CPU P90. This comparison does not show why sharing the host
as well gained less.
Each active full-screen source added memory: the feed raised peak GPU memory by 31β33 MiB, and the host a further 25β33 MiB.
These are small differences on one device and one synthetic feed. The CPU P90 gains are similar in size to the pass-to-pass variation (up to 0.42 ms), so a repeat could change the ranking of close rows. CPU frame duration measures UI-thread and RenderThread cost, not GPU shader time, and no GPU timing was recorded, so these results do not show how much GPU work sharing adds or saves. They also say nothing about sources that are more expensive to replay than this feed, or about a screen whose RenderThread is already close to its budget. Measure a source before opting it in.
Measurement details
Builds. The same two benchmarkRelease APKs ran every invocation. They were built from code at
commit cb12b9e35, with the runbook at commit 0d3c1eb0a. The sample APK
SHA-256 was c2ee22c206113120eccb5ff3de3ce2cbb194c5e8092dfc368614fc4d50321349 and the
benchmark APK SHA-256 was
04c07f07c1840afb96a0e71bb151938cf683a582eaa17537c134b36e9865d2a2. The device was a Pixel 8a
running google/akita_beta/akita:17/CP41.260831.016/16521119 (API 37).
Run order. Each method ran eight measured iterations in its own Gradle invocation, using
GlassFeedBenchmark. The forward pass ran the twelve methods in the order Glass video, Glass
scroll, Blur video, Blur scroll, each as Neither, Feed only, Both. The reverse pass ran
them in exactly the opposite order. That is 24 invocations, 192 iterations, 192 Perfetto
traces and 34,558 measured frames (180 per iteration, except four iterations that
differed by one frame). No completed iteration was discarded. One invocation, forward
feedVideoGlassNestedUnshared, was set aside and rerun after the Google app's trace producer
timed out (see conditions); the original is kept separately.
Aggregation. The table averages the two per-pass values; a mean of per-pass P90s is not a pooled percentile. The per-pass values (forward / reverse) were:
| Workload | Controls | Sharing | CPU P90 (ms) | Overrun P90 (ms) | DrawFrames avg (ms) |
Peak GPU (MiB) |
|---|---|---|---|---|---|---|
| Video | Glass | Neither | 7.12 / 6.97 | -4.62 / -4.92 | 3.59 / 3.52 | 115.7 / 115.6 |
| Video | Glass | Feed only | 6.68 / 6.56 | -5.58 / -5.64 | 3.36 / 3.22 | 148.5 / 148.8 |
| Video | Glass | Both | 6.57 / 6.89 | -5.23 / -5.07 | 3.34 / 3.41 | 181.4 / 181.5 |
| Video | Blur | Neither | 6.81 / 6.87 | -5.59 / -5.67 | 3.34 / 3.47 | 113.1 / 113.1 |
| Video | Blur | Feed only | 6.48 / 6.90 | -6.53 / -6.13 | 3.18 / 3.16 | 146.0 / 146.0 |
| Video | Blur | Both | 6.50 / 6.72 | -5.88 / -5.85 | 3.29 / 3.30 | 178.9 / 176.9 |
| Scroll | Glass | Neither | 8.18 / 8.19 | -3.61 / -3.50 | 3.96 / 4.02 | 142.2 / 142.3 |
| Scroll | Glass | Feed only | 7.63 / 7.74 | -4.46 / -4.59 | 3.70 / 3.75 | 173.1 / 173.1 |
| Scroll | Glass | Both | 8.01 / 7.97 | -3.98 / -4.10 | 3.86 / 3.88 | 198.1 / 198.0 |
| Scroll | Blur | Neither | 7.75 / 7.85 | -4.83 / -4.61 | 3.78 / 3.89 | 141.7 / 141.9 |
| Scroll | Blur | Feed only | 7.42 / 7.40 | -5.43 / -5.61 | 3.57 / 3.63 | 174.8 / 174.7 |
| Scroll | Blur | Both | 7.67 / 7.68 | -4.73 / -4.83 | 3.77 / 3.72 | 199.5 / 199.5 |
Between the two passes, the largest differences were 0.42 ms in CPU P90, 0.40 ms in frame
overrun P90, 0.14 ms in the DrawFrames average and 2.0 MiB in peak GPU memory. Feed-only
sharing had lower CPU P90 than Neither in seven of the eight pass-level comparisons (Blur
video in the reverse pass was 0.03 ms higher), and lower DrawFrames average and frame overrun
in all eight.
Device conditions. The device was on AC power at 80% charge, with a fixed 60 Hz display
(peak_refresh_rate and min_refresh_rate both 60) and fixed-performance mode enabled. Thermal status was 0 at the
start of each pass and after the session; the battery temperature rose from 28.0 to 33.7 Β°C
over the session. The first invocation hit a Perfetto track_event producer timeout caused by
the Google app, so the Google app was disabled with pm disable-user for the rest of the
session, following the runbook's Android 17 fallback. Afterwards, fixed-performance mode and
the refresh-rate settings were reset and the Google app re-enabled. A readback confirmed the refresh-rate settings and the
Google app; fixed-performance mode has no getter.
Replay oracle. Source replays are counted in the Perfetto traces, not from application
recording. Each source holds a fixed alpha marker that is replayed with the source. A replay of
the feed's display list emits one RenderThread slice named alpha caused saveLayer 494x105,
and a replay of the host's emits alpha caused saveLayer 231x105. The oracle.sql query
counted both inside each DrawFrames slice in the measured window, over 34,849 frames. In
every iteration the most common frame was 7 feed and 3 host replays for Neither, 1 and 3 for
Feed only, and 1 and 1 for Both, and 180 frames matched it. The other 289 frames in total,
one to three per trace, replayed fewer markers. The runbook
has the query and its calibration.
CPU placement. placement.sql summed the running time of the main thread and RenderThread
on each CPU during the measured window. Fixed-performance mode does not pin threads to cores.
The RenderThread spent 49β97% of its running time on its most-used cluster, 51β62% in most
iterations. That was the middle cluster (CPUs 4β7) in 159 of 192 iterations and the big core
(CPU 8) in the other 33: 15 of 64 Neither iterations, 5 of 64 Feed only and 13 of 64
Both. The main thread's most-used cluster was the middle cluster in 184 iterations, taking
89% of its running time on average, and the little cluster (CPUs 0β3) in the other 8. Placement was not
controlled and differed between configurations, so it may have contributed to the differences
in the table; the iterations were not split by placement.
Artifacts. Raw benchmark JSON and messages, XML, Perfetto traces, per-method device
readbacks and oracle and placement outputs are retained locally, not published, under
internal/benchmark/build/benchmark-results/input-sharing-1615.cb12b9e35/, with the two
APKs, conditions.txt, run-order.txt and summary.md, which records 192 traces
verified.
Reporter's real-app retest. The report in #1615 came from a different app and device. These results come from the benchmark app, not that app, so they neither confirm nor contradict its findings.
Earlier results. Input-sharing results measured before #1615 described a separate rendered copy of the source, which retained a second replay of it, and are no longer published. The earlier global offscreen flag from #1611 was not equivalent to that copy, and neither is evidence for the current implementation. The flat, single-source feed scenarios have not been re-measured on it. The superseded tables remain in git history.
Placement update deduplicationΒΆ
When an effect's size changed, Haze updated the effect twice in the same frame: once when Compose
placed the node, and again when it reported the node's global position. The second callback now
skips its update if the first already applied the same position, size, and transform, its source
reads completed, and its sources haven't changed since
(#1437). This comparison counts
HazeEffectNode-updateEffect trace slices before and after that change in four Glass profiling
workloads.
It used a Pixel 8a running Android 17 (full SDK 37.2) at 60 Hz, with fixed-performance mode
enabled and normal CPU scheduling. Each workload resizes one or nine Glass effects over a stationary
source for three seconds; shapeUpdateBalanced also animates the corner radii with the size. See
the controlled scenario baseline for the exact workloads. Each
method ran eight iterations in each of two passes, in opposite build order.
Values are before β after. Update counts pool both passes (16 iterations per build). Frame timings are the arithmetic mean of two per-pass P90s, not the P90 of pooled frames.
| Workload | Effects | updateEffect per effect per frame |
per animating frame | CPU P90 (ms) | Overrun P90 (ms) |
|---|---|---|---|---|---|
resizeQuality |
1 | 1.90 β 0.96 | 1.99 β 1.00 | 5.35 β 5.40 | -6.14 β -6.17 |
resizeQuality9 |
9 | 1.84 β 0.93 | 1.99 β 1.00 | 12.72 β 12.53 | 0.58 β 0.62 |
shapeUpdateBalanced |
1 | 2.81 β 1.91 | 2.98 β 1.99 | 5.47 β 5.38 | -6.83 β -7.06 |
shapeUpdateBalanced9 |
9 | 2.76 β 1.85 | 2.99 β 1.99 | 12.94 β 12.75 | 0.47 β 0.24 |
"Per frame" counts every main-thread frame in the measured window, including idle frames at its edges. "Per animating frame" counts only frames that ran at least one update.
The change removed one update from each animating frame. The resize workloads went from two
updates per effect to one. The shapeUpdateBalanced workloads went from about three to about two.
In both builds, the remaining update in those workloads ran inside Recomposer:animation instead
of during placement. That is consistent with the single update Glass runs when state read by its
style changes, here the animated corner radius. A style-only frame already ran one update after
#1434 and
#1439, and this change does not merge that update
with the placement update. The traces show where the update ran, not which code path requested
it, so treat that attribution as likely rather than proven.
The measurements show fewer updates, not a frame-time change. Build differences were at most 0.19 ms in CPU P90 and 0.23 ms in overrun P90, while the two passes of the same build differed by up to 0.42 ms and 0.50 ms. Both nine-effect workloads missed the frame deadline at P90 (positive overrun) in both builds. CPU frame duration does not measure GPU cost.
Measurement details
The benchmarkRelease builds used commits 903b02e9 (before) and 1f1c8a3a (after); the
Android build was CP41.260831.007.A3. The commits differ only in HazeEffectNode and its
tests. internal/benchmark and sample are identical, and both builds produced a
byte-identical benchmark APK. The merged change also contains later commits that were not
measured. One restructures the placement record and drops a per-placement array copy. The
others make the skip rarer when sources join, leave, or change window during layout, and
release references. These workloads keep their sources attached, stationary, and in one
window, so the added conditions should not change their update counts, but the final code was
not re-measured.
The device was on AC power at 90% charge, and every readback reported thermal status 0. Before each measured run, all CPU sensors read 35Β°C or less and the battery 28.4Β°C or less; the runs waited until the CPU sensors read at most 33Β°C and the battery at most 28.5Β°C. The hottest CPU reading after a run was 48Β°C. The display stayed in its 60 Hz mode, and automatic brightness stayed enabled with the setting unchanged at 2. Fixed-performance mode pinned each CPU cluster to its maximum frequency during the passes. Force-stopping the Google app did not stop it from restarting immediately, so it was temporarily disabled to avoid its Chromium trace-producer delay. Fixed-performance mode was then turned off and the Google app was re-enabled.
Four workloads, two builds, two passes, and eight iterations produced 128 measured iterations and 128 Perfetto traces. No completed iteration was discarded. Each method was dry-run on both builds and measured through its own selector. JSON method identity, XML success, iteration count, and trace count were verified before continuing. Pass 1 ran the before build and then the after build, in the order shown above. Pass 2 reversed both the build order and the method order.
Per-pass CPU P90 / overrun P90, in milliseconds:
| Workload | Build | Pass 1 | Pass 2 |
|---|---|---|---|
resizeQuality |
before | 5.40 / -6.23 | 5.30 / -6.05 |
| after | 5.61 / -6.04 | 5.19 / -6.31 | |
resizeQuality9 |
before | 12.84 / 0.61 | 12.59 / 0.55 |
| after | 12.43 / 0.47 | 12.64 / 0.78 | |
shapeUpdateBalanced |
before | 5.46 / -7.08 | 5.47 / -6.58 |
| after | 5.27 / -7.16 | 5.49 / -6.95 | |
shapeUpdateBalanced9 |
before | 12.87 / 0.29 | 13.02 / 0.65 |
| after | 12.80 / 0.39 | 12.70 / 0.09 |
Each measured window is the benchmark's measureBlock slice, about 3.3 s long. Updates are the
HazeEffectNode-updateEffect slices that start in that window in the sample process. Frames are
its main-thread Choreographer#doFrame slices. Animating frames are the frames that contain at
least one update. Totals over both passes:
| Workload | Build | Updates | Frames | Animating frames |
|---|---|---|---|---|
resizeQuality |
before | 5,772 | 3,039 | 2,899 |
| after | 2,897 | 3,015 | 2,897 | |
resizeQuality9 |
before | 51,942 | 3,136 | 2,897 |
| after | 26,082 | 3,132 | 2,897 | |
shapeUpdateBalanced |
before | 8,648 | 3,076 | 2,899 |
| after | 5,776 | 3,020 | 2,896 | |
shapeUpdateBalanced9 |
before | 77,823 | 3,129 | 2,896 |
| after | 51,984 | 3,130 | 2,896 |
The name of a slice's parent shows where each update ran. Before the change, each animating
frame had one update inside AndroidOwner:measureAndLayout (placement) and one inside
Compose:onPositionedCallbacks (the global-position callback). After it, the second one fell
to fewer than two per effect per iteration. These counts add up to the update totals above.
| Workload | Build | measureAndLayout |
onPositionedCallbacks |
Recomposer:animation |
|---|---|---|---|---|
resizeQuality |
before | 2,873 | 2,899 | 0 |
| after | 2,875 | 22 | 0 | |
resizeQuality9 |
before | 25,866 | 26,076 | 0 |
| after | 25,884 | 198 | 0 | |
shapeUpdateBalanced |
before | 2,869 | 2,899 | 2,880 |
| after | 2,874 | 22 | 2,880 | |
shapeUpdateBalanced9 |
before | 25,839 | 26,064 | 25,920 |
| after | 25,812 | 252 | 25,920 |
Perfetto scheduler data showed the main thread and RenderThread running on the middle CPU
cluster (CPUs 4β7) for most of the window in every run. The one-effect workloads sometimes ran
more on the little cluster (CPUs 0β3). An iteration counts as slow-core if either thread spent
more than 25% of its running time in the window on the little cluster. Before the change, 2 of
16 resizeQuality and 2 of 16 shapeUpdateBalanced iterations were slow-core; after it, 4 and
3. No nine-effect iteration was. Excluding slow-core iterations, the pooled-frame CPU P90 moved
from 5.23 to 5.20 ms for resizeQuality (14 and 12 iterations) and from 5.43 to 5.26 ms for
shapeUpdateBalanced (14 and 13 iterations), which is also within the pass-to-pass variation.
The table above includes slow-core iterations.
Two passes per build is a small sample. Update counts were stable across iterations and passes
(for example, 0.92β0.93 per effect per frame for resizeQuality9 after the change), but frame
timings varied more between passes than between builds. Resolving a difference smaller than
about 0.5 ms would need more passes, again in alternating order.
Raw JSON, benchmark messages, XML, and traces are retained locally, outside the repository,
for the before-903b02e9 and after-1f1c8a3a builds, each with two passes. The retained set
also holds the dry-run XML, per-run device readbacks, the two builds' APK hashes, and the
trace-analysis scripts.
Representative workloadsΒΆ
These Glass sample measurements cover paging, an animated timeline, and steady-state scenes with different effect counts. They used a Pixel 6 running Android 17/API 37 at 60 Hz, with locked CPU frequency and eight iterations per scenario.
| Scenario | Workload | P90 CPU frame duration |
|---|---|---|
productPager |
Gallery paging journey | 7.5 ms |
playgroundTimeline |
Gallery animated timeline journey | 11.2 ms |
steadyFull |
Controlled steady state, 1 Glass effect | 5.6 ms |
steadyFull3 |
Controlled steady state, 3 Glass effects | 5.2 ms |
steadyFull9 |
Controlled steady state, 9 Glass effects | 8.1 ms |
The Gallery journeys are closer to visible user interactions than the steady-state controls. The effect-count rows are separate workload measurements, not a per-effect cost formula: three effects were not slower than one in this run. Measure the content and interactions your application actually uses.
Measurement details
The benchmarkRelease build used commit 334557df. Display resolution was 1080Γ2400
and render rate was locked to 60 Hz.
Cold-initialization effectAttach* results are omitted because they attach effects at
the measurement boundary and diagnose delegate/shader creation rather than representative
interactions. They remain covered by the benchmark runbook.
For the historical scrolling and dragging comparison between Haze 1 and Haze 2, see the migration guide.