-
-
Notifications
You must be signed in to change notification settings - Fork 475
ci(macrobenchmark): Run the startup benchmark on Sauce Labs and recover its results #5909
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
runningcode
wants to merge
15
commits into
main
Choose a base branch
from
no/macrobenchmark-sauce-results
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
5acdd0a
ci(macrobenchmark): Run startup benchmark on Sauce Labs (POC)
runningcode 233b430
ci(macrobenchmark): Call gradle directly instead of a Makefile target
runningcode 7e237f5
ci(macrobenchmark): Pull benchmark results via Real Device Access API
runningcode 9128c6f
ci(macrobenchmark): Fix app upload status and drop the iterations oveโฆ
runningcode 559c872
ci(macrobenchmark): Resolve the device from the catalog before openinโฆ
runningcode 1f99849
ci(macrobenchmark): Probe every region when the device catalog is empty
runningcode b8e8b84
ci(macrobenchmark): Resolve devices from /devices/status, not /devices
runningcode 4d56521
ci(macrobenchmark): Recover results through logcat, not the device API
runningcode 2972f5a
ci(macrobenchmark): Decode Sauce's JSON-lines device log
runningcode c212da5
docs(macrobenchmark): Record how Sauce results are retrieved
runningcode f23948c
refactor(macrobenchmark): Collect log chunks per file
runningcode edf75b3
ci(macrobenchmark): Drop unused submodule checkout and encryption keyโฆ
runningcode c1cd5bd
ci(macrobenchmark): Record why the benchmark runs on a high-end device
runningcode e4b8ddf
ref(macrobenchmark): Narrow the deprecation suppression to the deprecโฆ
runningcode fdded9e
ref(macrobenchmark): Trim comments and drop the cache encryption key
runningcode File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| name: 'Integration Tests - Macrobenchmark' | ||
| # Runs the sentry-uitest-android-macrobenchmark cold-start benchmark on a Sauce Labs real | ||
| # device and recovers timeToInitialDisplay from the device log. | ||
| # | ||
| on: | ||
| workflow_dispatch: | ||
| # 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: | ||
| branches: | ||
| - no/macrobenchmark-sauce-results | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| macrobenchmark: | ||
| name: Macrobenchmark | ||
| runs-on: ubuntu-latest | ||
|
|
||
| # we copy the secret to the env variable in order to access it in the workflow | ||
| env: | ||
| SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} | ||
|
|
||
| steps: | ||
| - name: Git checkout | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
|
|
||
| - name: 'Set up Java: 17' | ||
| uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 | ||
| with: | ||
| distribution: 'temurin' | ||
| java-version: '17' | ||
|
|
||
| - name: Setup Gradle | ||
| uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 | ||
|
|
||
| - name: Assemble target app and Macrobenchmark apk | ||
| if: env.SAUCE_USERNAME != null | ||
| run: ./gradlew :sentry-samples:sentry-samples-android:assembleRelease :sentry-android-integration-tests:sentry-uitest-android-macrobenchmark:assembleBenchmark | ||
|
|
||
| - name: Run Macrobenchmark in SauceLab | ||
| uses: saucelabs/saucectl-run-action@283660aa934c02723c497efa151d582a3acc5801 # pin@v3 | ||
| if: env.SAUCE_USERNAME != null | ||
| env: | ||
| GITHUB_TOKEN: ${{ github.token }} | ||
| with: | ||
| sauce-username: ${{ secrets.SAUCE_USERNAME }} | ||
| sauce-access-key: ${{ secrets.SAUCE_ACCESS_KEY }} | ||
| config-file: .sauce/sentry-uitest-android-macrobenchmark.yml | ||
|
|
||
| # Runs even when the suite fails: a failed benchmark still logs whatever it managed to | ||
| # measure, and the parser's own error explains what was missing. | ||
| - name: Recover benchmark results from the device log | ||
| if: always() && env.SAUCE_USERNAME != null | ||
| run: | | ||
| # Without pipefail the step passes on `tee`'s exit code, so a failed recovery | ||
| # would report success while silently producing no results. | ||
| set -o pipefail | ||
| python3 scripts/parse-macrobenchmark-log.py ./artifacts \ | ||
| --json-out ./artifacts/benchmarkData.json | tee -a "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| - name: Upload Sauce artifacts | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| if: always() | ||
| with: | ||
| name: macrobenchmark-results | ||
| path: ./artifacts/ | ||
| if-no-files-found: warn | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| apiVersion: v1alpha | ||
| kind: espresso | ||
| sauce: | ||
| region: us-west-1 | ||
| concurrency: 1 | ||
| metadata: | ||
| build: sentry-uitest-android-macrobenchmark-$GITHUB_REF-$GITHUB_SHA | ||
| tags: | ||
| - benchmarks | ||
| - android | ||
| - macrobenchmark | ||
|
|
||
| defaults: | ||
| timeout: 40m | ||
|
|
||
| espresso: | ||
| app: ./sentry-samples/sentry-samples-android/build/outputs/apk/release/sentry-samples-android-release.apk | ||
| testApp: ./sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/build/outputs/apk/benchmark/sentry-uitest-android-macrobenchmark-benchmark.apk | ||
|
|
||
| suites: | ||
|
|
||
| - name: "Macrobenchmark startup (api 35)" | ||
| # No test orchestrator and no clearPackageData: Macrobenchmark manages its own process | ||
| # restarts and AOT compilation, and StartupMode.COLD intentionally keeps app data and | ||
| # permissions (it force-stops rather than `pm clear`). | ||
| devices: | ||
| - id: Google_Pixel_9_Pro_XL_15_real_sjc1 # Google Pixel 9 Pro XL - api 35 (15) - high end | ||
|
|
||
| artifacts: | ||
| download: | ||
| when: always | ||
| match: | ||
| - junit.xml | ||
| - "*.log" | ||
| directory: ./artifacts/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| #!/usr/bin/env python3 | ||
| """Recover Macrobenchmark results from a Sauce Labs device log. | ||
|
|
||
| Sauce Labs cannot pull arbitrary files off a real device, so | ||
| SentryStartupBenchmark echoes its `<pkg>-benchmarkData.json` into logcat as | ||
| numbered chunks. This reassembles those chunks and prints a Markdown summary. | ||
|
|
||
| Usage: | ||
| parse-macrobenchmark-log.py <artifacts-dir> [--json-out benchmarkData.json] | ||
| """ | ||
|
|
||
| import argparse | ||
| import json | ||
| import re | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| # Must match SentryStartupBenchmark.LOG_TAG and its "[index/total]" chunk prefix. | ||
| CHUNK_RE = re.compile(r"SentryBenchmarkData\s*:\s*\[(\d+)/(\d+)\](.*)$") | ||
|
|
||
|
|
||
| def log_messages(log_file): | ||
| """Yields the message text of every log entry. | ||
|
|
||
| Sauce hands back device.log as JSON lines -- {"tag", "message", "level", ...} -- which | ||
| means the payload arrives with its quotes escaped, so it has to be decoded rather than | ||
| regexed out of the raw line. Plain-text lines are passed through unchanged so the same | ||
| parser works on `adb logcat` output from a local run. | ||
| """ | ||
| # Sauce device logs occasionally carry undecodable bytes; don't die on them. | ||
| for line in log_file.read_text(errors="replace").splitlines(): | ||
| line = line.strip() | ||
| if line.startswith("{"): | ||
| try: | ||
| yield json.loads(line).get("message", "") | ||
| continue | ||
| except json.JSONDecodeError: | ||
| pass | ||
| yield line | ||
|
|
||
|
|
||
| def collect_chunks(log_file): | ||
| """Returns one log's chunk texts keyed by index, plus the expected total.""" | ||
| chunks, total = {}, None | ||
| for message in log_messages(log_file): | ||
| match = CHUNK_RE.search(message) | ||
| if match: | ||
| index, total = int(match.group(1)), int(match.group(2)) | ||
| chunks[index] = match.group(3) | ||
| return chunks, total | ||
|
|
||
|
|
||
| def reassemble(chunks, total): | ||
| missing = [i for i in range(1, total + 1) if i not in chunks] | ||
| if missing: | ||
| sys.exit(f"Incomplete benchmark data: missing chunk(s) {missing} of {total}") | ||
| return "".join(chunks[i] for i in range(1, total + 1)) | ||
|
|
||
|
|
||
| def format_summary(data): | ||
| context = data["context"] | ||
| build = context["build"] | ||
| lines = [ | ||
| "## Macrobenchmark results", | ||
| "", | ||
| f"**Device:** {build['brand']} {build['model']} " | ||
| f"(api {build['version']['sdk']}, {context['cpuCoreCount']} cores) · " | ||
| f"**compilation:** {context['compilationMode']} · " | ||
| f"**CPU clocks locked:** {context['cpuLocked']}", | ||
| "", | ||
| ] | ||
|
|
||
| if not context["cpuLocked"]: | ||
| lines += [ | ||
| "> CPU clocks are unlocked on this device, so run-to-run spread is wide. " | ||
| "Treat these numbers as a trend, not a regression gate.", | ||
| "", | ||
| ] | ||
|
|
||
| table = [ | ||
| "| Benchmark | Metric | min | median | max | CoV | iterations |", | ||
| "|---|---|--:|--:|--:|--:|--:|", | ||
| ] | ||
| details = [] | ||
| for benchmark in data["benchmarks"]: | ||
| name = f"{benchmark['className'].rsplit('.', 1)[-1]}.{benchmark['name']}" | ||
| for metric, result in sorted(benchmark["metrics"].items()): | ||
| table.append( | ||
| f"| `{name}` | {metric} " | ||
| f"| {result['minimum']:.1f} | {result['median']:.1f} | {result['maximum']:.1f} " | ||
| f"| {result['coefficientOfVariation'] * 100:.1f}% | {len(result['runs'])} |" | ||
| ) | ||
| runs = ", ".join(f"{run:.1f}" for run in result["runs"]) | ||
| details += [ | ||
| "", | ||
| f"<details><summary>{metric} per iteration</summary>", | ||
| "", | ||
| runs, | ||
| "", | ||
| "</details>", | ||
| ] | ||
|
|
||
| return "\n".join(lines + table + details) | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("artifacts_dir", type=Path, help="directory of downloaded Sauce artifacts") | ||
| parser.add_argument("--json-out", type=Path, help="where to write the recovered benchmarkData.json") | ||
| args = parser.parse_args() | ||
|
|
||
| log_files = sorted(args.artifacts_dir.rglob("*.log")) | ||
| if not log_files: | ||
| sys.exit(f"No *.log files under {args.artifacts_dir}") | ||
|
|
||
| # Keyed by file so chunks from two devices can never be merged into one bogus document. | ||
| per_log = {log: collect_chunks(log) for log in log_files} | ||
| with_chunks = {log: result for log, (result, total) in per_log.items() if total} | ||
| if not with_chunks: | ||
| sys.exit( | ||
| "No SentryBenchmarkData chunks in the device log. The benchmark most likely " | ||
| "failed before reporting โ check junit.xml and the log for Macrobenchmark errors." | ||
| ) | ||
| if len(with_chunks) > 1: | ||
| sys.exit( | ||
| "Chunks from more than one run: " | ||
| + ", ".join(str(log) for log in with_chunks) | ||
| + ". This parser reports a single device." | ||
| ) | ||
| log_file = next(iter(with_chunks)) | ||
| chunks, total = per_log[log_file] | ||
|
|
||
| data = json.loads(reassemble(chunks, total)) | ||
|
|
||
| if args.json_out: | ||
| args.json_out.write_text(json.dumps(data, indent=2)) | ||
|
|
||
| print(format_summary(data)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.