| #!/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.exists?(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 |