| #!/usr/bin/env ruby | |
| # Copyright (C) 2018 Apple Inc. All rights reserved. | |
| # | |
| # Redistribution and use in source and binary forms, with or without | |
| # modification, are permitted provided that the following conditions | |
| # are met: | |
| # 1. Redistributions of source code must retain the above copyright | |
| # notice, this list of conditions and the following disclaimer. | |
| # 2. Redistributions in binary form must reproduce the above copyright | |
| # notice, this list of conditions and the following disclaimer in the | |
| # documentation and/or other materials provided with the distribution. | |
| # | |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' | |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, | |
| # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS | |
| # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF | |
| # THE POSSIBILITY OF SUCH DAMAGE. | |
| require 'fileutils' | |
| require 'pathname' | |
| require 'open3' | |
| require "JSON" | |
| require 'getoptlong' | |
| def usage | |
| puts "run-testmem [options]" | |
| puts "--build-dir (-b) Pass in a path to your build directory, e.g, WebKitBuild/Release" | |
| puts "--verbose (-v) Print more information as the benchmark runs" | |
| puts "--count (-c) Number of outer iterations to run the benchmark for" | |
| puts "--dry (-d) Print shell output that can be run as a bash script on a different device. When using this option, provide the --script-path and --build-dir options" | |
| puts "--script-path (-s) The path to the directory where you expect the testmem tests to live. Use this when doing a dry run with --dry" | |
| puts "--parse (-p) After executing the dry run, capture its stdout and write it to a file. Pass the path to that file for this option and run-testmem will compute the results of the benchmark run" | |
| puts "--help (-h) Print this message" | |
| end | |
| THIS_SCRIPT_PATH = Pathname.new(__FILE__).realpath | |
| SCRIPTS_PATH = THIS_SCRIPT_PATH.dirname | |
| $buildDir = nil | |
| $verbose = false | |
| $outerIterations = 3 | |
| $dryRun = false | |
| $scriptPath = nil | |
| $parsePath = nil | |
| GetoptLong.new(["--build-dir", "-b", GetoptLong::REQUIRED_ARGUMENT], | |
| ["--verbose", "-v", GetoptLong::NO_ARGUMENT], | |
| ["--count", "-c", GetoptLong::REQUIRED_ARGUMENT], | |
| ["--dry", "-d", GetoptLong::NO_ARGUMENT], | |
| ["--script-path", "-s", GetoptLong::REQUIRED_ARGUMENT], | |
| ["--parse", "-p", GetoptLong::REQUIRED_ARGUMENT], | |
| ["--help", "-h", GetoptLong::NO_ARGUMENT], | |
| ).each { | |
| | opt, arg | | |
| case opt | |
| when "--build-dir" | |
| $buildDir = arg | |
| when "--verbose" | |
| $verbose = true | |
| when "--count" | |
| $outerIterations = arg.to_i | |
| if $outerIterations < 1 | |
| puts "--count must be > 0" | |
| exit 1 | |
| end | |
| when "--dry" | |
| $dryRun = true | |
| when "--script-path" | |
| $scriptPath = arg | |
| when "--parse" | |
| $parsePath = arg | |
| when "--help" | |
| usage | |
| exit 1 | |
| end | |
| } | |
| if $scriptPath && !$dryRun | |
| puts "--script-path is only supported when you are doing a --dry run" | |
| exit 1 | |
| end | |
| def getBuildDirectory | |
| if $buildDir != nil | |
| return $buildDir | |
| end | |
| command = SCRIPTS_PATH.join("webkit-build-directory").to_s | |
| command += " --release" | |
| command += " --executablePath" | |
| output = `#{command}`.split("\n") | |
| if !output.length | |
| puts "Error: could not find release WebKitBuild" | |
| exit 1 | |
| end | |
| output = output[0] | |
| $buildDir = Pathname.new(output).to_s | |
| $buildDir | |
| end | |
| def getTestmemPath | |
| path = Pathname.new(getBuildDirectory).join("testmem").to_s | |
| if !File.exist?(path) && !$dryRun | |
| puts "Error: no testmem binary found in <build>/Release" | |
| exit 1 | |
| end | |
| path | |
| end | |
| def iterationCount(path) | |
| iterationMap = { | |
| "air" => 4, | |
| "basic" => 5, | |
| "splay" => 10, | |
| "hash-map" => 5, | |
| "box2d" => 3, | |
| } | |
| name = File.basename(path, ".js") | |
| iterationMap[name] || 20 | |
| end | |
| def getTests | |
| dirPath = Pathname.new(SCRIPTS_PATH).join("../../PerformanceTests/testmem") | |
| files = [] | |
| Dir.foreach(dirPath) { | |
| | filename | | |
| next unless filename =~ /\.js$/ | |
| filePath = dirPath.join(filename).to_s | |
| filePath = Pathname.new($scriptPath).join(filename).to_s if $scriptPath | |
| files.push([filePath, iterationCount(filePath)]) | |
| } | |
| files.sort_by { | (path) | File.basename(path) } | |
| end | |
| def processRunOutput(stdout, path) | |
| time, peakFootprint, footprintAtEnd = stdout.split("\n") | |
| raise unless time.slice!("time:") | |
| raise unless peakFootprint.slice!("peak footprint:") | |
| raise unless footprintAtEnd.slice!("footprint at end:") | |
| time = time.to_f | |
| peakFootprint = peakFootprint.to_f | |
| footprintAtEnd = footprintAtEnd.to_f | |
| if $verbose | |
| puts path | |
| puts "time: #{time}" | |
| puts "peak footprint: #{peakFootprint/1024/1024} MB" | |
| puts "end footprint: #{footprintAtEnd/1024/1024} MB\n" | |
| end | |
| {"time"=>time, "peak"=>peakFootprint, "end"=>footprintAtEnd} | |
| end | |
| def runTest(path, iters) | |
| command = "#{getTestmemPath} #{path} #{iters}" | |
| environment = { | |
| "DYLD_FRAMEWORK_PATH" => getBuildDirectory, | |
| "JSC_useJIT" => "false", | |
| "JSC_useRegExpJIT" => "false", | |
| } | |
| if $dryRun | |
| environment.each { | key, value | | |
| command = "#{key}=#{value} #{command}" | |
| } | |
| puts "echo \"#{command}\"" | |
| puts command | |
| return | |
| end | |
| stdout, stderr, exitCode = Open3.capture3(environment, command) | |
| if $verbose | |
| puts stdout | |
| puts stderr | |
| end | |
| if exitCode != 0 | |
| puts "testmem failed to run" | |
| puts stdout | |
| puts stderr | |
| exit 1 | |
| end | |
| processRunOutput(stdout, path) | |
| end | |
| def geomean(arr) | |
| score = arr.inject(1.0, :*) | |
| score ** (1.0 / arr.length) | |
| end | |
| def mean(arr) | |
| sum = arr.inject(0.0, :+) | |
| sum / arr.length | |
| end | |
| def processScores(scores) | |
| peakScore = [] | |
| endScore = [] | |
| timeScore = [] | |
| scores.each { | key, value | | |
| endAvg = mean(value.map { | element | element["end"] }) | |
| peakAvg = mean(value.map { | element | element["peak"] }) | |
| timeAvg = mean(value.map { | element | element["time"] }) | |
| peakScore.push(peakAvg) | |
| endScore.push(endAvg) | |
| timeScore.push(timeAvg) | |
| puts File.basename(key, ".js") | |
| puts " end: #{(endAvg/1024/1024).round(4)} MB" | |
| puts " peak: #{(peakAvg/1024/1024).round(4)} MB" | |
| puts " time: #{(timeAvg*1000).round(2)} ms\n" | |
| } | |
| endScore = geomean(endScore) | |
| peakScore = geomean(peakScore) | |
| timeScore = geomean(timeScore) | |
| puts | |
| puts "end score: #{(endScore/1024/1024).round(4)} MB" | |
| puts "peak score: #{(peakScore/1024/1024).round(4)} MB\n" | |
| puts "total memory score: #{(geomean([endScore, peakScore])/1024/1024).round(4)} MB" | |
| puts "time score: #{(timeScore*1000).round(2)} ms\n\n" | |
| puts JSON.pretty_generate(scores) if $verbose | |
| end | |
| def run | |
| tests = getTests | |
| scores = {} | |
| tests.each { | (path) | scores[path] = [] } | |
| count = $outerIterations | |
| if $dryRun | |
| (0..(count-1)).each { | currentIter | | |
| tests.each { | (path, iters) | | |
| runTest(path, iters) | |
| } | |
| } | |
| return | |
| end | |
| (0..(count-1)).each { | currentIter | | |
| tests.each { | (path, iters) | | |
| statusToPrint = "iteration #{currentIter + 1}: #{File.basename(path, ".js")}" | |
| print "#{statusToPrint}\r" | |
| scores[path].push(runTest(path, iters)) | |
| print "#{" ".rjust(statusToPrint.length)}\r" | |
| } | |
| } | |
| processScores(scores) | |
| end | |
| def parseResultOfDryRun(path) | |
| contents = IO.read(path).split("\n") | |
| if !contents.length || contents.length % 4 != 0 | |
| puts "Bad input, expect multiple of 4 number of lines from output of running the result of --dry" | |
| exit 1 | |
| end | |
| scores = {} | |
| i = 0 | |
| while i < contents.length | |
| path = contents[i + 0].split(" ")[-2] | |
| scores[path] = [] if !scores[path] | |
| stdout = [contents[i + 1], contents[i + 2], contents[i + 3]].join("\n") | |
| scores[path].push(processRunOutput(stdout, path)) | |
| i += 4 | |
| end | |
| processScores(scores) | |
| end | |
| if $parsePath | |
| parseResultOfDryRun($parsePath) | |
| else | |
| run | |
| end |