9f228f24ee03dac1317595f158ba7c434fa51a68
[WebKit-https.git] / PerformanceTests / JetStream2 / RAMification.py
1 # Copyright (C) 2018-2019 Apple Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions
5 # are met:
6 # 1. Redistributions of source code must retain the above copyright
7 #    notice, this list of conditions and the following disclaimer.
8 # 2. Redistributions in binary form must reproduce the above copyright
9 #    notice, this list of conditions and the following disclaimer in the
10 #    documentation and/or other materials provided with the distribution.
11 #
12 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
13 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
14 # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
15 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
16 # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
17 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
18 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
19 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
20 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
21 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
22 # THE POSSIBILITY OF SUCH DAMAGE.
23
24 import argparse
25 import collections
26 import json
27 import math
28 import os
29 import re
30 import subprocess
31
32 jitTests = ["3d-cube-SP", "3d-raytrace-SP", "acorn-wtb", "ai-astar", "Air", "async-fs", "Babylon", "babylon-wtb", "base64-SP", "Basic", "Box2D", "cdjs", "chai-wtb", "coffeescript-wtb", "crypto", "crypto-aes-SP", "crypto-md5-SP", "crypto-sha1-SP", "date-format-tofte-SP", "date-format-xparb-SP", "delta-blue", "earley-boyer", "espree-wtb", "first-inspector-code-load", "FlightPlanner", "float-mm.c", "gaussian-blur", "gbemu", "gcc-loops-wasm", "hash-map", "HashSet-wasm", "jshint-wtb", "json-parse-inspector", "json-stringify-inspector", "lebab-wtb", "mandreel", "ML", "multi-inspector-code-load", "n-body-SP", "navier-stokes", "octane-code-load", "octane-zlib", "OfflineAssembler", "pdfjs", "prepack-wtb", "quicksort-wasm", "raytrace", "regex-dna-SP", "regexp", "richards", "richards-wasm", "splay", "stanford-crypto-aes", "stanford-crypto-pbkdf2", "stanford-crypto-sha256", "string-unpack-code-SP", "tagcloud-SP", "tsf-wasm", "typescript", "uglify-js-wtb", "UniPoker", "WSL"]
33
34 nonJITTests = ["3d-cube-SP", "3d-raytrace-SP", "acorn-wtb", "ai-astar", "Air", "async-fs", "Babylon", "babylon-wtb", "base64-SP", "Basic", "Box2D", "cdjs", "chai-wtb", "coffeescript-wtb", "crypto-aes-SP", "delta-blue", "earley-boyer", "espree-wtb", "first-inspector-code-load", "gaussian-blur", "gbemu", "hash-map", "jshint-wtb", "json-parse-inspector", "json-stringify-inspector", "lebab-wtb", "mandreel", "ML", "multi-inspector-code-load", "octane-code-load", "OfflineAssembler", "pdfjs", "prepack-wtb", "raytrace", "regex-dna-SP", "regexp", "splay", "stanford-crypto-aes", "string-unpack-code-SP", "tagcloud-SP", "typescript", "uglify-js-wtb"]
35
36 # Run two groups of tests with each group in a single JSC instance to see how well memory recovers between tests.
37 groupTests = ["typescript,acorn-wtb,Air,pdfjs,crypto-aes-SP", "splay,FlightPlanner,prepack-wtb,octane-zlib,3d-cube-SP"]
38
39 luaTests = [("hello_world-LJF", "LuaJSFight/hello_world.js", 5), ("list_search-LJF", "LuaJSFight/list_search.js", 5), ("lists-LJF", "LuaJSFight/lists.js", 5), ("string_lists-LJF", "LuaJSFight/string_lists.js", 5), ("richards", "LuaJSFight/richards.js", 5)]
40
41 oneMB = float(1024 * 1024)
42 footprintRE = re.compile("Current Footprint: (\d+(?:.\d+)?)")
43 peakFootprintRE = re.compile("Peak Footprint: (\d+(?:.\d+)?)")
44
45 TestResult = collections.namedtuple("TestResult", ["name", "returnCode", "footprint", "peakFootprint"])
46
47 ramification_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
48
49 remoteHooks = {}
50
51
52 def mean(values):
53     if not len(values):
54         return None
55
56     return sum(values) / len(values)
57
58
59 def geomean(values):
60     if not len(values):
61         return None
62
63     product = 1.0
64
65     for x in values:
66         product *= x
67
68     return math.pow(product, (1.0 / len(values)))
69
70
71 def frameworkPathFromExecutablePath(execPath):
72     if not os.path.abspath(execPath):
73         execPath = os.path.isabs(execPath)
74
75     pathMatch = re.match("(.*?/WebKitBuild/(Release|Debug)+)/([a-zA-Z]+)$", execPath)
76     if pathMatch:
77         return pathMatch.group(1)
78
79     pathMatch = re.match("(.*?)/JavaScriptCore.framework/Resources/([a-zA-Z]+)$", execPath)
80     if pathMatch:
81         return pathMatch.group(1)
82
83     pathMatch = re.match("(.*?/(Release|Debug)+)/([a-zA-Z]+)$", execPath)
84     if pathMatch:
85         return pathMatch.group(1)
86
87     pathMatch = re.match("(.*?)/JavaScriptCore.framework/(.*?)/Resources/([a-zA-Z]+)$", execPath)
88     if pathMatch:
89         return pathMatch.group(1)
90
91
92 def parseArgs(parser=None):
93     def optStrToBool(arg):
94         if arg.lower() in ("true", "t", "yes", "y"):
95             return True
96         if arg.lower() in ("false", "f", "no", "n"):
97             return False
98
99         raise argparse.ArgumentTypeError("Boolean value expected")
100
101     if not parser:
102         parser = argparse.ArgumentParser(description="RAMification benchmark driver script")
103         parser.set_defaults(runner=LocalRunner)
104
105     verbosityGroup = parser.add_mutually_exclusive_group()
106     verbosityGroup.add_argument("-q", "--quiet", dest="verbose", action="store_false", help="Provide less output")
107     verbosityGroup.add_argument("-v", "--verbose", dest="verbose", action="store_true", default=True, help="Provide more output")
108
109     parser.add_argument("-c", "--jsc", dest="jscCommand", type=str, default="/usr/local/bin/jsc", metavar="path-to-jsc", help="Path to jsc command")
110     parser.add_argument("-d", "--jetstream2-dir", dest="testDir", type=str, default=ramification_dir, metavar="path-to-JetStream2-files", help="JetStream2 root directory")
111     parser.add_argument("-e", "--env-var", dest="extraEnvVars", action="append", default=[], metavar="env-var=value", help="Specify additional environment variables")
112     parser.add_argument("-f", "--format-json", dest="formatJSON", action="store_true", default=False, help="Format JSON with whitespace")
113     parser.add_argument("-g", "--run-grouped-tests", dest="runGroupedTests", nargs="?", const=True, default=None, type=optStrToBool, metavar="true / false", help="Run grouped tests [default]")
114     parser.add_argument("-j", "--run-jit", dest="runJITTests", nargs="?", const=True, default=None, type=optStrToBool, metavar="true / false", help="Run JIT tests [default]")
115     parser.add_argument("-l", "--lua", dest="runLuaTests", nargs="?", const=True, default=None, type=optStrToBool, metavar="true / false", help="Run Lua comparison tests [default]")
116     parser.add_argument("-n", "--run-no-jit", dest="runNoJITTests", nargs="?", const=True, default=None, type=optStrToBool, metavar="true / false", help="Run no JIT tests [default]")
117     parser.add_argument("-o", "--output", dest="jsonFilename", type=str, default=None, metavar="JSON-output-file", help="Path to JSON output")
118
119     args = parser.parse_args()
120
121     subtestArgs = [args.runGroupedTests, args.runJITTests, args.runLuaTests, args.runNoJITTests]
122     allDefault = all([arg is None for arg in subtestArgs])
123     anyTrue = any([arg is True for arg in subtestArgs])
124     anyFalse = any([arg is False for arg in subtestArgs])
125
126     # Default behavior is to run all subtests.
127     # If a test is explicitly specified not to run, skip that test and use the default behavior for the remaining tests.
128     # If tests are explicitly specified to run, only run those tests.
129     # If there is a mix of tests specified to run and not to run, also do not run any unspecified tests.
130     getArgValue = lambda arg: True if allDefault else False if arg is None and anyTrue else True if arg is None and anyFalse else arg
131
132     args.runJITTests = getArgValue(args.runJITTests)
133     args.runNoJITTests = getArgValue(args.runNoJITTests)
134     args.runLuaTests = getArgValue(args.runLuaTests)
135     args.runGroupedTests = getArgValue(args.runGroupedTests)
136
137     return args
138
139
140 class BaseRunner:
141     def __init__(self, args):
142         self.rootDir = args.testDir
143         self.environmentVars = {}
144
145     def setup(self):
146         pass
147
148     def setEnv(self, variable, value):
149         self.environmentVars[variable] = value
150
151     def unsetEnv(self, variable):
152         self.environmentVars.pop(variable, None)
153
154     def resetForTest(self, testName):
155         self.testName = testName
156         self.footprint = None
157         self.peakFootprint = None
158         self.returnCode = 0
159
160     def processLine(self, line):
161         line = line.strip()
162
163         footprintMatch = re.match(footprintRE, line)
164         if footprintMatch:
165             self.footprint = int(footprintMatch.group(1))
166             return
167
168         peakFootprintMatch = re.match(peakFootprintRE, line)
169         if peakFootprintMatch:
170             self.peakFootprint = int(peakFootprintMatch.group(1))
171
172     def setReturnCode(self, returnCode):
173         self.returnCode = returnCode
174
175     def getResults(self):
176         return TestResult(name=self.testName, returnCode=self.returnCode, footprint=self.footprint, peakFootprint=self.peakFootprint)
177
178
179 class LocalRunner(BaseRunner):
180     def __init__(self, args):
181         BaseRunner.__init__(self, args)
182         self.jscCommand = args.jscCommand
183
184     def runOneTest(self, test, extraOptions=None, useJetStream2Harness=True):
185         self.resetForTest(test)
186
187         args = [self.jscCommand]
188         if extraOptions:
189             args.extend(extraOptions)
190
191         if useJetStream2Harness:
192             args.extend(["-e", "testList='{test}'; runMode='RAMification'".format(test=test), "cli.js"])
193         else:
194             args.extend(["--footprint", "{test}".format(test=test)])
195
196         self.resetForTest(test)
197
198         proc = subprocess.Popen(args, cwd=self.rootDir, env=self.environmentVars, stdout=subprocess.PIPE, stderr=None, shell=False)
199         while True:
200             line = proc.stdout.readline()
201             self.processLine(line)
202
203             if line == "":
204                 break
205
206         self.setReturnCode(proc.wait())
207
208         return self.getResults()
209
210
211 def main(parser=None):
212     footprintValues = []
213     peakFootprintValues = []
214     testResultsDict = {}
215     hasFailedRuns = False
216
217     args = parseArgs(parser=parser)
218
219     testRunner = args.runner(args)
220
221     dyldFrameworkPath = frameworkPathFromExecutablePath(args.jscCommand)
222     if dyldFrameworkPath:
223         testRunner.setEnv("DYLD_FRAMEWORK_PATH", dyldFrameworkPath)
224
225     for envVar in args.extraEnvVars:
226         envVarParts = envVar.split("=")
227         if len(envVarParts) == 1:
228             envVarParts[1] = "1"
229         testRunner.setEnv(envVarParts[0], envVarParts[1])
230
231     testRunner.setup()
232
233     def runTestList(testList, extraOptions=None, useJetStream2Harness=True):
234         testScoresDict = {}
235
236         for testInfo in testList:
237             footprintScores = []
238             peakFootprintScores = []
239             if isinstance(testInfo, tuple):
240                 testName, test, weight = testInfo
241             else:
242                 testName, test, weight = testInfo, testInfo, 1
243
244             print "Running {}...".format(testName),
245             testResult = testRunner.runOneTest(test, extraOptions, useJetStream2Harness)
246
247             if testResult.returnCode == 0 and testResult.footprint and testResult.peakFootprint:
248                 if args.verbose:
249                     print "footprint: {}, peak footprint: {}".format(testResult.footprint, testResult.peakFootprint)
250                 else:
251                     print
252
253                 if testResult.footprint:
254                     footprintScores.append(int(testResult.footprint))
255                     for count in range(0, weight):
256                         footprintValues.append(testResult.footprint / oneMB)
257                 if testResult.peakFootprint:
258                     peakFootprintScores.append(int(testResult.peakFootprint))
259                     for count in range(0, weight):
260                         peakFootprintValues.append(testResult.peakFootprint / oneMB)
261             else:
262                 hasFailedRuns = True
263                 print "failed",
264                 if testResult.returnCode != 0:
265                     print " exit code = {}".format(testResult.returnCode),
266                 if not testResult.footprint:
267                     print " footprint = {}".format(testResult.footprint),
268                 if not testResult.peakFootprint:
269                     print " peak footprint = {}".format(testResult.peakFootprint),
270                 print
271
272             testScoresDict[test] = {"metrics": {"Allocations": ["Geometric"]}, "tests": {"end": {"metrics": {"Allocations": {"current": footprintScores}}}, "peak": {"metrics": {"Allocations": {"current": peakFootprintScores}}}}}
273
274         return testScoresDict
275
276     current_path = os.getcwd()
277     os.chdir(ramification_dir)  # To allow JS libraries to load
278
279     if args.runLuaTests:
280         if args.verbose:
281             print "== LuaJSFight No JIT tests =="
282
283         # Use system malloc for LuaJSFight tests
284         testRunner.setEnv("Malloc", "X")
285
286         scoresDict = runTestList(luaTests, ["--useJIT=false", "--forceMiniVMMode=true"], useJetStream2Harness=False)
287
288         testResultsDict["LuaJSFight No JIT Tests"] = {"metrics": {"Allocations": ["Geometric"]}, "tests": scoresDict}
289
290         testRunner.unsetEnv("Malloc")
291
292     if args.runGroupedTests:
293         if args.verbose:
294             print "== Grouped tests =="
295
296         scoresDict = runTestList(groupTests)
297
298         testResultsDict["Grouped Tests"] = {"metrics": {"Allocations": ["Geometric"]}, "tests": scoresDict}
299
300     if args.runJITTests:
301         if args.verbose:
302             print "== JIT tests =="
303
304         scoresDict = runTestList(jitTests)
305
306         testResultsDict["JIT Tests"] = {"metrics": {"Allocations": ["Geometric"]}, "tests": scoresDict}
307
308     if args.runNoJITTests:
309         if args.verbose:
310             print "== No JIT tests =="
311
312         scoresDict = runTestList(nonJITTests, ["--useJIT=false", "-e", "testIterationCount=1"])
313
314         testResultsDict["No JIT Tests"] = {"metrics": {"Allocations": ["Geometric"]}, "tests": scoresDict}
315
316     footprintGeomean = int(geomean(footprintValues) * oneMB)
317     peakFootprintGeomean = int(geomean(peakFootprintValues) * oneMB)
318     totalScore = int(geomean([footprintGeomean, peakFootprintGeomean]))
319
320     if footprintGeomean:
321         print "Footprint geomean: {} ({:.3f} MB)".format(footprintGeomean, footprintGeomean / oneMB)
322
323     if peakFootprintGeomean:
324         print "Peak Footprint geomean: {} ({:.3f} MB)".format(peakFootprintGeomean, peakFootprintGeomean / oneMB)
325
326     if footprintGeomean and peakFootprintGeomean:
327         print "Score: {} ({:.3f} MB)".format(totalScore, totalScore / oneMB)
328
329     resultsDict = {"RAMification": {"metrics": {"Allocations": {"current": [totalScore]}}, "tests": testResultsDict}}
330
331     os.chdir(current_path)  # Reset the path back to what it was before
332
333     if args.jsonFilename:
334         with open(args.jsonFilename, "w") as jsonFile:
335             if args.formatJSON:
336                 json.dump(resultsDict, jsonFile, indent=4, separators=(',', ': '))
337             else:
338                 json.dump(resultsDict, jsonFile)
339
340     if hasFailedRuns:
341         print "Detected failed run(s), exiting with non-zero return code"
342
343     return hasFailedRuns
344
345
346 if __name__ == "__main__":
347     exit(main())