Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PojavLauncherTeam
GitHub Repository: PojavLauncherTeam/angle
Path: blob/main_old/src/tests/run_perf_tests.py
1693 views
1
#! /usr/bin/env vpython
2
#
3
# Copyright 2021 The ANGLE Project Authors. All rights reserved.
4
# Use of this source code is governed by a BSD-style license that can be
5
# found in the LICENSE file.
6
#
7
# run_perf_test.py:
8
# Runs ANGLE perf tests using some statistical averaging.
9
10
import argparse
11
import fnmatch
12
import json
13
import logging
14
import time
15
import os
16
import re
17
import sys
18
19
# Add //src/testing into sys.path for importing xvfb and test_env, and
20
# //src/testing/scripts for importing common.
21
d = os.path.dirname
22
THIS_DIR = d(os.path.abspath(__file__))
23
ANGLE_DIR = d(d(THIS_DIR))
24
sys.path.append(os.path.join(ANGLE_DIR, 'testing'))
25
sys.path.append(os.path.join(ANGLE_DIR, 'testing', 'scripts'))
26
27
import common
28
import test_env
29
import xvfb
30
31
sys.path.append(os.path.join(ANGLE_DIR, 'third_party', 'catapult', 'tracing'))
32
from tracing.value import histogram
33
from tracing.value import histogram_set
34
from tracing.value import merge_histograms
35
36
DEFAULT_TEST_SUITE = 'angle_perftests'
37
DEFAULT_LOG = 'info'
38
DEFAULT_SAMPLES = 5
39
DEFAULT_TRIALS = 3
40
DEFAULT_MAX_ERRORS = 3
41
DEFAULT_WARMUP_LOOPS = 3
42
DEFAULT_CALIBRATION_TIME = 3
43
44
# Filters out stuff like: " I 72.572s run_tests_on_device(96071FFAZ00096) "
45
ANDROID_LOGGING_PREFIX = r'I +\d+.\d+s \w+\(\w+\) '
46
47
# Test expectations
48
FAIL = 'FAIL'
49
PASS = 'PASS'
50
SKIP = 'SKIP'
51
52
53
def is_windows():
54
return sys.platform == 'cygwin' or sys.platform.startswith('win')
55
56
57
def get_binary_name(binary):
58
if is_windows():
59
return '.\\%s.exe' % binary
60
else:
61
return './%s' % binary
62
63
64
def _run_and_get_output(args, cmd, env):
65
lines = []
66
with common.temporary_file() as tempfile_path:
67
if args.xvfb:
68
ret = xvfb.run_executable(cmd, env, stdoutfile=tempfile_path)
69
else:
70
ret = test_env.run_command_with_output(cmd, env=env, stdoutfile=tempfile_path)
71
if ret:
72
logging.error('Error running test suite.')
73
return None
74
with open(tempfile_path) as f:
75
for line in f:
76
lines.append(line.strip())
77
return lines
78
79
80
def _filter_tests(tests, pattern):
81
return [test for test in tests if fnmatch.fnmatch(test, pattern)]
82
83
84
def _shard_tests(tests, shard_count, shard_index):
85
return [tests[index] for index in range(shard_index, len(tests), shard_count)]
86
87
88
def _get_results_from_output(output, result):
89
output = '\n'.join(output)
90
m = re.search(r'Running (\d+) tests', output)
91
if m and int(m.group(1)) > 1:
92
raise Exception('Found more than one test result in output')
93
94
# Results are reported in the format:
95
# name_backend.result: story= value units.
96
pattern = r'\.' + result + r':.*= ([0-9.]+)'
97
logging.debug('Searching for %s in output' % pattern)
98
m = re.findall(pattern, output)
99
if not m:
100
logging.warning('Did not find the result "%s" in the test output.' % result)
101
return None
102
103
return [float(value) for value in m]
104
105
106
def _get_tests_from_output(lines):
107
seen_start_of_tests = False
108
tests = []
109
android_prefix = re.compile(ANDROID_LOGGING_PREFIX)
110
logging.debug('Read %d lines from test output.' % len(lines))
111
for line in lines:
112
line = android_prefix.sub('', line.strip())
113
if line == 'Tests list:':
114
seen_start_of_tests = True
115
elif line == 'End tests list.':
116
break
117
elif seen_start_of_tests:
118
tests.append(line)
119
if not seen_start_of_tests:
120
raise Exception('Did not find test list in test output!')
121
logging.debug('Found %d tests from test output.' % len(tests))
122
return tests
123
124
125
def _truncated_list(data, n):
126
"""Compute a truncated list, n is truncation size"""
127
if len(data) < n * 2:
128
raise ValueError('list not large enough to truncate')
129
return sorted(data)[n:-n]
130
131
132
def _mean(data):
133
"""Return the sample arithmetic mean of data."""
134
n = len(data)
135
if n < 1:
136
raise ValueError('mean requires at least one data point')
137
return float(sum(data)) / float(n) # in Python 2 use sum(data)/float(n)
138
139
140
def _sum_of_square_deviations(data, c):
141
"""Return sum of square deviations of sequence data."""
142
ss = sum((float(x) - c)**2 for x in data)
143
return ss
144
145
146
def _coefficient_of_variation(data):
147
"""Calculates the population coefficient of variation."""
148
n = len(data)
149
if n < 2:
150
raise ValueError('variance requires at least two data points')
151
c = _mean(data)
152
ss = _sum_of_square_deviations(data, c)
153
pvar = ss / n # the population variance
154
stddev = (pvar**0.5) # population standard deviation
155
return stddev / c
156
157
158
def _save_extra_output_files(args, test_results, histograms):
159
isolated_out_dir = os.path.dirname(args.isolated_script_test_output)
160
if not os.path.isdir(isolated_out_dir):
161
return
162
benchmark_path = os.path.join(isolated_out_dir, args.test_suite)
163
if not os.path.isdir(benchmark_path):
164
os.makedirs(benchmark_path)
165
test_output_path = os.path.join(benchmark_path, 'test_results.json')
166
logging.info('Saving test results to %s.' % test_output_path)
167
with open(test_output_path, 'w') as out_file:
168
out_file.write(json.dumps(test_results, indent=2))
169
perf_output_path = os.path.join(benchmark_path, 'perf_results.json')
170
logging.info('Saving perf histograms to %s.' % perf_output_path)
171
with open(perf_output_path, 'w') as out_file:
172
out_file.write(json.dumps(histograms.AsDicts(), indent=2))
173
174
175
def main():
176
parser = argparse.ArgumentParser()
177
parser.add_argument('--isolated-script-test-output', type=str)
178
parser.add_argument('--isolated-script-test-perf-output', type=str)
179
parser.add_argument(
180
'-f', '--filter', '--isolated-script-test-filter', type=str, help='Test filter.')
181
parser.add_argument('--test-suite', help='Test suite to run.', default=DEFAULT_TEST_SUITE)
182
parser.add_argument('--xvfb', help='Use xvfb.', action='store_true')
183
parser.add_argument(
184
'--shard-count',
185
help='Number of shards for test splitting. Default is 1.',
186
type=int,
187
default=1)
188
parser.add_argument(
189
'--shard-index',
190
help='Index of the current shard for test splitting. Default is 0.',
191
type=int,
192
default=0)
193
parser.add_argument(
194
'-l', '--log', help='Log output level. Default is %s.' % DEFAULT_LOG, default=DEFAULT_LOG)
195
parser.add_argument(
196
'-s',
197
'--samples-per-test',
198
help='Number of samples to run per test. Default is %d.' % DEFAULT_SAMPLES,
199
type=int,
200
default=DEFAULT_SAMPLES)
201
parser.add_argument(
202
'-t',
203
'--trials-per-sample',
204
help='Number of trials to run per sample. Default is %d.' % DEFAULT_TRIALS,
205
type=int,
206
default=DEFAULT_TRIALS)
207
parser.add_argument(
208
'--steps-per-trial', help='Fixed number of steps to run per trial.', type=int)
209
parser.add_argument(
210
'--max-errors',
211
help='After this many errors, abort the run. Default is %d.' % DEFAULT_MAX_ERRORS,
212
type=int,
213
default=DEFAULT_MAX_ERRORS)
214
parser.add_argument(
215
'--smoke-test-mode', help='Do a quick run to validate correctness.', action='store_true')
216
parser.add_argument(
217
'--warmup-loops',
218
help='Number of warmup loops to run in the perf test. Default is %d.' %
219
DEFAULT_WARMUP_LOOPS,
220
type=int,
221
default=DEFAULT_WARMUP_LOOPS)
222
parser.add_argument(
223
'--calibration-time',
224
help='Amount of time to spend each loop in calibration and warmup. Default is %d seconds.'
225
% DEFAULT_CALIBRATION_TIME,
226
type=int,
227
default=DEFAULT_CALIBRATION_TIME)
228
229
args, extra_flags = parser.parse_known_args()
230
logging.basicConfig(level=args.log.upper(), stream=sys.stdout)
231
232
start_time = time.time()
233
234
# Use fast execution for smoke test mode.
235
if args.smoke_test_mode:
236
args.steps_per_trial = 1
237
args.trials_per_sample = 1
238
args.samples_per_test = 1
239
240
env = os.environ.copy()
241
242
# Get sharding args
243
if 'GTEST_TOTAL_SHARDS' in env and int(env['GTEST_TOTAL_SHARDS']) != 1:
244
if 'GTEST_SHARD_INDEX' not in env:
245
logging.error('Sharding params must be specified together.')
246
sys.exit(1)
247
args.shard_count = int(env.pop('GTEST_TOTAL_SHARDS'))
248
args.shard_index = int(env.pop('GTEST_SHARD_INDEX'))
249
250
# Get test list
251
cmd = [get_binary_name(args.test_suite), '--list-tests', '--verbose']
252
lines = _run_and_get_output(args, cmd, env)
253
if not lines:
254
raise Exception('Could not find test list from test output.')
255
tests = _get_tests_from_output(lines)
256
257
if args.filter:
258
tests = _filter_tests(tests, args.filter)
259
260
# Get tests for this shard (if using sharding args)
261
tests = _shard_tests(tests, args.shard_count, args.shard_index)
262
263
# Run tests
264
results = {
265
'tests': {},
266
'interrupted': False,
267
'seconds_since_epoch': time.time(),
268
'path_delimiter': '.',
269
'version': 3,
270
'num_failures_by_type': {
271
FAIL: 0,
272
PASS: 0,
273
SKIP: 0,
274
},
275
}
276
277
test_results = {}
278
histograms = histogram_set.HistogramSet()
279
total_errors = 0
280
281
for test in tests:
282
cmd = [
283
get_binary_name(args.test_suite),
284
'--gtest_filter=%s' % test,
285
'--extract-test-list-from-filter',
286
'--enable-device-cache',
287
'--skip-clear-data',
288
'--use-existing-test-data',
289
'--verbose',
290
'--calibration-time',
291
str(args.calibration_time),
292
]
293
if args.steps_per_trial:
294
steps_per_trial = args.steps_per_trial
295
else:
296
cmd_calibrate = cmd + [
297
'--calibration',
298
'--warmup-loops',
299
str(args.warmup_loops),
300
]
301
calibrate_output = _run_and_get_output(args, cmd_calibrate, env)
302
if not calibrate_output:
303
logging.error('Failed to get calibration output')
304
test_results[test] = {'expected': PASS, 'actual': FAIL, 'is_unexpected': True}
305
results['num_failures_by_type'][FAIL] += 1
306
total_errors += 1
307
continue
308
steps_per_trial = _get_results_from_output(calibrate_output, 'steps_to_run')
309
if not steps_per_trial:
310
logging.warning('Skipping test %s' % test)
311
continue
312
assert (len(steps_per_trial) == 1)
313
steps_per_trial = int(steps_per_trial[0])
314
logging.info('Running %s %d times with %d trials and %d steps per trial.' %
315
(test, args.samples_per_test, args.trials_per_sample, steps_per_trial))
316
wall_times = []
317
test_histogram_set = histogram_set.HistogramSet()
318
for sample in range(args.samples_per_test):
319
if total_errors >= args.max_errors:
320
logging.error('Error count exceeded max errors (%d). Aborting.' % args.max_errors)
321
return 1
322
323
cmd_run = cmd + [
324
'--steps-per-trial',
325
str(steps_per_trial),
326
'--trials',
327
str(args.trials_per_sample),
328
]
329
if args.smoke_test_mode:
330
cmd_run += ['--no-warmup']
331
else:
332
cmd_run += ['--warmup-loops', str(args.warmup_loops)]
333
with common.temporary_file() as histogram_file_path:
334
cmd_run += ['--isolated-script-test-perf-output=%s' % histogram_file_path]
335
output = _run_and_get_output(args, cmd_run, env)
336
if output:
337
sample_wall_times = _get_results_from_output(output, 'wall_time')
338
if not sample_wall_times:
339
logging.warning('Test %s failed to produce a sample output' % test)
340
break
341
logging.info('Sample %d wall_time results: %s' %
342
(sample, str(sample_wall_times)))
343
wall_times += sample_wall_times
344
with open(histogram_file_path) as histogram_file:
345
sample_json = json.load(histogram_file)
346
sample_histogram = histogram_set.HistogramSet()
347
sample_histogram.ImportDicts(sample_json)
348
test_histogram_set.Merge(sample_histogram)
349
else:
350
logging.error('Failed to get sample for test %s' % test)
351
total_errors += 1
352
353
if not wall_times:
354
logging.warning('Skipping test %s. Assuming this is intentional.' % test)
355
test_results[test] = {'expected': SKIP, 'actual': SKIP}
356
results['num_failures_by_type'][SKIP] += 1
357
elif len(wall_times) == (args.samples_per_test * args.trials_per_sample):
358
if len(wall_times) > 7:
359
truncation_n = len(wall_times) >> 3
360
logging.info(
361
'Truncation: Removing the %d highest and lowest times from wall_times.' %
362
truncation_n)
363
wall_times = _truncated_list(wall_times, truncation_n)
364
365
if len(wall_times) > 1:
366
logging.info(
367
'Mean wall_time for %s is %.2f, with coefficient of variation %.2f%%' %
368
(test, _mean(wall_times), (_coefficient_of_variation(wall_times) * 100.0)))
369
test_results[test] = {'expected': PASS, 'actual': PASS}
370
results['num_failures_by_type'][PASS] += 1
371
372
# Merge the histogram set into one histogram
373
with common.temporary_file() as merge_histogram_path:
374
logging.info('Writing merged histograms to %s.' % merge_histogram_path)
375
with open(merge_histogram_path, 'w') as merge_histogram_file:
376
json.dump(test_histogram_set.AsDicts(), merge_histogram_file)
377
merge_histogram_file.close()
378
merged_dicts = merge_histograms.MergeHistograms(
379
merge_histogram_path, groupby=['name'])
380
merged_histogram = histogram_set.HistogramSet()
381
merged_histogram.ImportDicts(merged_dicts)
382
histograms.Merge(merged_histogram)
383
else:
384
logging.error('Test %s failed to record some samples' % test)
385
test_results[test] = {'expected': PASS, 'actual': FAIL, 'is_unexpected': True}
386
results['num_failures_by_type'][FAIL] += 1
387
388
if test_results:
389
results['tests'][args.test_suite] = test_results
390
391
if args.isolated_script_test_output:
392
with open(args.isolated_script_test_output, 'w') as out_file:
393
out_file.write(json.dumps(results, indent=2))
394
395
# Uses special output files to match the merge script.
396
_save_extra_output_files(args, results, histograms)
397
398
if args.isolated_script_test_perf_output:
399
with open(args.isolated_script_test_perf_output, 'w') as out_file:
400
out_file.write(json.dumps(histograms.AsDicts(), indent=2))
401
402
end_time = time.time()
403
logging.info('Elapsed time: %.2lf seconds.' % (end_time - start_time))
404
405
return 0
406
407
408
# This is not really a "script test" so does not need to manually add
409
# any additional compile targets.
410
def main_compile_targets(args):
411
json.dump([], args.output)
412
413
414
if __name__ == '__main__':
415
# Conform minimally to the protocol defined by ScriptTest.
416
if 'compile_targets' in sys.argv:
417
funcs = {
418
'run': None,
419
'compile_targets': main_compile_targets,
420
}
421
sys.exit(common.run_script(sys.argv[1:], funcs))
422
sys.exit(main())
423
424