Skip to content

Commit 523730a

Browse files
authored
Revamp test harness for macrobenchmark tests (#4071)
1 parent 65356d8 commit 523730a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+506
-393
lines changed

ci/fireci/fireciplugins/macrobenchmark.py

Lines changed: 86 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import re
2222
import shutil
2323
import sys
24+
import tempfile
2425
import uuid
2526

2627
import click
@@ -31,42 +32,55 @@
3132

3233
from fireci import ci_command
3334
from fireci import ci_utils
34-
from fireci.dir_utils import chdir
3535
from fireci import uploader
36+
from fireci.dir_utils import chdir
3637

3738
_logger = logging.getLogger('fireci.macrobenchmark')
3839

3940

41+
@click.option(
42+
'--build-only/--no-build-only',
43+
default=False,
44+
help='Whether to only build tracing test apps or to also run them on FTL afterwards'
45+
)
4046
@ci_command()
41-
def macrobenchmark():
47+
def macrobenchmark(build_only):
4248
"""Measures app startup times for Firebase SDKs."""
43-
asyncio.run(_launch_macrobenchmark_test())
49+
asyncio.run(_launch_macrobenchmark_test(build_only))
4450

4551

46-
async def _launch_macrobenchmark_test():
52+
async def _launch_macrobenchmark_test(build_only):
4753
_logger.info('Starting macrobenchmark test...')
4854

49-
artifact_versions, config, _, _ = await asyncio.gather(
50-
_parse_artifact_versions(),
51-
_parse_config_yaml(),
52-
_create_gradle_wrapper(),
53-
_copy_google_services(),
54-
)
55-
55+
artifact_versions = await _assemble_all_artifacts()
5656
_logger.info(f'Artifact versions: {artifact_versions}')
5757

58-
with chdir('health-metrics/macrobenchmark'):
59-
runners = [MacrobenchmarkTest(k, v, artifact_versions) for k, v in config.items()]
60-
results = await asyncio.gather(*[x.run() for x in runners], return_exceptions=True)
58+
test_dir = await _prepare_test_directory()
59+
_logger.info(f'Directory for test apps: {test_dir}')
60+
61+
config = await _process_config_yaml()
62+
_logger.info(f'Processed yaml configurations: {config}')
63+
64+
tests = [MacrobenchmarkTest(app, artifact_versions, os.getcwd(), test_dir) for app in config['test-apps']]
6165

62-
await _post_processing(results)
66+
_logger.info(f'Building {len(tests)} macrobenchmark test apps...')
67+
# TODO(yifany): investigate why it is much slower with asyncio.gather
68+
# - on corp workstations (9 min) than M1 macbook pro (3 min)
69+
# - with gradle 7.5.1 (9 min) than gradle 6.9.2 (5 min)
70+
# await asyncio.gather(*[x.build() for x in tests])
71+
for test in tests:
72+
await test.build()
73+
74+
if not build_only:
75+
_logger.info(f'Submitting {len(tests)} tests to Firebase Test Lab...')
76+
results = await asyncio.gather(*[x.test() for x in tests], return_exceptions=True)
77+
await _post_processing(results)
6378

6479
_logger.info('Macrobenchmark test finished.')
6580

6681

67-
async def _parse_artifact_versions():
68-
proc = await asyncio.subprocess.create_subprocess_exec('./gradlew', 'assembleAllForSmokeTests')
69-
await proc.wait()
82+
async def _assemble_all_artifacts():
83+
await (await asyncio.create_subprocess_exec('./gradlew', 'assembleAllForSmokeTests')).wait()
7084

7185
with open('build/m2repository/changed-artifacts.json') as json_file:
7286
artifacts = json.load(json_file)
@@ -78,35 +92,36 @@ def _artifact_key_version(artifact):
7892
return f'{group_id}:{artifact_id}', version
7993

8094

81-
async def _parse_config_yaml():
82-
with open('health-metrics/macrobenchmark/config.yaml') as yaml_file:
83-
return yaml.safe_load(yaml_file)
95+
async def _process_config_yaml():
96+
with open('health-metrics/benchmark/config.yaml') as yaml_file:
97+
config = yaml.safe_load(yaml_file)
98+
for app in config['test-apps']:
99+
app['plugins'] = app.get('plugins', [])
100+
app['traces'] = app.get('traces', [])
101+
app['plugins'].extend(config['common-plugins'])
102+
app['traces'].extend(config['common-traces'])
103+
return config
84104

85105

86-
async def _create_gradle_wrapper():
87-
with open('health-metrics/macrobenchmark/settings.gradle', 'w'):
88-
pass
106+
async def _prepare_test_directory():
107+
test_dir = tempfile.mkdtemp(prefix='benchmark-test-')
89108

90-
proc = await asyncio.subprocess.create_subprocess_exec(
91-
'./gradlew',
92-
'wrapper',
93-
'--gradle-version',
94-
'6.9',
95-
'--project-dir',
96-
'health-metrics/macrobenchmark'
97-
)
98-
await proc.wait()
109+
# Required for creating gradle wrapper, as the dir is not defined in the root settings.gradle
110+
open(os.path.join(test_dir, 'settings.gradle'), 'w').close()
99111

112+
command = ['./gradlew', 'wrapper', '--gradle-version', '7.5.1', '--project-dir', test_dir]
113+
await (await asyncio.create_subprocess_exec(*command)).wait()
100114

101-
async def _copy_google_services():
102-
if 'FIREBASE_CI' in os.environ:
103-
src = os.environ['FIREBASE_GOOGLE_SERVICES_PATH']
104-
dst = 'health-metrics/macrobenchmark/template/app/google-services.json'
105-
_logger.info(f'Running on CI. Copying "{src}" to "{dst}"...')
106-
shutil.copyfile(src, dst)
115+
return test_dir
107116

108117

109118
async def _post_processing(results):
119+
_logger.info(f'Macrobenchmark results: {results}')
120+
121+
if os.getenv('CI') is None:
122+
_logger.info('Running locally. Results upload skipped.')
123+
return
124+
110125
# Upload successful measurements to the metric service
111126
measurements = []
112127
for result in results:
@@ -130,51 +145,63 @@ class MacrobenchmarkTest:
130145
"""Builds the test based on configurations and runs the test on FTL."""
131146
def __init__(
132147
self,
133-
sdk_name,
134148
test_app_config,
135149
artifact_versions,
150+
repo_root_dir,
151+
test_dir,
136152
logger=_logger
137153
):
138-
self.sdk_name = sdk_name
139154
self.test_app_config = test_app_config
140155
self.artifact_versions = artifact_versions
141-
self.logger = MacrobenchmarkLoggerAdapter(logger, sdk_name)
142-
self.test_app_dir = os.path.join('test-apps', test_app_config['name'])
156+
self.repo_root_dir = repo_root_dir
157+
self.test_dir = test_dir
158+
self.logger = MacrobenchmarkLoggerAdapter(logger, test_app_config['sdk'])
159+
self.test_app_dir = os.path.join(test_dir, test_app_config['name'])
143160
self.test_results_bucket = 'fireescape-benchmark-results'
144161
self.test_results_dir = str(uuid.uuid4())
145162
self.gcs_client = storage.Client()
146163

147-
async def run(self):
148-
"""Starts the workflow of src creation, apks assembly, FTL testing and results upload."""
164+
async def build(self):
165+
"""Creates test app project and assembles app and test apks."""
149166
await self._create_benchmark_projects()
150167
await self._assemble_benchmark_apks()
168+
169+
async def test(self):
170+
"""Runs benchmark tests on FTL and fetches FTL results from GCS."""
151171
await self._execute_benchmark_tests()
152172
return await self._aggregate_benchmark_results()
153173

154174
async def _create_benchmark_projects(self):
155175
app_name = self.test_app_config['name']
156176
self.logger.info(f'Creating test app "{app_name}"...')
157177

158-
mustache_context = await self._prepare_mustache_context()
178+
self.logger.info(f'Copying project template files into "{self.test_app_dir}"...')
179+
template_dir = os.path.join(self.repo_root_dir, 'health-metrics/benchmark/template')
180+
shutil.copytree(template_dir, self.test_app_dir)
181+
182+
self.logger.info(f'Copying gradle wrapper binary into "{self.test_app_dir}"...')
183+
shutil.copy(os.path.join(self.test_dir, 'gradlew'), self.test_app_dir)
184+
shutil.copy(os.path.join(self.test_dir, 'gradlew.bat'), self.test_app_dir)
185+
shutil.copytree(os.path.join(self.test_dir, 'gradle'), os.path.join(self.test_app_dir, 'gradle'))
159186

160-
shutil.copytree('template', self.test_app_dir)
161187
with chdir(self.test_app_dir):
188+
mustache_context = await self._prepare_mustache_context()
162189
renderer = pystache.Renderer()
163190
mustaches = glob.glob('**/*.mustache', recursive=True)
164191
for mustache in mustaches:
192+
self.logger.info(f'Processing template file: {mustache}')
165193
result = renderer.render_path(mustache, mustache_context)
166-
original_name = mustache[:-9] # TODO(yifany): mustache.removesuffix('.mustache')
194+
original_name = mustache.removesuffix('.mustache')
167195
with open(original_name, 'w') as file:
168196
file.write(result)
169197

170198
async def _assemble_benchmark_apks(self):
171-
executable = './gradlew'
172-
args = ['assemble', 'assembleAndroidTest', '--project-dir', self.test_app_dir]
173-
await self._exec_subprocess(executable, args)
199+
with chdir(self.test_app_dir):
200+
await self._exec_subprocess('./gradlew', ['assemble'])
174201

175202
async def _execute_benchmark_tests(self):
176-
app_apk_path = glob.glob(f'{self.test_app_dir}/app/**/*.apk', recursive=True)[0]
177-
test_apk_path = glob.glob(f'{self.test_app_dir}/benchmark/**/*.apk', recursive=True)[0]
203+
app_apk_path = glob.glob(f'{self.test_app_dir}/**/app-benchmark.apk', recursive=True)[0]
204+
test_apk_path = glob.glob(f'{self.test_app_dir}/**/macrobenchmark-benchmark.apk', recursive=True)[0]
178205

179206
self.logger.info(f'App apk: {app_apk_path}')
180207
self.logger.info(f'Test apk: {test_apk_path}')
@@ -189,7 +216,7 @@ async def _execute_benchmark_tests(self):
189216
args += ['--type', 'instrumentation']
190217
args += ['--app', app_apk_path]
191218
args += ['--test', test_apk_path]
192-
args += ['--device', 'model=redfin,version=30,locale=en,orientation=portrait']
219+
args += ['--device', 'model=oriole,version=32,locale=en,orientation=portrait']
193220
args += ['--directories-to-pull', '/sdcard/Download']
194221
args += ['--results-bucket', f'gs://{self.test_results_bucket}']
195222
args += ['--results-dir', self.test_results_dir]
@@ -200,19 +227,13 @@ async def _execute_benchmark_tests(self):
200227
await self._exec_subprocess(executable, args)
201228

202229
async def _prepare_mustache_context(self):
203-
app_name = self.test_app_config['name']
204-
205230
mustache_context = {
206-
'plugins': [],
231+
'm2repository': os.path.join(self.repo_root_dir, 'build/m2repository'),
232+
'plugins': self.test_app_config.get('plugins', []),
233+
'traces': self.test_app_config.get('traces', []),
207234
'dependencies': [],
208235
}
209236

210-
if app_name != 'baseline':
211-
mustache_context['plugins'].append('com.google.gms.google-services')
212-
213-
if 'plugins' in self.test_app_config:
214-
mustache_context['plugins'].extend(self.test_app_config['plugins'])
215-
216237
if 'dependencies' in self.test_app_config:
217238
for dep in self.test_app_config['dependencies']:
218239
if '@' in dep:
@@ -234,9 +255,9 @@ async def _aggregate_benchmark_results(self):
234255
for benchmark in benchmarks:
235256
method = benchmark['name']
236257
clazz = benchmark['className'].split('.')[-1]
237-
runs = benchmark['metrics']['startupMs']['runs']
258+
runs = benchmark['metrics']['timeToInitialDisplayMs']['runs']
238259
results.append({
239-
'sdk': self.sdk_name,
260+
'sdk': self.test_app_config['sdk'],
240261
'device': device,
241262
'name': f'{clazz}.{method}',
242263
'min': min(runs),

health-metrics/README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,14 @@ Refer to [README.md](apk-size/README.md) in the subdirectory `apk-size` for more
1212

1313
## App startup time
1414

15-
**TODO(yifany)**: Add more details once the measurement tools and infrastructure is ready.
15+
Firebase runs during different
16+
[app lifecycle](https://d.android.com/guide/components/activities/process-lifecycle)
17+
phases, and contributes to the overall
18+
[app startup time](https://d.android.com/topic/performance/vitals/launch-time)
19+
in many ways.
20+
21+
We are currently using
22+
[benchmarking](https://d.android.com/topic/performance/benchmarking/benchmarking-overview)
23+
and [tracing](https://d.android.com/topic/performance/tracing) to measure its
24+
latency impact. Refer to [README.md](benchmark/README.md) in the subdirectory
25+
`benchmark` for more details.

health-metrics/benchmark/README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Benchmark
2+
3+
This directory contains the benchmark test apps used for measuring latency for
4+
initializing Firebase Android SDKs during app startup.
5+
6+
## Test app configurations
7+
8+
[config.yaml](config.yaml) contains a list of configuration blocks for
9+
building a macrobenchmark test app for each of the Firebase Android SDKs.
10+
If not all of them are required, comment out irrelevant ones for faster build
11+
and test time.
12+
13+
## Run benchmark tests
14+
15+
### Prerequisite
16+
17+
1. `fireci` CLI tool
18+
19+
Refer to its [readme](../../ci/fireci/README.md) for how to install it.
20+
21+
1. `google-services.json`
22+
23+
Download it from the Firebase project
24+
[`fireescape-integ-tests`](https://firebase.corp.google.com/u/0/project/fireescape-integ-tests)
25+
to the directory `./template/app`.
26+
27+
1. Authentication to Google Cloud
28+
29+
Authentication is required by Google Cloud SDK and Google Cloud Storage
30+
client library used in the benchmark tests.
31+
32+
One simple way is to configure it is to set an environment variable
33+
`GOOGLE_APPLICATION_CREDENTIALS` to a service account key file. However,
34+
please refer to the official Google Cloud
35+
[doc](https://cloud.google.com/docs/authentication) for full guidance on
36+
authentication.
37+
38+
### Run benchmark tests locally
39+
40+
1. Build all test apps by running below command in the root
41+
directory `firebase-android-sdk`:
42+
43+
```shell
44+
fireci macrobenchmark --build-only
45+
```
46+
47+
1. [Connect an Android device to the computer](https://d.android.com/studio/run/device)
48+
49+
1. Locate the temporary test apps directory from the log, for example:
50+
51+
- on linux: `/tmp/benchmark-test-*/`
52+
- on macos: `/var/folders/**/benchmark-test-*/`
53+
54+
1. Start the benchmark tests from CLI or Android Studio:
55+
56+
- CLI
57+
58+
Run below command in the above test app project directory
59+
60+
```
61+
./gradlew :macrobenchmark:connectedCheck
62+
```
63+
64+
- Android Studio
65+
66+
1. Import the project (e.g. `**/benchmark-test-*/firestore`) into Android Studio
67+
1. Start the benchmark test by clicking gutter icons in the file `BenchmarkTest.kt`
68+
69+
1. Inspect the benchmark test results:
70+
71+
- CLI
72+
73+
Result files are created in `<test-app-dir>/macrobenchmark/build/outputs/`:
74+
75+
- `*-benchmarkData.json` contains metric aggregates
76+
- `*.perfetto-trace` are the raw trace files
77+
78+
Additionally, upload `.perfetto-trace` files to
79+
[Perfetto Trace Viewer](https://ui.perfetto.dev/) to visualize all traces.
80+
81+
- Android Studio
82+
83+
Test results are displayed directly in the "Run" tool window, including
84+
85+
- macrobenchmark built-in metrics
86+
- duration of custom traces
87+
- links to trace files that can be visualized within the IDE
88+
89+
Alternatively, same set of result files are produced at the same output
90+
location as invoking tests from CLI, which can be used for inspection.
91+
92+
### Run benchmark tests on Firebase Test Lab
93+
94+
Build and run all tests on FTL by running below command in the root
95+
directory `firebase-android-sdk`:
96+
97+
```
98+
fireci macrobenchmark
99+
```
100+
101+
Alternatively, it is possible to build all test apps via steps described in
102+
[Running benchmark tests locally](#running-benchmark-tests-locally)
103+
and manually
104+
[run tests on FTL with `gcloud` CLI ](https://firebase.google.com/docs/test-lab/android/command-line#running_your_instrumentation_tests).
105+
106+
Aggregated benchmark results are displayed in the log. The log also
107+
contains links to FTL result pages and result files on Google Cloud Storage.
108+
109+
## Toolchains
110+
111+
- Gradle 7.5.1
112+
- Android Gradle Plugin 7.2.2

0 commit comments

Comments
 (0)