Skip to content

Commit 74cc48f

Browse files
committed
Make apks assembly and ftl execution run for different SDKs in parallel.'
1 parent ea0a896 commit 74cc48f

File tree

15 files changed

+304
-511
lines changed

15 files changed

+304
-511
lines changed

ci/fireci/fireci/dir_utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 logging
17+
import os
18+
19+
_logger = logging.getLogger('fireci.dir_utils')
20+
21+
22+
@contextlib.contextmanager
23+
def chdir(directory):
24+
"""Change working dir to `directory` and restore to original afterwards."""
25+
_logger.debug(f'Changing directory to: {directory} ...')
26+
original_dir = os.getcwd()
27+
os.chdir(directory)
28+
try:
29+
yield
30+
finally:
31+
_logger.debug(f'Restoring directory to: {original_dir} ...')
32+
os.chdir(original_dir)

ci/fireci/fireciplugins/fireperf.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from fireci import ci_command
2222
from fireci import gradle
23+
from fireci.dir_utils import chdir
2324

2425
_logger = logging.getLogger('fireci.fireperf')
2526

@@ -60,19 +61,6 @@ def fireperf_e2e_test(target_environment, plugin_repo_dir):
6061
gradle.run(*fireperf_e2e_test_gradle_command)
6162

6263

63-
@contextlib.contextmanager
64-
def chdir(directory):
65-
"""Change working dir to `directory` and restore to original afterwards."""
66-
_logger.debug(f'Changing directory to: {directory} ...')
67-
original_dir = os.getcwd()
68-
os.chdir(directory)
69-
try:
70-
yield
71-
finally:
72-
_logger.debug(f'Restoring directory to: {original_dir} ...')
73-
os.chdir(original_dir)
74-
75-
7664
def _find_fireperf_plugin_version():
7765
local_maven_repo_dir = pathlib.Path.home().joinpath('.m2', 'repository')
7866
artifacts_path = local_maven_repo_dir.joinpath('com', 'google', 'firebase', 'perf-plugin')
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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 asyncio
16+
import glob
17+
import json
18+
import logging
19+
import os
20+
import random
21+
import sys
22+
23+
import pystache
24+
import shutil
25+
import yaml
26+
27+
from fireci import ci_command
28+
from fireci.dir_utils import chdir
29+
30+
_logger = logging.getLogger('fireci.macrobenchmark')
31+
32+
33+
@ci_command()
34+
def macrobenchmark():
35+
"""Measures app startup times for Firebase SDKs."""
36+
asyncio.run(_launch_macrobenchmark_test())
37+
38+
39+
async def _launch_macrobenchmark_test():
40+
_logger.info('Starting macrobenchmark test...')
41+
42+
artifact_versions, config, _ = await asyncio.gather(
43+
_parse_artifact_versions(),
44+
_parse_config_yaml(),
45+
_create_gradle_wrapper()
46+
)
47+
48+
with chdir('macrobenchmark'):
49+
runners = [MacrobenchmarkTest(k, v, artifact_versions) for k, v in config.items()]
50+
await asyncio.gather(*[x.run() for x in runners])
51+
52+
_logger.info('Macrobenchmark test finished.')
53+
54+
55+
async def _parse_artifact_versions():
56+
proc = await asyncio.subprocess.create_subprocess_exec('./gradlew', 'assembleAllForSmokeTests')
57+
await proc.wait()
58+
59+
with open('build/m2repository/changed-artifacts.json', 'r') as json_file:
60+
artifacts = json.load(json_file)
61+
return dict(_artifact_key_version(x) for x in artifacts['headGit'])
62+
63+
64+
def _artifact_key_version(artifact):
65+
group_id, artifact_id, version = artifact.split(':')
66+
return f'{group_id}:{artifact_id}', version
67+
68+
69+
async def _parse_config_yaml():
70+
with open('macrobenchmark/config.yaml', 'r') as yaml_file:
71+
return yaml.safe_load(yaml_file)
72+
73+
74+
async def _create_gradle_wrapper():
75+
with open('macrobenchmark/settings.gradle', 'w'):
76+
pass
77+
78+
proc = await asyncio.subprocess.create_subprocess_exec(
79+
'./gradlew',
80+
'wrapper',
81+
'--gradle-version',
82+
'7.0',
83+
'--project-dir',
84+
'macrobenchmark'
85+
)
86+
await proc.wait()
87+
88+
89+
class MacrobenchmarkTest:
90+
"""Builds the test based on configurations and runs the test on FTL."""
91+
def __init__(
92+
self,
93+
sdk_name,
94+
test_app_config,
95+
artifact_versions,
96+
logger=_logger
97+
):
98+
self.sdk_name = sdk_name
99+
self.test_app_config = test_app_config
100+
self.artifact_versions = artifact_versions
101+
self.logger = MacrobenchmarkLoggerAdapter(logger, sdk_name)
102+
self.test_app_dir = os.path.join('test-apps', test_app_config['name'])
103+
104+
async def run(self):
105+
"""Starts the workflow of src creation, apks assembling, and FTL testing in order."""
106+
await self._create_test_src()
107+
await self._assemble_apks()
108+
await self._upload_apks_to_ftl()
109+
110+
async def _create_test_src(self):
111+
app_name = self.test_app_config['name']
112+
app_id = self.test_app_config['application-id']
113+
self.logger.info(f'Creating test app "{app_name}" with application-id "{app_id}"...')
114+
115+
mustache_context = {
116+
'application-id': app_id,
117+
'plugins': self.test_app_config['plugins'] if 'plugins' in self.test_app_config else [],
118+
'dependencies': [
119+
{
120+
'key': x,
121+
'version': self.artifact_versions[x]
122+
} for x in self.test_app_config['dependencies']
123+
] if 'dependencies' in self.test_app_config else [],
124+
}
125+
126+
if app_name != 'baseline':
127+
mustache_context['plugins'].append('com.google.gms.google-services')
128+
129+
shutil.copytree('template', self.test_app_dir)
130+
with chdir(self.test_app_dir):
131+
renderer = pystache.Renderer()
132+
mustaches = glob.glob('**/*.mustache', recursive=True)
133+
for mustache in mustaches:
134+
result = renderer.render_path(mustache, mustache_context)
135+
original_name = mustache[:-9] # TODO(yifany): mustache.removesuffix('.mustache')
136+
with open(original_name, 'w') as file:
137+
file.write(result)
138+
139+
async def _assemble_apks(self):
140+
executable = './gradlew'
141+
args = ['assemble', 'assembleAndroidTest', '--project-dir', self.test_app_dir]
142+
await self._exec_subprocess(executable, args)
143+
144+
async def _upload_apks_to_ftl(self):
145+
app_apk_path = glob.glob(f'{self.test_app_dir}/app/**/*.apk', recursive=True)[0]
146+
test_apk_path = glob.glob(f'{self.test_app_dir}/benchmark/**/*.apk', recursive=True)[0]
147+
148+
self.logger.info(f'App apk: {app_apk_path}')
149+
self.logger.info(f'Test apk: {test_apk_path}')
150+
151+
ftl_environment_variables = [
152+
'clearPackageData=true',
153+
'additionalTestOutputDir=/sdcard/Download',
154+
'no-isolated-storage=true',
155+
]
156+
executable = 'gcloud'
157+
args = ['firebase', 'test', 'android', 'run']
158+
args += ['--type', 'instrumentation']
159+
args += ['--app', app_apk_path]
160+
args += ['--test', test_apk_path]
161+
args += ['--device', 'model=flame,version=30,locale=en,orientation=portrait']
162+
args += ['--directories-to-pull', '/sdcard/Download']
163+
args += ['--environment-variables', ','.join(ftl_environment_variables)]
164+
args += ['--timeout', '30m']
165+
args += ['--project', 'fireescape-c4819']
166+
167+
await self._exec_subprocess(executable, args)
168+
169+
async def _exec_subprocess(self, executable, args):
170+
self.logger.info(f'Executing command: "{" ".join([executable, *args])}"...')
171+
172+
proc = await asyncio.subprocess.create_subprocess_exec(
173+
executable,
174+
*args,
175+
stdout=asyncio.subprocess.PIPE,
176+
stderr=asyncio.subprocess.PIPE
177+
)
178+
await asyncio.gather(
179+
self._stream_output(executable, proc.stdout),
180+
self._stream_output(executable, proc.stderr)
181+
)
182+
183+
async def _stream_output(self, executable, stream: asyncio.StreamReader):
184+
async for line in stream:
185+
self.logger.info(f'[{executable}] {line.decode("utf-8").strip()}')
186+
187+
188+
class MacrobenchmarkLoggerAdapter(logging.LoggerAdapter):
189+
"""Decorates log messages for a sdk to make them more distinguishable."""
190+
191+
reset_code = '\x1b[m'
192+
193+
@staticmethod
194+
def random_color_code():
195+
code = random.randint(16, 231) # https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
196+
return f'\x1b[38;5;{code}m'
197+
198+
def __init__(self, logger, sdk_name, color_code=None):
199+
super().__init__(logger, {})
200+
self.sdk_name = sdk_name
201+
self.color_code = self.random_color_code() if color_code is None else color_code
202+
203+
def process(self, msg, kwargs):
204+
colored = f'{self.color_code}[{self.sdk_name}]{self.reset_code} {msg}'
205+
uncolored = f'[{self.sdk_name}] {msg}'
206+
return colored if sys.stderr.isatty() else uncolored, kwargs

0 commit comments

Comments
 (0)