Skip to content

Commit e661702

Browse files
authored
Merge pull request #18924 from palimondo/fluctuation-of-the-pupil
BenchmarkDriver Strangler replaces Benchmark_Driver run
2 parents a01938a + 13c4993 commit e661702

File tree

5 files changed

+229
-129
lines changed

5 files changed

+229
-129
lines changed

benchmark/scripts/Benchmark_Driver

Lines changed: 111 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@
1212
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
1313
#
1414
# ===---------------------------------------------------------------------===//
15+
"""
16+
Benchmark_Driver is a tool for running and analysing Swift Benchmarking Suite.
17+
18+
Example:
19+
$ Benchmark_Driver run
20+
21+
Use `Benchmark_Driver -h` for help on available commands and options.
22+
23+
class `BenchmarkDriver` runs performance tests and impements the `run` COMMAND.
24+
class `BenchmarkDoctor` analyzes performance tests, implements `check` COMMAND.
25+
26+
"""
1527

1628
import argparse
1729
import glob
@@ -29,19 +41,26 @@ DRIVER_DIR = os.path.dirname(os.path.realpath(__file__))
2941

3042

3143
class BenchmarkDriver(object):
32-
"""Executes tests from Swift Benchmark Suite."""
44+
"""Executes tests from Swift Benchmark Suite.
45+
46+
It's a higher level wrapper for the Benchmark_X family of binaries
47+
(X = [O, Onone, Osize]).
48+
"""
3349

3450
def __init__(self, args, tests=None, _subprocess=None, parser=None):
35-
"""Initialized with command line arguments.
51+
"""Initialize with command line arguments.
3652
37-
Optional parameters for injecting dependencies; used for testing.
53+
Optional parameters are for injecting dependencies -- used for testing.
3854
"""
3955
self.args = args
4056
self._subprocess = _subprocess or subprocess
4157
self.all_tests = []
4258
self.tests = tests or self._get_tests()
4359
self.parser = parser or LogParser()
4460
self.results = {}
61+
# Set a constant hash seed. Some tests are currently sensitive to
62+
# fluctuations in the number of hash collisions.
63+
os.environ['SWIFT_DETERMINISTIC_HASHING'] = '1'
4564

4665
def _invoke(self, cmd):
4766
return self._subprocess.check_output(
@@ -54,6 +73,28 @@ class BenchmarkDriver(object):
5473
else 'O')
5574
return os.path.join(self.args.tests, "Benchmark_" + suffix)
5675

76+
def _git(self, cmd):
77+
"""Execute the Git command in the `swift-repo`."""
78+
return self._invoke(
79+
('git -C {0} '.format(self.args.swift_repo) + cmd).split()).strip()
80+
81+
@property
82+
def log_file(self):
83+
"""Full path to log file.
84+
85+
If `swift-repo` is set, log file is tied to Git branch and revision.
86+
"""
87+
if not self.args.output_dir:
88+
return None
89+
log_dir = self.args.output_dir
90+
harness_name = os.path.basename(self.test_harness)
91+
suffix = '-' + time.strftime('%Y%m%d%H%M%S', time.localtime())
92+
if self.args.swift_repo:
93+
log_dir = os.path.join(
94+
log_dir, self._git('rev-parse --abbrev-ref HEAD')) # branch
95+
suffix += '-' + self._git('rev-parse --short HEAD') # revision
96+
return os.path.join(log_dir, harness_name + suffix + '.log')
97+
5798
@property
5899
def _cmd_list_benchmarks(self):
59100
# Use tab delimiter for easier parsing to override the default comma.
@@ -128,6 +169,65 @@ class BenchmarkDriver(object):
128169
[self.run(test, measure_memory=True)
129170
for _ in range(self.args.iterations)])
130171

172+
def log_results(self, output, log_file=None):
173+
"""Log output to `log_file`.
174+
175+
Creates `args.output_dir` if it doesn't exist yet.
176+
"""
177+
log_file = log_file or self.log_file
178+
dir = os.path.dirname(log_file)
179+
if not os.path.exists(dir):
180+
os.makedirs(dir)
181+
print('Logging results to: %s' % log_file)
182+
with open(log_file, 'w') as f:
183+
f.write(output)
184+
185+
RESULT = '{:>3} {:<25} {:>7} {:>7} {:>7} {:>8} {:>6} {:>10} {:>10}'
186+
187+
def run_and_log(self, csv_console=True):
188+
"""Run benchmarks and continuously log results to the console.
189+
190+
There are two console log formats: CSV and justified columns. Both are
191+
compatible with `LogParser`. Depending on the `csv_console` parameter,
192+
the CSV log format is either printed to console or returned as a string
193+
from this method. When `csv_console` is False, the console output
194+
format is justified columns.
195+
"""
196+
197+
format = (
198+
(lambda values: ','.join(values)) if csv_console else
199+
(lambda values: self.RESULT.format(*values))) # justified columns
200+
201+
def console_log(values):
202+
print(format(values))
203+
204+
console_log(['#', 'TEST', 'SAMPLES', 'MIN(μs)', 'MAX(μs)', # header
205+
'MEAN(μs)', 'SD(μs)', 'MEDIAN(μs)', 'MAX_RSS(B)'])
206+
207+
def result_values(r):
208+
return map(str, [r.test_num, r.name, r.num_samples, r.min, r.max,
209+
int(r.mean), int(r.sd), r.median, r.max_rss])
210+
211+
results = []
212+
for test in self.tests:
213+
result = result_values(self.run_independent_samples(test))
214+
console_log(result)
215+
results.append(result)
216+
217+
print(
218+
'\nTotal performance tests executed: {0}'.format(len(self.tests)))
219+
return (None if csv_console else
220+
('\n'.join([','.join(r) for r in results]) + '\n')) # csv_log
221+
222+
@staticmethod
223+
def run_benchmarks(args):
224+
"""Run benchmarks and log results."""
225+
driver = BenchmarkDriver(args)
226+
csv_log = driver.run_and_log(csv_console=(args.output_dir is None))
227+
if csv_log:
228+
driver.log_results(csv_log)
229+
return 0
230+
131231

132232
class LoggingReportFormatter(logging.Formatter):
133233
"""Format logs as plain text or with colors on the terminal.
@@ -356,118 +456,21 @@ class BenchmarkDoctor(object):
356456

357457
@staticmethod
358458
def run_check(args):
459+
"""Validate benchmarks conform to health rules, report violations."""
359460
doctor = BenchmarkDoctor(args)
360461
doctor.check()
361462
# TODO non-zero error code when errors are logged
362463
# See https://stackoverflow.com/a/31142078/41307
363464
return 0
364465

365466

366-
def get_current_git_branch(git_repo_path):
367-
"""Return the selected branch for the repo `git_repo_path`"""
368-
return subprocess.check_output(
369-
['git', '-C', git_repo_path, 'rev-parse',
370-
'--abbrev-ref', 'HEAD'], stderr=subprocess.STDOUT).strip()
371-
372-
373-
def get_git_head_ID(git_repo_path):
374-
"""Return the short identifier for the HEAD commit of the repo
375-
`git_repo_path`"""
376-
return subprocess.check_output(
377-
['git', '-C', git_repo_path, 'rev-parse',
378-
'--short', 'HEAD'], stderr=subprocess.STDOUT).strip()
379-
380-
381-
def log_results(log_directory, driver, formatted_output, swift_repo=None):
382-
"""Log `formatted_output` to a branch specific directory in
383-
`log_directory`
384-
"""
385-
try:
386-
branch = get_current_git_branch(swift_repo)
387-
except (OSError, subprocess.CalledProcessError):
388-
branch = None
389-
try:
390-
head_ID = '-' + get_git_head_ID(swift_repo)
391-
except (OSError, subprocess.CalledProcessError):
392-
head_ID = ''
393-
timestamp = time.strftime("%Y%m%d%H%M%S", time.localtime())
394-
if branch:
395-
output_directory = os.path.join(log_directory, branch)
396-
else:
397-
output_directory = log_directory
398-
driver_name = os.path.basename(driver)
399-
try:
400-
os.makedirs(output_directory)
401-
except OSError:
402-
pass
403-
log_file = os.path.join(output_directory,
404-
driver_name + '-' + timestamp + head_ID + '.log')
405-
print('Logging results to: %s' % log_file)
406-
with open(log_file, 'w') as f:
407-
f.write(formatted_output)
408-
409-
410-
def run_benchmarks(driver,
411-
log_directory=None, swift_repo=None):
412-
"""Run perf tests individually and return results in a format that's
413-
compatible with `LogParser`.
414-
"""
415-
# Set a constant hash seed. Some tests are currently sensitive to
416-
# fluctuations in the number of hash collisions.
417-
#
418-
# FIXME: This should only be set in the environment of the child process
419-
# that runs the tests.
420-
os.environ["SWIFT_DETERMINISTIC_HASHING"] = "1"
421-
422-
output = []
423-
headings = ['#', 'TEST', 'SAMPLES', 'MIN(μs)', 'MAX(μs)', 'MEAN(μs)',
424-
'SD(μs)', 'MEDIAN(μs)', 'MAX_RSS(B)']
425-
line_format = '{:>3} {:<25} {:>7} {:>7} {:>7} {:>8} {:>6} {:>10} {:>10}'
426-
if log_directory:
427-
print(line_format.format(*headings))
428-
else:
429-
print(','.join(headings))
430-
for test in driver.tests:
431-
r = driver.run_independent_samples(test)
432-
test_output = map(str, [
433-
r.test_num, r.name, r.num_samples, r.min, r.max, int(r.mean),
434-
int(r.sd), r.median, r.max_rss])
435-
if log_directory:
436-
print(line_format.format(*test_output))
437-
else:
438-
print(','.join(test_output))
439-
output.append(test_output)
440-
if not output:
441-
return
442-
formatted_output = '\n'.join([','.join(l) for l in output])
443-
totals = ['Totals', str(len(driver.tests))]
444-
totals_output = '\n\n' + ','.join(totals)
445-
if log_directory:
446-
print(line_format.format(*([''] + totals + ([''] * 6))))
447-
else:
448-
print(totals_output[1:])
449-
formatted_output += totals_output
450-
if log_directory:
451-
log_results(log_directory, driver.test_harness, formatted_output,
452-
swift_repo)
453-
return formatted_output
454-
455-
456-
def run(args):
457-
run_benchmarks(
458-
BenchmarkDriver(args),
459-
log_directory=args.output_dir,
460-
swift_repo=args.swift_repo)
461-
return 0
462-
463-
464467
def format_name(log_path):
465-
"""Return the filename and directory for a log file"""
468+
"""Return the filename and directory for a log file."""
466469
return '/'.join(log_path.split('/')[-2:])
467470

468471

469472
def compare_logs(compare_script, new_log, old_log, log_dir, opt):
470-
"""Return diff of log files at paths `new_log` and `old_log`"""
473+
"""Return diff of log files at paths `new_log` and `old_log`."""
471474
print('Comparing %s %s ...' % (format_name(old_log), format_name(new_log)))
472475
subprocess.call([compare_script, '--old-file', old_log,
473476
'--new-file', new_log, '--format', 'markdown',
@@ -477,10 +480,10 @@ def compare_logs(compare_script, new_log, old_log, log_dir, opt):
477480

478481
def compare(args):
479482
log_dir = args.log_dir
480-
swift_repo = args.swift_repo
481483
compare_script = args.compare_script
482484
baseline_branch = args.baseline_branch
483-
current_branch = get_current_git_branch(swift_repo)
485+
current_branch = \
486+
BenchmarkDriver(args, tests=[''])._git('rev-parse --abbrev-ref HEAD')
484487
current_branch_dir = os.path.join(log_dir, current_branch)
485488
baseline_branch_dir = os.path.join(log_dir, baseline_branch)
486489

@@ -557,6 +560,7 @@ def compare(args):
557560

558561

559562
def positive_int(value):
563+
"""Verify the value is a positive integer."""
560564
ivalue = int(value)
561565
if not (ivalue > 0):
562566
raise ValueError
@@ -608,7 +612,7 @@ def parse_args(args):
608612
run_parser.add_argument(
609613
'--swift-repo',
610614
help='absolute path to the Swift source repository')
611-
run_parser.set_defaults(func=run)
615+
run_parser.set_defaults(func=BenchmarkDriver.run_benchmarks)
612616

613617
check_parser = subparsers.add_parser(
614618
'check',
@@ -641,6 +645,7 @@ def parse_args(args):
641645

642646

643647
def main():
648+
"""Parse command line arguments and execute the specified COMMAND."""
644649
args = parse_args(sys.argv[1:])
645650
return args.func(args)
646651

benchmark/scripts/compare_perf_tests.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,20 @@
1313
#
1414
# ===---------------------------------------------------------------------===//
1515
"""
16-
This script is used for comparing performance test results.
16+
This script compares performance test logs and issues a formatted report.
17+
18+
Invoke `$ compare_perf_tests.py -h ` for complete list of options.
19+
20+
class `Sample` is single benchmark measurement.
21+
class `PerformanceTestSamples` is collection of `Sample`s and their statistics.
22+
class `PerformanceTestResult` is a summary of performance test execution.
23+
class `LogParser` converts log files into `PerformanceTestResult`s.
24+
class `ResultComparison` compares new and old `PerformanceTestResult`s.
25+
class `TestComparator` analyzes changes betweeen the old and new test results.
26+
class `ReportFormatter` creates the test comparison report in specified format.
1727
18-
It is structured into several classes that can be imported into other modules.
1928
"""
29+
2030
from __future__ import print_function
2131

2232
import argparse
@@ -48,7 +58,7 @@ class PerformanceTestSamples(object):
4858
"""
4959

5060
def __init__(self, name, samples=None):
51-
"""Initialized with benchmark name and optional list of Samples."""
61+
"""Initialize with benchmark name and optional list of Samples."""
5262
self.name = name # Name of the performance test
5363
self.samples = []
5464
self.outliers = []
@@ -201,7 +211,7 @@ class PerformanceTestResult(object):
201211
"""
202212

203213
def __init__(self, csv_row):
204-
"""Initialized from a row with 8 or 9 columns with benchmark summary.
214+
"""Initialize from a row with 8 or 9 columns with benchmark summary.
205215
206216
The row is an iterable, such as a row provided by the CSV parser.
207217
"""
@@ -298,7 +308,7 @@ def _reset(self):
298308

299309
# Parse lines like this
300310
# #,TEST,SAMPLES,MIN(μs),MAX(μs),MEAN(μs),SD(μs),MEDIAN(μs)
301-
results_re = re.compile(r'(\d+[, \t]*\w+[, \t]*' +
311+
results_re = re.compile(r'([ ]*\d+[, \t]*\w+[, \t]*' +
302312
r'[, \t]*'.join([r'[\d.]+'] * 6) +
303313
r'[, \t]*[\d.]*)') # optional MAX_RSS(B)
304314

@@ -409,7 +419,7 @@ class TestComparator(object):
409419
"""
410420

411421
def __init__(self, old_results, new_results, delta_threshold):
412-
"""Initialized with dictionaries of old and new benchmark results.
422+
"""Initialize with dictionaries of old and new benchmark results.
413423
414424
Dictionary keys are benchmark names, values are
415425
`PerformanceTestResult`s.

0 commit comments

Comments
 (0)