-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathrun-benchmarks.py
204 lines (169 loc) · 5.94 KB
/
run-benchmarks.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#!/usr/bin/python
# This script runs all the benchmarks, and outputs:
# - Flamegraphs of each benchmark
# - A detailed csv of all the processed results
# File names will include your git branch as a prefix, making it easier to compare changes.
#
# Pass --output or -o to set an output dir, helpful for comparisons and organization (ex: -o bin/featureName-benchmarks)
# Pass --runs or -r to set how many time each benchmark is run (ex: -r 5)
# Pass --dev or -d to run tests in DEV and COMPAT_WARNINGS mode (ex: --dev)
BENCHMARK_FILES = "bin/run-*-benchmark.luau"
PROJECT_JSON = "tests.project.json"
OUTPUT_PATTERN = r"(.+) x ([\d\.]+) ([/\w]+) ±([\d\.]+)\% \((\d+) runs sampled\)"
import os
import sys
import re
import subprocess
import glob
import svg
os.system('color') # Colored output support
# Profiler output node
class Node(svg.Node):
def __init__(self):
svg.Node.__init__(self)
self.function = ""
self.source = ""
self.line = 0
self.ticks = 0
def text(self):
return self.function
def title(self):
if self.line > 0:
return "{}\n{}:{}".format(self.function, self.source, self.line)
else:
return self.function
def details(self, root):
return "Function: {} [{}:{}] ({:,} usec, {:.1%}); self: {:,} usec".format(self.function, self.source, self.line, self.width, self.width / root.width, self.ticks)
# Default parameters
parameters = {
"directory": "bin/benchmarks",
"runs": 3,
"dev": "",
}
# Parse command line arguments
argNum = 1
while argNum < len(sys.argv):
arg = sys.argv[argNum]
if arg == "-o" or arg == "--output":
value = sys.argv[argNum+1]
if value[0:1] != "-":
parameters['directory'] = value
else:
print(f"Error: Argument for {arg} is missing, please specify an output directory")
exit(1)
argNum += 2
elif arg == "-r" or arg == "--runs":
value = sys.argv[argNum+1]
if value[0:1] != "-":
parameters['runs'] = int(value)
else:
print(f"Error: Argument for {arg} is missing, please specify a number of runs")
exit(1)
argNum += 2
elif arg == "-d" or arg == "--dev":
parameters['dev'] = " --lua.globals=__DEV__=true --lua.globals=__COMPAT_WARNINGS__=true"
argNum += 1
elif arg[0:1] == "-":
print(f"Error: Unsupported flag {arg}")
exit(1)
else:
argNum += 1
# Gather the path information
branch = subprocess.getoutput("git symbolic-ref --short HEAD").strip().replace("/", "-")
prefix=f"{parameters['directory']}/{branch}"
logPath = f"{prefix}-benchmark.log"
# Create the results directory
if not os.path.exists(parameters['directory']):
os.makedirs(parameters['directory'])
logFile = open(logPath, mode="w", encoding="utf-8")
logFile.write("")
logFile.flush()
# Run each benchmark file
for test in glob.iglob(BENCHMARK_FILES):
testName = test[8:-14]
print(f"\033[94mRunning {testName}...\033[0m", flush=True) # Colored output since benchmarks can be noisy and this helps readability
logFile.write(f"TEST: {testName.replace('-', ' ')}\n")
logFile.flush()
for i in range(1, parameters['runs']+1):
print(f" Run {i}", flush=True)
runResults = subprocess.Popen(
f"robloxdev-cli run --load.model {PROJECT_JSON} --run {test} --headlessRenderer 1 --fastFlags.overrides \"EnableDelayedTaskMethods=true\" \"FIntScriptProfilerFrequency=1000000\" \"DebugScriptProfilerEnabled=true\" \"EnableLoadModule=true\" --fastFlags.allOnLuau" + parameters['dev'],
encoding="utf-8", stdout=logFile,
)
runResults.wait()
logFile.flush()
# Generate flamegraph from last run data
flameFile=f"{prefix}-{testName.replace('/', '-')}-profile.svg"
dump = open("profile.out").readlines()
root = Node()
for l in dump:
ticks, stack = l.strip().split(" ", 1)
node = root
for f in reversed(stack.split(";")):
source, function, line = f.split(",")
child = node.child(f)
child.function = function
child.source = source
child.line = int(line) if len(line) > 0 else 0
node = child
node.ticks += int(ticks)
svg.layout(root, lambda n: n.ticks)
svg.display(open(flameFile, mode="w"), root, "Flame Graph", "hot", flip = True)
print(f"Flamegraph results written to {flameFile}")
if os.path.exists("profile.out"):
os.remove("profile.out")
logFile.flush()
logFile.close()
# Process the benchmark data into a csv
results = {}
testName = ""
for line in open(logPath, mode="r", encoding="utf-8").readlines():
newTestMatch = re.match(r"TEST: (.+)", line)
if newTestMatch:
testName = newTestMatch.group(1)
results[testName] = {}
else:
metricMatch = re.match(OUTPUT_PATTERN, line)
if metricMatch:
metric = metricMatch.group(1)
value = metricMatch.group(2)
unit = metricMatch.group(3)
deviation = metricMatch.group(4)
samples = metricMatch.group(5)
testResult = results.get(testName)
if not testResult:
testResult = {}
results[testName] = testResult
metricResult = testResult.get(metric)
if not metricResult:
metricResult = {
"count": 0,
"valueSum": 0,
"unit": unit,
"deviationSum": 0,
"samples": 0,
}
testResult[metric] = metricResult
metricResult['count'] += 1
metricResult['valueSum'] += float(value)
metricResult['unit'] = unit
metricResult['deviationSum'] += float(deviation)
metricResult['samples'] += int(samples)
# Build the csv file from the result data
outputFile = open(f"{prefix}-benchmark.csv", mode="w", encoding="utf-8")
outputFile.write("Test,Metric,Value,Unit,Deviation,Samples")
outputFile.write('\n')
for testName, testResults in results.items():
for metric, metricResult in testResults.items():
outputFile.write("{test},\"{metric}\",{value:.5f},{unit},\"±{deviation:.2f}%\",{samples}".format(
test=testName,
metric=metric,
value=metricResult['valueSum']/metricResult['count'],
unit=metricResult['unit'],
deviation=metricResult['deviationSum']/metricResult['count'],
samples=metricResult['samples'],
))
outputFile.write('\n')
os.remove(logPath)
outputFile.close()
print(f"Benchmark results written to {prefix}-benchmark.csv")