#!/usr/bin/env ruby
# coding: utf-8

require 'getoptlong'
require 'pathname'

$benchmarks_all = [
    # Single-threaded benchmarks.
    "churn",
    "list_allocate",
    "tree_allocate",
    "tree_churn",
    "fragment",
    "fragment_iterate",
    "medium",
    "big",

    # Benchmarks based on browser recordings.
    "facebook",
    "reddit",
    "flickr",
    "theverge",
    "nimlang",

    # Multi-threaded benchmark variants.
    "message_one",
    "message_many",
    "churn --parallel",
    "list_allocate --parallel",
    "tree_allocate --parallel",
    "tree_churn --parallel",
    "fragment --parallel",
    "fragment_iterate --parallel",

    # These tests often crash TCMalloc: <rdar://problem/13657137>.
    "medium --parallel",
    "big --parallel",

    # Enable these tests to test memory footprint. The way they run is not
    # really compatible with throughput testing.
    # "reddit_memory_warning --runs 0",
    # "flickr_memory_warning --runs 0",
    # "theverge_memory_warning --runs 0",

    # Enable this test to test shrinking back down from a large heap while a process remains active.
    # The way it runs is not really compatible with throughput testing.
    # "balloon"
    "facebook --parallel",
    "reddit --parallel",
    "flickr --parallel",
    "theverge --parallel",
    # "nimlang --use-thread-id",
]

$benchmarks_memory = [
    "facebook",
    "reddit",
    "flickr",
    "theverge",
    "nimlang"
]

$benchmarks_memory_warning = [
    "reddit_memory_warning --runs 0",
    "flickr_memory_warning --runs 0",
    "theverge_memory_warning --runs 0",
]

$benchmarks = $benchmarks_all
$heap = 0

def usage
	puts "run-malloc-benchmarks [options] /path/to/MallocBench Name:/path/to/libmbmalloc.dylib [ Name:/path/to/libmbmalloc.dylib ]"
	puts
	puts "    Runs a suite of memory allocation and access benchmarks."
    puts
    puts "    <Name:/path/to/libmbmalloc.dylib> is a symbolic name followed by a path to libmbmalloc.dylib."
    puts
    puts "    Specify \"SystemMalloc\" to test the built-in libc malloc."
    puts "    Specify \"NanoMalloc\" to test the built-in libc malloc using the NanoMalloc zone."
    puts
    puts "    Example usage:"
    puts
    puts "        run-malloc-benchmarks /BUILD/MallocBench SystemMalloc:/BUILD/libmbmalloc.dylib NanoMalloc:/BUILD/libmbmalloc.dylib"
    puts "        run-malloc-benchmarks /BUILD/MallocBench FastMalloc:/BUILD/FastMalloc/libmbmalloc.dylib"
    puts "        run-malloc-benchmarks --benchmark churn SystemMalloc:/BUILD/libmbmalloc.dylib FastMalloc:/BUILD/FastMalloc/libmbmalloc.dylib"
    puts
	puts "Options:"
    puts
    puts "    --benchmark <benchmark>      Select a single benchmark to run instead of the full suite."
    puts "    --heap <heap>                Set a baseline heap size."
    puts
end

class Dylib
    attr_reader :name
    attr_reader :path

    def initialize(name, path)
        @name = name
        @path = path
    end
end

class Results
    attr_reader :executionTime
    attr_reader :peakMemory
    attr_reader :memoryAtEnd

    def initialize(executionTime, peakMemory, memoryAtEnd)
        @executionTime = executionTime
        @peakMemory = peakMemory
        @memoryAtEnd = memoryAtEnd
    end
end

class Stat
    attr_reader :benchmark
    attr_reader :result

    def initialize(benchmark, result)
        @benchmark = benchmark
        @result = result[/\d+/].to_i
    end
end

class TimeStat < Stat
    def to_s
    	@result + "ms"
    end
end

class MemoryStat < Stat
    def to_s
    	@result + "kB"
    end
end

class PeakMemoryStat < Stat
    def to_s
    	@result + "kB"
    end
end

def lpad(str, chars)
    if str.length > chars
        str
    else
        "%#{chars}s"%(str)
    end
end

def rpad(str, chars)
    while str.length < chars
        str += " "
    end
    str
end

def computeArithmeticMean(array)
  sum = 0.0
  array.each {
    | value |
    sum += value
  }
  (sum / array.length)
end

def computeGeometricMean(array)
  mult = 1.0
  array.each {
    | value |
    mult *= value ? value : 1.0
  }
  (mult ** (1.0 / array.length))
end

def computeHarmonicMean(array)
  1.0 / computeArithmeticMean(array.collect{ | value | 1.0 / value })
end

def lowerIsBetter(a, b, better, worse)
    if b < a
        return "^ " + (a.to_f / b.to_f).round(2).to_s + "x " + better
    end

    if b == a
        return ""
    end

    "! " + (b.to_f / a.to_f).round(2).to_s + "x " + worse
end


def lowerIsFaster(a, b)
    lowerIsBetter(a, b, "faster", "slower")
end

def lowerIsSmaller(a, b)
    lowerIsBetter(a, b, "smaller", "bigger")
end

def numberWithDelimiter(number)
    number.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse
end

def prettify(number, suffix)
    numberWithDelimiter(number) + suffix
end

def parseOptions
    GetoptLong.new(
        ['--benchmark', GetoptLong::REQUIRED_ARGUMENT],
        ['--memory', GetoptLong::NO_ARGUMENT],
        ['--memory_warning', GetoptLong::NO_ARGUMENT],
        ['--heap', GetoptLong::REQUIRED_ARGUMENT],
        ['--help', GetoptLong::NO_ARGUMENT],
    ).each {
        | opt, arg |
        case opt
        when '--benchmark'
            $benchmarks = [ arg ]
        when '--memory'
            $benchmarks = $benchmarks_memory
        when '--memory_warning'
            $benchmarks = $benchmarks_memory_warning
        when '--heap'
            $heap = arg
        when '--help'
            usage
            exit 1
        else
          raise "bad option: #{opt}"
        end
    }

    if ARGV.length < 1
        puts "Error: No MallocBench specified."
        exit 1
    end

    if ARGV.length < 2
        puts "Error: No dylib specified."
        exit 1
    end

    $mallocBench = File.absolute_path(ARGV.shift)
    if !File.exists?($mallocBench)
        puts "File not found: #{$mallocBench}."
        exit 1
    end

    $buildDir = Pathname.new($mallocBench).dirname

    dylibs = []
    ARGV.each {
        | arg |
        name, path = arg.split(":")
        if !name || name.length < 1 ||
            !path || path.length < 1
            puts "Invalid <Name:/path/to/dylib>: '#{arg}'."
            exit 1
        end

        dylib = Dylib.new(name, File.expand_path(path))

        if !File.exists?(dylib.path)
            puts "File not found: #{dylib.path}."
            exit 1
        end

        dylibs.push(dylib)
    }
    dylibs
end

def runBenchmarks(dylibs)
    executionTime = []
    peakMemory = []
    memoryAtEnd = []

    $benchmarks.each {
        | benchmark |

        executionTime.push([])
        peakMemory.push([])
        memoryAtEnd.push([])

        dylibs.each {
            | dylib |

            $stderr.print "\rRUNNING #{dylib.name}: #{benchmark}...                                "
            env = "DYLD_LIBRARY_PATH='#{Pathname.new(dylib.path).dirname}' "
            env += "LD_LIBRARY_PATH='#{Pathname.new(dylib.path).dirname}' "
            if dylib.name == "NanoMalloc"
                env += "MallocNanoZone=1 "
            elsif dylib.name == "SystemMalloc"
                env += "MallocNanoZone=0 "
            end
            input = "cd '#{$buildDir}'; #{env} '#{$mallocBench}' --benchmark #{benchmark} --heap #{$heap}}"
            output =`#{input}`
            splitOutput = output.split("\n")

            executionTime[-1].push(TimeStat.new(benchmark, splitOutput[1]))
            peakMemory[-1].push(PeakMemoryStat.new(benchmark, splitOutput.length > 3 ? splitOutput[2] : "0"))
            memoryAtEnd[-1].push(MemoryStat.new(benchmark, splitOutput.length > 2 ? splitOutput[3] : "0"))
        }
    }
    $stderr.print "\r                                                                                \n"

    Results.new(executionTime, peakMemory, memoryAtEnd)
end

def printResults(dylibs, results)
    def printHeader(dylibs, fieldSize)
        print
        print lpad("", fieldSize)
        print lpad(dylibs[0].name, fieldSize)
        if dylibs.length > 1
            print lpad(dylibs[1].name, fieldSize)
            print lpad("Δ", fieldSize)
        end
        print "\n"
    end

    def printMetric(name, results, compareFunction, suffix, fieldSize)
        def printMean(name, results, meanFunction, compareFunction, suffix, fieldSize)
            means = []

            means.push(meanFunction.call(results.collect { | stats | stats[0].result }))
            print rpad("    " + name, fieldSize)
            print lpad("#{prettify(means[0].round, suffix)}", fieldSize)

            if results[0][1]
                means.push(meanFunction.call(results.collect { | stats | stats[1].result }))
                print lpad("#{prettify(means[1].round, suffix)}", fieldSize)
                print lpad(compareFunction.call(means[0], means[1]), fieldSize)
            end

            print "\n"
        end

        if results[0][0].result == 0
            return
        end

        print name + ":\n"
        results.each {
            | stats |

            print rpad("    " + stats[0].benchmark, fieldSize)
            print lpad("#{prettify(stats[0].result, suffix)}", fieldSize)

            if stats[1]
                print lpad("#{prettify(stats[1].result, suffix)}", fieldSize)
                print lpad(compareFunction.call(stats[0].result, stats[1].result), fieldSize)
            end

            print "\n"
        }

        print "\n"

        printMean("<geometric mean>", results, method(:computeGeometricMean), compareFunction, suffix, fieldSize)
        printMean("<arithmetic mean>", results, method(:computeArithmeticMean), compareFunction, suffix, fieldSize)
        printMean("<harmonic mean>", results, method(:computeHarmonicMean), compareFunction, suffix, fieldSize)

        print "\n"
    end

    fieldSize = ($benchmarks + ["<arithmetic mean>"]).collect {
        | benchmark |
        benchmark.size
    }.max + 4

    printHeader(dylibs, fieldSize)
    printMetric("Execution Time", results.executionTime, method(:lowerIsFaster), "ms", fieldSize)
    printMetric("Peak Memory", results.peakMemory, method(:lowerIsSmaller), "kB", fieldSize)
    printMetric("Memory at End", results.memoryAtEnd, method(:lowerIsSmaller), "kB", fieldSize)
end

def main
    begin
        dylibs = parseOptions()
        results = runBenchmarks(dylibs)
        printResults(dylibs, results)
    rescue => exception
        puts
        puts
        puts exception
        puts exception.backtrace
        puts
    end
end

main()
