Skip to content

Commit 3fbeeba

Browse files
committed
[Stress Tester] Use new duration measurement capability of stress tester to analyse code completion durations
Use the new capability of the stress tester to measure the time it took to execute code completion requests, gather that information and report an analysis on how long all code completion requests that were executed during the stress tester run took. This should allow us to verify changes to code completion don’t introduce significant regressions and will hopefully give us a way to find situations in which code completion is slower than it should be
1 parent 0bd943d commit 3fbeeba

File tree

2 files changed

+148
-2
lines changed

2 files changed

+148
-2
lines changed

analyze_request_durations.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env python
2+
3+
from __future__ import print_function
4+
5+
import argparse
6+
import os
7+
import json
8+
9+
# This value was emirically determined on an iMac Pro with a fairly small sample size.
10+
# All measurements in seconds should be taken with extreme care
11+
INSTRUCTIONS_PER_SECOND = 6693088549.09358
12+
13+
# The length of the bars in the histograms
14+
HISTOGRAM_BAR_LENGTH = 40
15+
16+
17+
def parse_args():
18+
parser = argparse.ArgumentParser()
19+
parser.add_argument('request_durations_file')
20+
return parser.parse_args()
21+
22+
23+
def print_stats(entries):
24+
"""
25+
entries are dictionaries from the JSON file under the
26+
files.<file-name>.<request-name> path containing the following keys
27+
- average
28+
- logHistogram
29+
- requestsExecuted
30+
"""
31+
32+
# Create a log histogram for all entries
33+
aggregated_completion_log_histogram = {}
34+
# Adds totalInstruction for each entry
35+
total_instructions = 0
36+
# Adds requestsExecuted for each entry
37+
total_requests_executed = 0
38+
for entry in entries:
39+
total_instructions += int(entry['totalInstructions'])
40+
total_requests_executed += int(entry['requestsExecuted'])
41+
for (bucket, count) in entry['logHistogram'].iteritems():
42+
bucket = int(bucket)
43+
count = int(count)
44+
if bucket not in aggregated_completion_log_histogram:
45+
aggregated_completion_log_histogram[bucket] = 0
46+
aggregated_completion_log_histogram[bucket] += count
47+
48+
if total_requests_executed == 0:
49+
print('No requests executed')
50+
return
51+
52+
print('Requests: {}'.format(total_requests_executed))
53+
54+
average_instructions_per_request = total_instructions / total_requests_executed
55+
seconds_estimate = average_instructions_per_request / INSTRUCTIONS_PER_SECOND
56+
print('Average: {:.0e} instr (ca. {:.3f}s)'
57+
.format(average_instructions_per_request, seconds_estimate))
58+
59+
print('\nHistogram:')
60+
61+
min_bucket = min(aggregated_completion_log_histogram)
62+
max_bucket = max(aggregated_completion_log_histogram)
63+
max_count = max([x[1] for x in aggregated_completion_log_histogram.iteritems()])
64+
65+
# Sums up the percentages for each entry to make it easier to spot the
66+
# n-th percentile in the histogram
67+
running_percentage_sum = 0
68+
for bucket in range(min_bucket, max_bucket + 1):
69+
count = aggregated_completion_log_histogram.get(bucket, 0)
70+
executed_instructions = pow(2, bucket)
71+
estimated_seconds = executed_instructions / INSTRUCTIONS_PER_SECOND
72+
bar_fill = (count * HISTOGRAM_BAR_LENGTH / max_count)
73+
bar = '*' * bar_fill + ' ' * (HISTOGRAM_BAR_LENGTH - bar_fill)
74+
percentage = float(count) / total_requests_executed * 100
75+
running_percentage_sum += percentage
76+
print('< {:5.0e} instr (ca. {:6.3f}s): {} {:4.1f}% (< {:5.1f}%)'
77+
.format(executed_instructions, estimated_seconds, bar, percentage,
78+
running_percentage_sum))
79+
80+
81+
def print_stats_for_all_files(request_durations, request_name):
82+
print('\n---------- All {} requests ----------\n'.format(request_name))
83+
all_request_entries = []
84+
for (file_name, file_stats) in request_durations['files'].iteritems():
85+
all_request_entries.append(file_stats[request_name])
86+
print_stats(all_request_entries)
87+
88+
89+
def print_slowest_files(request_durations, request_name, num_files):
90+
print('\n---------- {} slowest files for {} requests ----------\n'
91+
.format(num_files, request_name))
92+
93+
file_and_average = []
94+
for (file_name, file_stats) in request_durations['files'].iteritems():
95+
instr_executed_in_file = int(file_stats[request_name]['totalInstructions'])
96+
requests_in_file = int(file_stats[request_name]['requestsExecuted'])
97+
if requests_in_file > 0:
98+
average = instr_executed_in_file / requests_in_file
99+
file_and_average.append((file_name, average))
100+
file_and_average = sorted(file_and_average, key=lambda x: x[1], reverse=True)
101+
102+
for (file_name, average_request_duration) in file_and_average[0:num_files]:
103+
print('-- {} --\n'.format(file_name))
104+
print_stats([request_durations['files'][file_name][request_name]])
105+
print('\n')
106+
107+
108+
def print_seconds_disclaimer():
109+
print('''IMPORTANT:
110+
All measurements printed by this script were done in number of number of
111+
instructions. Measurements in seconds are computed assuming that all machines
112+
execute the same number of instructions per second at all times, which is not
113+
correct. Thus all measurements in seconds should be taken with extreme care.
114+
As they scale linearly with the number of instructions, it is safe to compare
115+
values in seconds output by this script.
116+
''')
117+
118+
119+
def analyze_request_durations(file):
120+
if not os.path.exists(file):
121+
print('{} does not exist. No request durations to analyze.'.format(file))
122+
return
123+
print_seconds_disclaimer()
124+
print('\n---------- Raw durations for further analysis ----------\n')
125+
with open(file, 'r') as json_file:
126+
print(json_file.read())
127+
128+
with open(file, 'r') as json_file:
129+
request_durations = json.load(json_file, encoding='utf-8')
130+
print_stats_for_all_files(request_durations, 'CodeComplete')
131+
print_slowest_files(request_durations, 'CodeComplete', 10)
132+
133+
134+
def main():
135+
args = parse_args()
136+
analyze_request_durations(args.request_durations_file)
137+
138+
139+
if __name__ == '__main__':
140+
main()

run_sk_stress_test

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import subprocess
2222
import argparse
2323
import platform
2424
import codecs
25+
from analyze_request_durations import analyze_request_durations
2526

2627
import common
2728

@@ -230,9 +231,10 @@ class StressTesterRunner(object):
230231
# temporary file paths
231232
filtered_projects = os.path.join(script_dir, 'stress-tester-projects.json')
232233
results = os.path.join(script_dir, 'stress-tester-results.json')
234+
request_durations = os.path.join(script_dir, 'stress-tester-request-durations.json')
233235

234236
# remove temporary files if they already exist
235-
self._cleanup([filtered_projects, results])
237+
self._cleanup([filtered_projects, results, request_durations])
236238

237239
run_env = {
238240
'SK_STRESS_TEST': self.stress_tester,
@@ -243,7 +245,9 @@ class StressTesterRunner(object):
243245
'SK_XFAILS_PATH': self.xfails_path,
244246
'SK_STRESS_ACTIVE_CONFIG': self.swift_branch,
245247
'SK_STRESS_SUPPRESS_OUTPUT': 'true',
246-
'SK_STRESS_REWRITE_MODES': 'none concurrent insideout'}
248+
'SK_STRESS_REWRITE_MODES': 'none concurrent insideout',
249+
'SK_STRESS_REQUEST_DURATIONS_FILE': request_durations,
250+
}
247251
run_env.update(os.environ)
248252
run_cmd = ['./runner.py',
249253
'--projects', filtered_projects,
@@ -264,6 +268,8 @@ class StressTesterRunner(object):
264268
except common.ExecuteCommandFailure:
265269
self.compat_runner_failed = True
266270

271+
analyze_request_durations(request_durations)
272+
267273
success = self._process_output(results, self.xfails_path)
268274

269275
return success

0 commit comments

Comments
 (0)