Skip to content

Commit af2772c

Browse files
committed
Add test apps template and fireci commands to measure sdk startup times.
1 parent 40df7c8 commit af2772c

40 files changed

+1113
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import contextlib
16+
import glob
17+
import json
18+
import logging
19+
import os
20+
import pystache
21+
import shutil
22+
import subprocess
23+
import yaml
24+
25+
from fireci import ci_command
26+
from fireci import gradle
27+
28+
_logger = logging.getLogger('fireci.startup_time')
29+
30+
31+
@ci_command()
32+
def startup_time():
33+
"""Measures app startup times for Firebase SDKs."""
34+
35+
gradle.run('assembleAllForSmokeTests')
36+
artifact_versions = _parse_artifacts()
37+
38+
os.chdir('macrobenchmark')
39+
40+
config = _parse_config_yaml()
41+
_populate_test_apps(config, artifact_versions)
42+
43+
gradle.run('assemble', 'assembleAndroidTest')
44+
45+
_run_all_benchmark_tests()
46+
47+
48+
def _parse_artifacts():
49+
with open('build/m2repository/changed-artifacts.json', 'r') as json_file:
50+
artifacts = json.load(json_file)
51+
return dict(_artifact_key_version(x) for x in artifacts['headGit'])
52+
53+
54+
def _artifact_key_version(artifact):
55+
group_id, artifact_id, version = artifact.split(':')
56+
return f'{group_id}:{artifact_id}', version
57+
58+
59+
def _parse_config_yaml():
60+
with open('config.yaml', 'r') as yaml_file:
61+
return yaml.safe_load(yaml_file)
62+
63+
64+
def _populate_test_apps(config, artifact_versions):
65+
for sdk, test_app_configs in config.items():
66+
_logger.info(f'Creating test apps for "{sdk}" ...')
67+
for cfg in test_app_configs:
68+
_create_test_app(cfg, artifact_versions)
69+
70+
71+
def _create_test_app(cfg, artifact_versions):
72+
_logger.info(f'Creating test app "{cfg["name"]}" with app-id "{cfg["application-id"]}" ...')
73+
74+
mustache_context = {
75+
'application-id': cfg['application-id'],
76+
'plugins': cfg['plugins'] if 'plugins' in cfg else False,
77+
'dependencies': [
78+
{
79+
'key': x,
80+
'version': artifact_versions[x]
81+
} for x in cfg['dependencies']
82+
] if 'dependencies' in cfg else False,
83+
}
84+
85+
output_path = f'test-apps/{cfg["name"]}'
86+
87+
shutil.copytree('template', output_path)
88+
89+
with chdir(output_path):
90+
renderer = pystache.Renderer()
91+
mustaches = glob.glob('**/*.mustache', recursive=True)
92+
for mustache in mustaches:
93+
result = renderer.render_path(mustache, mustache_context)
94+
# TODO(yifany): mustache.removesuffix('.mustache') with python 3.9
95+
original_name = mustache[:-9]
96+
with open(original_name, 'w') as file:
97+
file.write(result)
98+
99+
100+
def _run_all_benchmark_tests():
101+
test_apps = os.listdir('test-apps')
102+
benchmark_tests = [
103+
{
104+
'name': test_app,
105+
'app_apk_path': glob.glob(f'test-apps/{test_app}/app/**/*.apk', recursive=True)[0],
106+
'test_apk_path': glob.glob(f'test-apps/{test_app}/benchmark/**/*.apk', recursive=True)[0],
107+
} for test_app in test_apps
108+
]
109+
for test in benchmark_tests:
110+
_logger.info(f'Running macrobenchmark test for "{test["name"]}" ...')
111+
_run_on_ftl(test['app_apk_path'], test['test_apk_path'])
112+
113+
114+
def _run_on_ftl(app_apk_path, test_apk_path, results_bucket=None):
115+
ftl_environment_variables = [
116+
'clearPackageData=true',
117+
'additionalTestOutputDir=/sdcard/Download',
118+
'no-isolated-storage=true',
119+
]
120+
commands = ['gcloud', 'firebase', 'test', 'android', 'run']
121+
commands += ['--type', 'instrumentation']
122+
commands += ['--app', app_apk_path]
123+
commands += ['--test', test_apk_path]
124+
commands += ['--device', 'model=flame,version=30,locale=en,orientation=portrait']
125+
commands += ['--directories-to-pull', '/sdcard/Download']
126+
commands += ['--results-bucket', results_bucket] if results_bucket else []
127+
commands += ['--environment-variables', ','.join(ftl_environment_variables)]
128+
commands += ['--timeout', '30m']
129+
commands += ['--project', 'fireescape-c4819']
130+
return subprocess.run(commands, stdout=subprocess.PIPE, check=True)
131+
132+
133+
# TODO(yifany): same as the one in fierperf.py
134+
@contextlib.contextmanager
135+
def chdir(directory):
136+
"""Change working dir to `directory` and restore to original afterwards."""
137+
original_dir = os.getcwd()
138+
abs_directory = os.path.join(original_dir, directory)
139+
_logger.debug(f'Changing directory from {original_dir} to: {abs_directory} ...')
140+
os.chdir(directory)
141+
try:
142+
yield
143+
finally:
144+
_logger.debug(f'Restoring directory to: {original_dir} ...')
145+
os.chdir(original_dir)

ci/fireci/setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
install_requires=[
2828
'click==7.0',
2929
'PyGithub==1.43.8',
30+
'pystache==0.5.4',
3031
'requests==2.23.0',
32+
'PyYAML==5.4.1',
3133
],
3234
packages=find_packages(exclude=['tests']),
3335
entry_points={

macrobenchmark/.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
*.iml
2+
.gradle
3+
/local.properties
4+
/.idea/caches
5+
/.idea/libraries
6+
/.idea/modules.xml
7+
/.idea/workspace.xml
8+
/.idea/navEditor.xml
9+
/.idea/assetWizardSettings.xml
10+
.DS_Store
11+
/build
12+
/captures
13+
.externalNativeBuild
14+
.cxx
15+
local.properties
16+
test-apps

macrobenchmark/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Macrobenchmark
2+
3+
This directory contains infrastructure to measure startup times for Firebase SDKs.
4+
5+
Run to invoke the startup time tests:
6+
```shell
7+
fireci startup_time
8+
```
9+
10+
See more details on fireci [here](../ci/README.md).

macrobenchmark/build.gradle

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Top-level build file where you can add configuration options common to all sub-projects/modules.
2+
buildscript {
3+
ext.kotlin_version = "1.4.32"
4+
repositories {
5+
google()
6+
mavenCentral()
7+
}
8+
dependencies {
9+
classpath "com.android.tools.build:gradle:4.2.0-rc01"
10+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11+
12+
// NOTE: Do not place your application dependencies here; they belong
13+
// in the individual module build.gradle files
14+
15+
classpath 'com.google.gms:google-services:4.3.5'
16+
17+
// Add the Crashlytics Gradle plugin (be sure to add version
18+
// 2.0.0 or later if you built your app with Android Studio 4.1).
19+
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.2'
20+
21+
// Add the dependency for the Performance Monitoring plugin
22+
classpath 'com.google.firebase:perf-plugin:1.3.5' // Performance Monitoring plugin
23+
}
24+
}
25+
26+
allprojects {
27+
repositories {
28+
google()
29+
mavenCentral()
30+
31+
// snapshot repository for androidx - not necessary once alpha is public
32+
maven {
33+
url uri('https://androidx.dev/snapshots/builds/7205390/artifacts/repository')
34+
}
35+
36+
maven {
37+
url rootProject.file('../build/m2repository')
38+
}
39+
}
40+
}
41+
42+
task cleanTestApps(type: Delete) {
43+
delete 'test-apps'
44+
}
45+
46+
task clean(type: Delete) {
47+
delete rootProject.buildDir
48+
dependsOn cleanTestApps
49+
}

macrobenchmark/config.yaml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
baseline:
2+
- name: baseline
3+
application-id: com.google.firebase.benchmark.baseline
4+
5+
firebase-config:
6+
- name: config
7+
application-id: com.google.firebase.benchmark.config
8+
dependencies:
9+
- com.google.firebase:firebase-config-ktx
10+
11+
firebase-crashlytics:
12+
- name: crash
13+
application-id: com.google.firebase.benchmark.crash
14+
dependencies:
15+
- com.google.firebase:firebase-crashlytics-ktx
16+
plugins:
17+
- com.google.firebase.crashlytics
18+
19+
firebase-database:
20+
- name: database
21+
application-id: com.google.firebase.benchmark.database
22+
dependencies:
23+
- com.google.firebase:firebase-database-ktx
24+
25+
firebase-dynamic-links:
26+
- name: dynamiclinks
27+
application-id: com.google.firebase.benchmark.dynamiclinks
28+
dependencies:
29+
- com.google.firebase:firebase-dynamic-links-ktx
30+
31+
firebase-firestore:
32+
- name: firestore
33+
application-id: com.google.firebase.benchmark.firestore
34+
dependencies:
35+
- com.google.firebase:firebase-firestore-ktx
36+
37+
firebase-functions:
38+
- name: functions
39+
application-id: com.google.firebase.benchmark.functions
40+
dependencies:
41+
- com.google.firebase:firebase-functions-ktx
42+
43+
firebase-inappmessaging-display:
44+
- name: inappmessaging
45+
application-id: com.google.firebase.benchmark.inappmessaging
46+
dependencies:
47+
- com.google.firebase:firebase-inappmessaging-ktx
48+
- com.google.firebase:firebase-inappmessaging-display-ktx
49+
50+
firebase-messaging:
51+
- name: messaging
52+
application-id: com.google.firebase.benchmark.messaging
53+
dependencies:
54+
- com.google.firebase:firebase-messaging-ktx
55+
56+
firebase-perf:
57+
- name: perf
58+
application-id: com.google.firebase.benchmark.perf
59+
dependencies:
60+
- com.google.firebase:firebase-perf-ktx
61+
plugins:
62+
- com.google.firebase.firebase-perf
63+
64+
firebase-storage:
65+
- name: storage
66+
application-id: com.google.firebase.benchmark.storage
67+
dependencies:
68+
- com.google.firebase:firebase-storage-ktx
69+
70+
71+
72+
# TODO(yifany): google3 sdks, customizing FTL devices
73+
# auth
74+
# analytics
75+
# combined
76+
# - crashlytics + analytics
77+
# - crashlytics + fireperf
78+
# - auth + firestore
79+
# - ...

macrobenchmark/gradle.properties

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Project-wide Gradle settings.
2+
# IDE (e.g. Android Studio) users:
3+
# Gradle settings configured through the IDE *will override*
4+
# any settings specified in this file.
5+
# For more details on how to configure your build environment visit
6+
# http://www.gradle.org/docs/current/userguide/build_environment.html
7+
# Specifies the JVM arguments used for the daemon process.
8+
# The setting is particularly useful for tweaking memory settings.
9+
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10+
# When configured, Gradle will run in incubating parallel mode.
11+
# This option should only be used with decoupled projects. More details, visit
12+
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13+
org.gradle.parallel=true
14+
# AndroidX package structure to make it clearer which packages are bundled with the
15+
# Android operating system, and which are packaged with your app"s APK
16+
# https://developer.android.com/topic/libraries/support-library/androidx-rn
17+
android.useAndroidX=true
18+
# Kotlin code style for this project: "official" or "obsolete":
19+
kotlin.code.style=official
53.1 KB
Binary file not shown.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
3+
distributionPath=wrapper/dists
4+
zipStorePath=wrapper/dists
5+
zipStoreBase=GRADLE_USER_HOME

0 commit comments

Comments
 (0)