Skip to content

ci(macrobenchmark): Run the startup benchmark on Sauce Labs and recover its results - #5909

Open
runningcode wants to merge 15 commits into
mainfrom
no/macrobenchmark-sauce-results
Open

ci(macrobenchmark): Run the startup benchmark on Sauce Labs and recover its results#5909
runningcode wants to merge 15 commits into
mainfrom
no/macrobenchmark-sauce-results

Conversation

@runningcode

@runningcode runningcode commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📜 Description

Runs the sentry-uitest-android-macrobenchmark cold-start benchmark on a Sauce Labs real device and gets the numbers back into CI. See the results here.

In a future PR, we will either decide to post this as PR comments OR run this as a pre-release task against the last relase. But for now I'm just getting the mechanism working.

Q: Why is this so complicated?
A: Sauce does not provide any APIs to pull files off the devices unlike firebase test lab or other tools. Here's a link to Sauce docs in case you find something I didn't. So to work around this we echo the benchmarkData.json into logcat in chunks and sauce does give us back a device.log. scripts/parse-macrobenchmark-log.py uses that to reassemble the benchmarkData.json and parse it.

The things my clanker tried are documented in the README of the module so others can check/verify.

Note: sauce labs devices run with unlocked CPU clocks so the benchmarks will be unstable!!

💡 Motivation and Context

Get a reliable Sdk init benchmark!

💚 How did you test it?

Green run on a Pixel 9 Pro XL (api 35) — 31159874391 — recovered all three metrics over 12 iterations:

Metric min median max CoV
SentryAndroid.initSumMs 15.7 21.3 24.3 13.4%
SentryAndroid.initCount 1.0 1.0 1.0 0.0%
timeToInitialDisplayMs 443.4 538.6 612.3 11.9%

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

Before this leaves draft:

  • Remove the temporary push: trigger on this branch from the workflow — it only exists because workflow_dispatch cannot target a workflow absent from the default branch. It is marked Remove before merge in the file.
  • Confirm one green run on the current head, since the device was reverted after the Pixel 4 attempts.
  • Consider if-no-files-found: error on the upload step. A failed Sauce run currently uploads nothing while the step reports success.

Follow-ups, out of scope here:

#skip-changelog

runningcode and others added 12 commits August 7, 2026 10:23
The sentry-uitest-android-macrobenchmark module currently only runs on a
locally connected device. This wires it to Sauce Labs so we can evaluate
whether the cold-start timeToInitialDisplay benchmark can run on the
real-device cloud already used by our other benchmarks.

This is a proof-of-concept, gated behind workflow_dispatch and kept off the
per-PR path. Device-guard errors are intentionally not suppressed: on
non-rooted, unlocked-clock cloud devices the guards (UNLOCKED, DEBUGGABLE,
...) are expected to fire, and seeing which ones fire is the point of the
POC. It also tells us whether timeToInitialDisplay can be retrieved from
Sauce artifacts (benchmark JSON) or must be parsed from the device log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the assembleMacrobenchmark Makefile target and invoke the two gradle
assemble tasks directly from the workflow. Only one workflow uses them, so
the extra indirection isn't worth it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saucectl cannot retrieve Macrobenchmark's output. Its artifacts.download.match
filters a hardcoded list of assets Sauce hosts for a job -- device.log,
junit.xml, video.mp4, network.har, crash.json, screenshots.zip -- and never
reads the device filesystem, so benchmarkData.json and the per-iteration
perfetto traces stay stranded on the device. The POC's "*-benchmarkData.json"
match line could never have matched anything; drop it.

Drive the run through the Real Device Access API instead, which exposes adb
shell and pullFile: reserve a device, install both APKs, run the
instrumentation, then pull the results off. This also recovers the perfetto
traces, the only way to resolve sub-millisecond SDK changes that
timeToInitialDisplay cannot see.

The API is an open-beta add-on, so the workflow probes entitlement before
spending a Gradle build on it, and defaults to a one-iteration smoke run.
…rride

App Storage upload answers 201, not 200, so the strict status check rejected
every upload before a device was even reserved.

Also drop --iterations: androidx.benchmark.iterations only feeds the
microbenchmark path (BenchmarkStateLegacy, MicrobenchmarkPhase).
Macrobenchmark reads its iteration count from the test source and ignores the
argument, so skipping AOT compilation is the only way to shorten a run that
still writes results -- dryRunMode would shorten it but forces outputEnable to
false, producing no benchmarkData.json to retrieve.

Track installations by installationId rather than by the app reference, which
Sauce may echo back normalised.
…g a session

The first run reached the API but matched zero devices: Real Device Access
descriptors are not the ids used in .sauce/*.yml, which carry an OS version and
a region suffix (Google_Pixel_9_Pro_XL_15_real_sjc1).

Fetch the catalog unfiltered and match client-side against both descriptor id
and name, then open the session with the resolved concrete id rather than a
pattern. On no match, print every available Android device so a naming mismatch
is diagnosable from a single run -- the endpoint's own deviceId filter cannot
distinguish "no devices on this account" from "your pattern matched nothing".

Also run the no-match check before --probe-only returns. It previously came
after, so the probe reported success and the real run failed on it minutes
later.
An empty catalog answers 200 exactly like a populated one, so the first probe
reported the API as reachable while there was in fact nothing to run on. Report
device availability and the other regions' catalogs too, so an unentitled
account is distinguishable from a misconfigured region.
On a public-cloud account /devices answers 200 with an empty list, which made
the probe look like a missing entitlement. /devices/status returns the whole
fleet -- 392 devices, all public, 368 available -- and its descriptors are
exactly the ids already used in .sauce/*.yml, so the original
Google_Pixel_9_Pro_XL_15_real_sjc1 is back as the default.

Prefer AVAILABLE devices when a pattern matches several, and report each
match's state so an IN_USE device explains itself.
The Real Device Access API can pull files off a device, but it cannot reserve
one on a public-cloud account: POST /sessions rejects every request with
deviceClasses=[PRIVATE_DEVICE], and the request body has no way to ask for a
public device. Everything up to reservation worked -- App Storage upload, device
resolution from /devices/status, the session request itself -- so this is a
commercial limit, not a technical one. Revert to saucectl and drop the script;
git history has it if we ever lease private devices, since that route would also
return the per-iteration perfetto traces.

Instead have the benchmark echo its own benchmarkData.json into logcat in
chunks, which reaches CI as device.log, the one asset Sauce does hand back.
scripts/parse-macrobenchmark-log.py reassembles it, validates the JSON, and
writes a timeToInitialDisplay table to the job summary.
The recovery worked on the device but not in the parser. Sauce returns
device.log as JSON lines -- {"tag", "message", "level", ...} -- so the payload
arrives with its quotes escaped and cannot be regexed straight out of the raw
line. Decode each entry and match against its message, falling back to the raw
line so the same parser still handles `adb logcat` output from a local run.

Enable pipefail in the workflow step too. Piping into tee meant the step exited
on tee's status, so this very failure reported success while producing no
results.

Verified against the device.log from run 31117974318: timeToInitialDisplayMs
min 443.5 / median 477.3 / max 571.2 over 12 iterations on a Pixel 9 Pro XL.
Document the three routes that do not work -- artifacts.download.match,
Macrobenchmark's own reporting channels, and the Real Device Access API on a
public-cloud account -- so the next person does not re-derive them.
Chunks were merged across every *.log found, so two devices each emitting the
same number of chunks would overwrite each other's indices and reassemble into
one bogus document. Collect per file and report when more than one log carries
results, which also drops the cross-file total comparison that was standing in
for this check.

Build the summary table and per-iteration details in one pass while here.
… env

The repo has no submodules -- no .gitmodules, no gitlink entries -- so
submodules: 'recursive' does nothing; sentry-native-ndk is a Maven dependency
pinned in gradle/libs.versions.toml. GRADLE_ENCRYPTION_KEY is never read as
env.* or $GRADLE_ENCRYPTION_KEY either; setup-gradle takes the secret directly
via cache-encryption-key, which stays.
@sentry

sentry Bot commented Aug 7, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.52.0 (1) release

⚙️ sentry-android Build Distribution Settings

Older hardware would resolve SDK-init regressions better, but
Google_Pixel_4_13_real_us could not be allocated in three attempts -- each
queued ~61 minutes before Sauce gave up with a concurrency retry-timeout,
leaving an empty device log and tests="0". The Pixel 9 Pro XL allocates
reliably and is the device this pipeline is proven green on.
@runningcode
runningcode force-pushed the no/macrobenchmark-sauce-results branch from 9bf03e7 to c1cd5bd Compare August 7, 2026 12:51
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 341.64 ms 431.10 ms 89.46 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
6b019b7 319.84 ms 333.15 ms 13.31 ms
d15471f 322.58 ms 396.08 ms 73.50 ms
b750b96 421.25 ms 444.09 ms 22.84 ms
d217708 411.22 ms 430.86 ms 19.63 ms
604a261 380.65 ms 451.27 ms 70.62 ms
5b1a06b 352.27 ms 413.70 ms 61.43 ms
fcec2f2 357.47 ms 447.32 ms 89.85 ms
7414e9b 370.39 ms 422.18 ms 51.79 ms
fcec2f2 311.35 ms 384.94 ms 73.59 ms
f6cdbf0 314.19 ms 357.59 ms 43.40 ms

App size

Revision Plain With Sentry Diff
6b019b7 0 B 0 B 0 B
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
b750b96 1.58 MiB 2.10 MiB 533.20 KiB
d217708 1.58 MiB 2.10 MiB 532.97 KiB
604a261 1.58 MiB 2.10 MiB 533.42 KiB
5b1a06b 0 B 0 B 0 B
fcec2f2 1.58 MiB 2.12 MiB 551.50 KiB
7414e9b 0 B 0 B 0 B
fcec2f2 1.58 MiB 2.12 MiB 551.51 KiB
f6cdbf0 0 B 0 B 0 B

Previous results on branch: no/macrobenchmark-sauce-results

Startup times

Revision Plain With Sentry Diff
426491f 368.53 ms 448.06 ms 79.53 ms

App size

Revision Plain With Sentry Diff
426491f 0 B 0 B 0 B

…ated call

@Suppress("DEPRECATION") covered the whole candidateDirs expression, including
externalCacheDir, which is not deprecated -- so a future deprecation there would
have been silently swallowed. Scope it to getExternalMediaDirs and record why it
cannot be migrated: it is deprecated in favour of MediaStore, which returns
content URIs rather than the filesystem path androidx.benchmark writes to.
The retrieval rationale these comments carried is documented in the module
README, so removing the duplicated prose loses nothing.

Also drops cache-encryption-key from the Setup Gradle step, leaving this
workflow without the Gradle configuration-cache encryption the other workflows
still configure.
# Temporary scaffolding: workflow_dispatch cannot target a workflow that does not exist on
# the default branch yet, so trigger on pushes to this branch while we validate the flow.
# Remove before merge.
push:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to remove this before merging and just use workflow_dispatch until we decide how we want to use this.

@runningcode
runningcode marked this pull request as ready for review August 7, 2026 14:36
@runningcode runningcode added the sanity-check PR needs a lightweight review for obvious issues label Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sanity-check PR needs a lightweight review for obvious issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant