#!/usr/bin/env ruby

# Copyright (C) 2012, 2013, 2014 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 'rubygems'

require 'readline'

begin
    require 'json'
    require 'highline'
rescue LoadError
    $stderr.puts "Error: some required gems are not installed!"
    $stderr.puts
    $stderr.puts "Try running:"
    $stderr.puts
    $stderr.puts "sudo gem install json"
    $stderr.puts "sudo gem install highline"
    exit 1
end

class Bytecode
    attr_accessor :bytecodes, :bytecodeIndex, :opcode, :description, :topCounts, :bottomCounts, :machineInlinees, :osrExits
    
    def initialize(bytecodes, bytecodeIndex, opcode, description)
        @bytecodes = bytecodes
        @bytecodeIndex = bytecodeIndex
        @opcode = opcode
        @description = description
        @topCounts = [] # "source" counts
        @bottomCounts = {} # "machine" counts, maps compilations to counts
        @machineInlinees = {} # maps my compilation to a set of inlinees
        @osrExits = []
    end
    
    def shouldHaveCounts?
        @opcode != "op_call_put_result"
    end
    
    def addTopCount(count)
        @topCounts << count
    end
    
    def addBottomCountForCompilation(count, compilation)
        @bottomCounts[compilation] = [] unless @bottomCounts[compilation]
        @bottomCounts[compilation] << count
    end
    
    def addMachineInlinee(compilation, inlinee)
        @machineInlinees[compilation] = {} unless @machineInlinees[compilation]
        @machineInlinees[compilation][inlinee] = true
    end
    
    def totalTopExecutionCount
        sum = 0
        @topCounts.each {
            | value |
            sum += value.count
        }
        sum
    end
    
    def topExecutionCount(engine)
        sum = 0
        @topCounts.each {
            | value |
            if value.engine == engine
                sum += value.count
            end
        }
        sum
    end
    
    def totalBottomExecutionCount
        sum = 0
        @bottomCounts.each_value {
            | counts |
            max = 0
            counts.each {
                | value |
                max = [max, value.count].max
            }
            sum += max
        }
        sum
    end
    
    def bottomExecutionCount(engine)
        sum = 0
        @bottomCounts.each_pair {
            | compilation, counts |
            if compilation.engine == engine
                max = 0
                counts.each {
                    | value |
                    max = [max, value.count].max
                }
                sum += max
            end
        }
        sum
    end
    
    def totalExitCount
        sum = 0
        @osrExits.each {
            | exit |
            sum += exit.count
        }
        sum
    end
end

class Bytecodes
    attr_accessor :codeHash, :inferredName, :source, :instructionCount, :machineInlineSites, :compilations
    
    def initialize(json)
        @codeHash = json["hash"].to_s
        @inferredName = json["inferredName"].to_s
        @source = json["sourceCode"].to_s
        @instructionCount = json["instructionCount"].to_i
        @bytecode = {}
        json["bytecode"].each {
            | subJson |
            index = subJson["bytecodeIndex"].to_i
            @bytecode[index] = Bytecode.new(self, index, subJson["opcode"].to_s, subJson["description"].to_s)
        }
        @machineInlineSites = {} # maps compilation to a set of origins
        @compilations = []
    end
    
    def name(limit)
        if to_s.size > limit
            "\##{@codeHash}"
        else
            to_s
        end
    end
    
    def to_s
        "#{@inferredName}\##{@codeHash}"
    end
    
    def matches(pattern)
        if pattern =~ /^#/
            $~.post_match == @codeHash
        elsif pattern =~ /#/
            pattern == to_s
        else
            pattern == @inferredName or pattern == @codeHash
        end
    end
    
    def each
        @bytecode.values.sort{|a, b| a.bytecodeIndex <=> b.bytecodeIndex}.each {
            | value |
            yield value
        }
    end
    
    def bytecode(bytecodeIndex)
        @bytecode[bytecodeIndex]
    end
    
    def addMachineInlineSite(compilation, origin)
        @machineInlineSites[compilation] = {} unless @machineInlineSites[compilation]
        @machineInlineSites[compilation][origin] = true
    end
    
    def totalMachineInlineSites
        sum = 0
        @machineInlineSites.each_value {
            | set |
            sum += set.size
        }
        sum
    end
    
    def sourceMachineInlineSites
        set = {}
        @machineInlineSites.each_value {
            | mySet |
            set.merge!(mySet)
        }
        set.size
    end
    
    def totalMaxTopExecutionCount
        max = 0
        @bytecode.each_value {
            | bytecode |
            max = [max, bytecode.totalTopExecutionCount].max
        }
        max
    end
    
    def maxTopExecutionCount(engine)
        max = 0
        @bytecode.each_value {
            | bytecode |
            max = [max, bytecode.topExecutionCount(engine)].max
        }
        max
    end
    
    def totalMaxBottomExecutionCount
        max = 0
        @bytecode.each_value {
            | bytecode |
            max = [max, bytecode.totalBottomExecutionCount].max
        }
        max
    end
    
    def maxBottomExecutionCount(engine)
        max = 0
        @bytecode.each_value {
            | bytecode |
            max = [max, bytecode.bottomExecutionCount(engine)].max
        }
        max
    end
    
    def totalExitCount
        sum = 0
        each {
            | bytecode |
            sum += bytecode.totalExitCount
        }
        sum
    end
    
    def codeHashSortKey
        codeHash
    end
end

class ProfiledBytecode
    attr_reader :bytecodeIndex, :description
    
    def initialize(json)
        @bytecodeIndex = json["bytecodeIndex"].to_i
        @description = json["description"].to_s
    end
end

class ProfiledBytecodes
    attr_reader :header, :bytecodes
    
    def initialize(json)
        @header = json["header"]
        @bytecodes = $bytecodes[json["bytecodesID"].to_i]
        @sequence = json["bytecode"].map {
            | subJson |
            ProfiledBytecode.new(subJson)
        }
    end
    
    def each
        @sequence.each {
            | description |
            yield description
        }
    end
end

def originStackFromJSON(json)
    json.map {
        | subJson |
        $bytecodes[subJson["bytecodesID"].to_i].bytecode(subJson["bytecodeIndex"].to_i)
    }
end

class CompiledBytecode
    attr_accessor :origin, :description
    
    def initialize(json)
        @origin = originStackFromJSON(json["origin"])
        @description = json["description"].to_s
    end
end

class ExecutionCounter
    attr_accessor :origin, :engine, :count
    
    def initialize(origin, engine, count)
        @origin = origin
        @engine = engine
        @count = count
    end
end

class OSRExit
    attr_reader :compilation, :origin, :codeAddresses, :exitKind, :isWatchpoint, :count
    
    def initialize(compilation, origin, codeAddresses, exitKind, isWatchpoint, count)
        @compilation = compilation
        @origin = origin
        @codeAddresses = codeAddresses
        @exitKind = exitKind
        @isWatchpoint = isWatchpoint
        @count = count
    end
    
    def dumpForDisplay(prefix)
        puts(prefix + "EXIT: due to #{@exitKind}, #{@count} times")
    end
end

class Compilation
    attr_accessor :bytecode, :engine, :descriptions, :counters, :compilationIndex
    attr_accessor :osrExits, :profiledBytecodes, :numInlinedGetByIds, :numInlinedPutByIds
    attr_accessor :numInlinedCalls, :jettisonReason
    
    def initialize(json)
        @bytecode = $bytecodes[json["bytecodesID"].to_i]
        @bytecode.compilations << self
        @compilationIndex = @bytecode.compilations.size
        @engine = json["compilationKind"]
        @descriptions = json["descriptions"].map {
            | subJson |
            CompiledBytecode.new(subJson)
        }
        @descriptions.each {
            | description |
            next if description.origin.empty?
            description.origin[1..-1].each_with_index {
                | inlinee, index |
                description.origin[0].addMachineInlinee(self, inlinee.bytecodes)
                inlinee.bytecodes.addMachineInlineSite(self, description.origin[0...index])
            }
        }
        @counters = {}
        json["counters"].each {
            | subJson |
            origin = originStackFromJSON(subJson["origin"])
            counter = ExecutionCounter.new(origin, @engine, subJson["executionCount"].to_i)
            @counters[origin] = counter
            origin[-1].addTopCount(counter)
            origin[0].addBottomCountForCompilation(counter, self)
        }
        @osrExits = {}
        json["osrExits"].each {
            | subJson |
            osrExit = OSRExit.new(self, originStackFromJSON(subJson["origin"]),
                                  json["osrExitSites"][subJson["id"]].map {
                                      | value |
                                      value.hex
                                  }, subJson["exitKind"], subJson["isWatchpoint"],
                                  subJson["count"])
            osrExit.codeAddresses.each {
                | codeAddress |
                osrExits[codeAddress] = [] unless osrExits[codeAddress]
                osrExits[codeAddress] << osrExit
            }
            osrExit.origin[-1].osrExits << osrExit
        }
        @profiledBytecodes = []
        json["profiledBytecodes"].each {
            | subJson |
            @profiledBytecodes << ProfiledBytecodes.new(subJson)
        }
        @numInlinedGetByIds = json["numInlinedGetByIds"]
        @numInlinedPutByIds = json["numInlinedPutByIds"]
        @numInlinedCalls = json["numInlinedCalls"]
        @jettisonReason = json["jettisonReason"]
    end
    
    def codeHashSortKey
        bytecode.codeHashSortKey + "-" + compilationIndex.to_s
    end
    
    def counter(origin)
        @counters[origin]
    end
    
    def totalCount
        sum = 0
        @counters.values.each {
            | value |
            sum += value.count
        }
        sum
    end
    
    def maxCount
        max = 0
        @counters.values.each {
            | value |
            max = [max, value.count].max
        }
        max
    end
    
    def to_s
        "#{bytecode}-#{compilationIndex}-#{engine}"
    end
end

class DescriptionLine
    attr_reader :actualCountsString, :sourceCountsString, :disassembly, :shouldShow
    
    def initialize(actualCountsString, sourceCountsString, disassembly, shouldShow)
        @actualCountsString = actualCountsString
        @sourceCountsString = sourceCountsString
        @disassembly = disassembly
        @shouldShow = shouldShow
    end
    
    def codeAddress
        if @disassembly =~ /^\s*(0x[0-9a-fA-F]+):/
            $1.hex
        else
            nil
        end
    end
end

def originToPrintStack(origin)
    (0...(origin.size - 1)).map {
        | index |
        "bc\##{origin[index].bytecodeIndex} --> #{origin[index + 1].bytecodes}"
    }
end

def originToString(origin)
    (originToPrintStack(origin) + ["bc\##{origin[-1].bytecodeIndex}"]).join(" ")
end

if ARGV.length != 1
    $stderr.puts "Usage: display-profiler-output <path to profiler output file>"
    $stderr.puts
    $stderr.puts "The typical usage pattern for the profiler currently looks something like:"
    $stderr.puts
    $stderr.puts "Path/To/jsc -p profile.json myprogram.js"
    $stderr.puts "display-profiler-output profile.json"
    exit 1
end

$json = JSON::parse(IO::read(ARGV[0]))
$bytecodes = $json["bytecodes"].map {
    | subJson |
    Bytecodes.new(subJson)
}
$compilations = $json["compilations"].map {
    | subJson |
    Compilation.new(subJson)
}
$engines = ["Baseline", "DFG", "FTL", "FTLForOSREntry"]

def isOptimizing(engine)
    engine == "DFG" or engine == "FTL" or engine == "FTLForOSREntry"
end

$showCounts = true
$sortMode = :time

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 center(str, chars)
    while str.length < chars
        str += " "
        if str.length < chars
            str = " " + str
        end
    end
    str
end

def mayBeHash(hash)
    hash =~ /#/ or hash.size == 6
end

def sourceOnOneLine(source, limit)
    source.gsub(/\s+/, ' ')[0...limit]
end

def screenWidth
    if $stdin.tty?
        HighLine::SystemExtensions.terminal_size[0]
    else
        200
    end
end

def sortByMode(list)
    if list.size == 1
        return list
    end
    case $sortMode
    when :time
        list
    when :hash
        puts "Will sort output by code hash instead of compilation time."
        puts "Use 'sort time' to change back to the default."
        puts
        list.sort { | a, b | a.codeHashSortKey <=> b.codeHashSortKey }
    else
        raise
    end
end

def summary(mode)
    remaining = screenWidth
    
    # Figure out how many columns we need for the code block names, and for counts
    maxCount = 0
    maxName = 0
    $bytecodes.each {
        | bytecodes |
        maxCount = ([maxCount] + $engines.map {
                        | engine |
                        bytecodes.maxTopExecutionCount(engine)
                    } + $engines.map {
                        | engine |
                        bytecodes.maxBottomExecutionCount(engine)
                    }).max
        maxName = [bytecodes.to_s.size, maxName].max
    }
    maxCountDigits = maxCount.to_s.size
    
    hashCols = [[maxName, 30].min, "CodeBlock".size].max
    remaining -= hashCols + 1
    
    countCols = [maxCountDigits * $engines.size + ($engines.size - 1), "Source Counts".size].max
    remaining -= countCols + 1
    
    if mode == :full
        instructionCountCols = 6
        remaining -= instructionCountCols + 1
        
        machineCountCols = [maxCountDigits * $engines.size, "Machine Counts".size].max
        remaining -= machineCountCols + 1
        
        compilationsCols = 7
        remaining -= compilationsCols + 1
        
        inlinesCols = 9
        remaining -= inlinesCols + 1
        
        exitCountCols = 7
        remaining -= exitCountCols + 1
        
        recentOptsCols = 12
        remaining -= recentOptsCols + 1
    end
    
    if remaining > 0
        sourceCols = remaining
    else
        sourceCols = nil
    end
    
    print(center("CodeBlock", hashCols))
    if mode == :full
        print(" " + center("#Instr", instructionCountCols))
    end
    print(" " + center("Source Counts", countCols))
    if mode == :full
        print(" " + center("Machine Counts", machineCountCols))
        print(" " + center("#Compil", compilationsCols))
        print(" " + center("Inlines", inlinesCols))
        print(" " + center("#Exits", exitCountCols))
        print(" " + center("Last Opts", recentOptsCols))
    end
    if sourceCols
        print(" " + center("Source", sourceCols))
    end
    puts
    
    print(center("", hashCols))
    if mode == :full
        print(" " + (" " * instructionCountCols))
    end
    print(" " + center("Base/DFG/FTL/FTLOSR", countCols))
    if mode == :full
        print(" " + center("Base/DFG/FTL/FTLOSR", machineCountCols))
        print(" " + (" " * compilationsCols))
        print(" " + center("Src/Total", inlinesCols))
        print(" " + (" " * exitCountCols))
        print(" " + center("Get/Put/Call", recentOptsCols))
    end
    puts
    $bytecodes.sort {
        | a, b |
        b.totalMaxTopExecutionCount <=> a.totalMaxTopExecutionCount
    }.each {
        | bytecode |
        print(center(bytecode.name(hashCols), hashCols))
        if mode == :full
            print(" " + center(bytecode.instructionCount.to_s, instructionCountCols))
        end
        print(" " +
              center($engines.map {
                         | engine |
                         bytecode.maxTopExecutionCount(engine).to_s
                     }.join("/"), countCols))
        if mode == :full
            print(" " + center($engines.map {
                                   | engine |
                                   bytecode.maxBottomExecutionCount(engine).to_s
                               }.join("/"), machineCountCols))
            print(" " + center(bytecode.compilations.size.to_s, compilationsCols))
            print(" " + center(bytecode.sourceMachineInlineSites.to_s + "/" + bytecode.totalMachineInlineSites.to_s, inlinesCols))
            print(" " + center(bytecode.totalExitCount.to_s, exitCountCols))
            lastCompilation = bytecode.compilations[-1]
            if lastCompilation
                optData = [lastCompilation.numInlinedGetByIds,
                           lastCompilation.numInlinedPutByIds,
                           lastCompilation.numInlinedCalls]
            else
                optData = ["N/A"]
            end
            print(" " + center(optData.join('/'), recentOptsCols))
        end
        if sourceCols
            print(" " + sourceOnOneLine(bytecode.source, sourceCols))
        end
        puts
    }
end

def queryCompilations(command, args)
    compilationIndex = nil
    
    case args.length
    when 1
        hash = args[0]
        engine = nil
    when 2
        if mayBeHash(args[0])
            hash = args[0]
            engine = args[1]
        else
            engine = args[0]
            hash = args[1]
        end
    else
        puts "Usage: #{command} <code block hash> <engine>"
        return
    end
    
    if hash and hash =~ /-(-?[0-9]+)-/
        hash = $~.pre_match
        engine = $~.post_match
        compilationIndex = $1.to_i
    end
    
    if engine and not $engines.index(engine)
        pattern = Regexp.new(Regexp.escape(engine), "i")
        trueEngine = nil
        $engines.each {
            | myEngine |
            if myEngine =~ pattern
                trueEngine = myEngine
                break
            end
        }
        unless trueEngine
            puts "#{engine} is not a valid engine, try #{$engines.join(' or ')}."
            return
        end
        engine = trueEngine
    end
    
    if hash == "*"
        hash = nil
    end
    
    sortByMode($compilations).each {
        | compilation |
        next if hash and not compilation.bytecode.matches(hash)
        next if engine and compilation.engine != engine
        if compilationIndex
            if compilationIndex < 0
                next unless compilation.bytecode.compilations[compilationIndex] == compilation
            else
                next unless compilation.compilationIndex == compilationIndex
            end
        end
        
        yield compilation
    }
end

def executeCommand(*commandArray)
    command = commandArray[0]
    args = commandArray[1..-1]
    case command
    when "help", "h", "?"
        puts "summary (s)     Print a summary of code block execution rates."
        puts "full (f)        Same as summary, but prints more information."
        puts "source          Show the source for a code block."
        puts "bytecode (b)    Show the bytecode for a code block, with counts."
        puts "profiling (p)   Show the (internal) profiling data for a code block."
        puts "log (l)         List the compilations, exits, and jettisons involving this code block."
        puts "display (d)     Display details for a code block."
        puts "inlines         Show all inlining stacks that the code block was on."
        puts "counts          Set whether to show counts for 'bytecode' and 'display'."
        puts "sort            Set how to sort compilations before display."
        puts "help (h)        Print this message."
        puts "quit (q)        Quit."
    when "quit", "q", "exit"
        exit 0
    when "summary", "s"
        summary(:summary)
    when "full", "f"
        summary(:full)
    when "source"
        if args.length != 1
            puts "Usage: source <code block hash>"
            return
        end
        $bytecodes.each {
            | bytecode |
            if bytecode.matches(args[0])
                puts bytecode.source
            end
        }
    when "bytecode", "b"
        if args.length != 1
            puts "Usage: source <code block hash>"
            return
        end
        
        hash = args[0]
        
        countCols = 10 * $engines.size
        machineCols = 10 * $engines.size
        pad = 1
        while (countCols + 1 + machineCols + pad) % 8 != 0
            pad += 1
        end
        
        sortByMode($bytecodes).each {
            | bytecodes |
            next unless bytecodes.matches(hash)
            if $showCounts
                puts(center("Source Counts", countCols) + " " + center("Machine Counts", machineCols) +
                     (" " * pad) + center("Bytecode for #{bytecodes}", screenWidth - pad - countCols - 1 - machineCols))
                puts(center("Base/DFG/FTL/FTLOSR", countCols) + " " + center("Base/DFG/FTL/FTLOSR", countCols))
            else
                puts("Bytecode for #{bytecodes}:")
            end
            bytecodes.each {
                | bytecode |
                if $showCounts
                    if bytecode.shouldHaveCounts?
                        countsString = $engines.map {
                            | myEngine |
                            bytecode.topExecutionCount(myEngine)
                        }.join("/")
                        machineString = $engines.map {
                            | myEngine |
                            bytecode.bottomExecutionCount(myEngine)
                        }.join("/")
                    else
                        countsString = ""
                        machineString = ""
                    end
                    print(center(countsString, countCols) + " " + center(machineString, machineCols) + (" " * pad))
                end
                puts(bytecode.description.chomp)
                bytecode.osrExits.each {
                    | exit |
                    if $showCounts
                        print(center("!!!!!", countCols) + " " + center("!!!!!", machineCols) + (" " * pad))
                    end
                    print(" " * 10)
                    puts("EXIT: in #{exit.compilation} due to #{exit.exitKind}, #{exit.count} times")
                }
            }
        }
    when "profiling", "p"
        if args.length != 1
            puts "Usage: profiling <code block hash>"
            return
        end
        
        hash = args[0]
        
        first = true
        sortByMode($compilations).each {
            | compilation |
            
            compilation.profiledBytecodes.each {
                | profiledBytecodes |
                if profiledBytecodes.bytecodes.matches(hash)
                    if first
                        first = false
                    else
                        puts
                    end
                    
                    puts "Compilation #{compilation}:"
                    profiledBytecodes.header.each {
                        | header |
                        puts(" " * 6 + header)
                    }
                    profiledBytecodes.each {
                        | bytecode |
                        puts(" " * 8 + bytecode.description)
                        profiledBytecodes.bytecodes.bytecode(bytecode.bytecodeIndex).osrExits.each {
                            | exit |
                            if exit.compilation == compilation
                                puts(" !!!!!           EXIT: due to #{exit.exitKind}, #{exit.count} times")
                            end
                        }
                    }
                end
            }
        }
    when "log", "l"
        queryCompilations("log", args) {
            | compilation |
            puts "Compilation #{compilation}:"
            puts "    Total count: #{compilation.totalCount}  Max count: #{compilation.maxCount}"
            compilation.osrExits.values.each {
                | exits |
                exits.each {
                    | exit |
                    puts "    EXIT: at #{originToString(exit.origin)} due to #{exit.exitKind}, #{exit.count} times"
                }
            }
            if compilation.jettisonReason != "NotJettisoned"
                puts "    Jettisoned due to #{compilation.jettisonReason}"
            end
        }
    when "inlines"
        if args.length != 1
            puts "Usage: inlines <code block hash>"
            return
        end
        
        hash = args[0]
        
        sortByMode($bytecodes).each {
            | bytecodes |
            next unless bytecodes.matches(hash)
            
            # FIXME: print something useful to say more about which code block this is.
            
            $compilations.each {
                | compilation |
                myOrigins = []
                compilation.descriptions.each {
                    | description |
                    if description.origin.index {
                            | myBytecode |
                            bytecodes == myBytecode.bytecodes
                        }
                        myOrigins << description.origin
                    end
                }
                myOrigins.uniq!
                myOrigins.sort! {
                    | a, b |
                    result = 0
                    [a.size, b.size].min.times {
                        | index |
                        result = a[index].bytecodeIndex <=> b[index].bytecodeIndex
                        break if result != 0
                    }
                    result
                }
                
                next if myOrigins.empty?

                printArray = []
                lastPrintStack = []
                
                def printStack(printArray, stack, lastStack)
                    stillCommon = true
                    stack.each_with_index {
                        | entry, index |
                        next if stillCommon and entry == lastStack[index]
                        printArray << ("    " * (index + 1) + entry)
                        stillCommon = false
                    }
                end
                
                myOrigins.each {
                    | origin |
                    currentPrintStack = originToPrintStack(origin)
                    printStack(printArray, currentPrintStack, lastPrintStack)
                    lastPrintStack = currentPrintStack
                }

                next if printArray.empty?
                
                puts "Compilation #{compilation}:"
                printArray.each {
                    | entry |
                    puts entry
                }
            }
        }
    when "display", "d"
        actualCountCols = 13
        sourceCountCols = 10 * $engines.size
        
        first = true

        queryCompilations("display", args) {
            | compilation |
            
            if first
                first = false
            else
                puts
            end
            
            puts("Compilation #{compilation}:")
            if $showCounts
                puts(center("Actual Counts", actualCountCols) + " " + center("Source Counts", sourceCountCols) + " " + center("Disassembly in #{compilation.engine}", screenWidth - 1 - sourceCountCols - 1 - actualCountCols))
                puts((" " * actualCountCols) + " " + center("Base/DFG/FTL/FTLOSR", sourceCountCols))
            else
                puts("Disassembly in #{compilation.engine}")
            end
                
            lines = []

            compilation.descriptions.each {
                | description |
                # FIXME: We should have a better way of detecting things like CountExecution nodes
                # and slow path entries in the baseline JIT.
                if description.description =~ /CountExecution\(/ and isOptimizing(compilation.engine)
                    shouldShow = false
                else
                    shouldShow = true
                end
                if not description.origin.empty? and not description.origin[-1]
                    p description.origin
                    p description.description
                end
                if description.origin.empty? or not description.origin[-1].shouldHaveCounts? or (compilation.engine == "Baseline" and description.description =~ /^\s*\(S\)/)
                    actualCountsString = ""
                    sourceCountsString = ""
                else
                    actualCountsString = compilation.counter(description.origin).count.to_s
                    sourceCountsString = $engines.map {
                        | myEngine |
                        description.origin[-1].topExecutionCount(myEngine)
                    }.join("/")
                end
                description.description.split("\n").each {
                    | line |
                    lines << DescriptionLine.new(actualCountsString, sourceCountsString, line.chomp, shouldShow)
                }
            }
            
            exitPrefix = ""
            if $showCounts
                exitPrefix += center("!!!!!", actualCountCols) + " " + center("!!!!!", sourceCountCols) + (" " * 15)
            else
                exitPrefix += "   !!!!!"
            end
            exitPrefix += " " * 10

            lines.each_with_index {
                | line, index |
                codeAddress = line.codeAddress
                if codeAddress
                    list = compilation.osrExits[codeAddress]
                    if list
                        list.each {
                            | exit |
                            if exit.isWatchpoint
                                exit.dumpForDisplay(exitPrefix)
                            end
                        }
                    end
                end
                if line.shouldShow
                    if $showCounts
                        print(center(line.actualCountsString, actualCountCols) + " " + center(line.sourceCountsString, sourceCountCols) + " ")
                    end
                    puts(line.disassembly)
                end
                if codeAddress
                    # Find the next disassembly address.
                    endIndex = index + 1
                    endAddress = nil
                    while endIndex < lines.size
                        myAddress = lines[endIndex].codeAddress
                        if myAddress
                            endAddress = myAddress
                            break
                        end
                        endIndex += 1
                    end
                    
                    if endAddress
                        list = compilation.osrExits[endAddress]
                        if list
                            list.each {
                                | exit |
                                unless exit.isWatchpoint
                                    exit.dumpForDisplay(exitPrefix)
                                end
                            }
                        end
                    end
                end
            }
        }
    when "counts"
        if args.length != 1
            puts "Usage: counts on|off|toggle"
        else
            case args[0].downcase
            when 'on'
                $showCounts = true
            when 'off'
                $showCounts = false
            when 'toggle'
                $showCounts = !$showCounts
            else
                puts "Usage: counts on|off|toggle"
            end
        end
        puts "Current value: #{$showCounts ? 'on' : 'off'}"
    when "sort"
        if args.length != 1
            puts "Usage: sort time|hash"
            puts
            puts "sort time: Sorts by the timestamp of when the code was compiled."
            puts "           This is the default."
            puts
            puts "sort hash: Sorts by the code hash. This is more deterministic,"
            puts "           and is useful for diffs."
            puts
        else
            case args[0].downcase
            when 'time'
                $sortMode = :time
            when 'hash'
                $sortMode = :hash
            else
                puts "Usage: sort time|hash"
            end
        end
        puts "Current value: #{$sortMode}"
    else
        puts "Invalid command: #{command}"
    end
end

if $stdin.tty?
    executeCommand("full")
end

while commandLine = Readline.readline("> ", true)
    executeCommand(*commandLine.split)
end

