3 # Copyright (C) 2012, 2013, 2014 Apple Inc. All rights reserved.
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
8 # 1. Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # 2. Redistributions in binary form must reproduce the above copyright
11 # notice, this list of conditions and the following disclaimer in the
12 # documentation and/or other materials provided with the distribution.
14 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
15 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
16 # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
18 # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
24 # THE POSSIBILITY OF SUCH DAMAGE.
34 $stderr.puts "Error: some required gems are not installed!"
36 $stderr.puts "Try running:"
38 $stderr.puts "sudo gem install json"
39 $stderr.puts "sudo gem install highline"
44 attr_accessor :bytecodes, :bytecodeIndex, :opcode, :description, :topCounts, :bottomCounts, :machineInlinees, :osrExits
46 def initialize(bytecodes, bytecodeIndex, opcode, description)
47 @bytecodes = bytecodes
48 @bytecodeIndex = bytecodeIndex
50 @description = description
51 @topCounts = [] # "source" counts
52 @bottomCounts = {} # "machine" counts, maps compilations to counts
53 @machineInlinees = {} # maps my compilation to a set of inlinees
58 @opcode != "op_call_put_result"
61 def addTopCount(count)
65 def addBottomCountForCompilation(count, compilation)
66 @bottomCounts[compilation] = [] unless @bottomCounts[compilation]
67 @bottomCounts[compilation] << count
70 def addMachineInlinee(compilation, inlinee)
71 @machineInlinees[compilation] = {} unless @machineInlinees[compilation]
72 @machineInlinees[compilation][inlinee] = true
75 def totalTopExecutionCount
84 def topExecutionCount(engine)
88 if value.engine == engine
95 def totalBottomExecutionCount
97 @bottomCounts.each_value {
102 max = [max, value.count].max
109 def bottomExecutionCount(engine)
111 @bottomCounts.each_pair {
112 | compilation, counts |
113 if compilation.engine == engine
117 max = [max, value.count].max
136 attr_accessor :codeHash, :inferredName, :source, :instructionCount, :machineInlineSites, :compilations
139 @codeHash = json["hash"].to_s
140 @inferredName = json["inferredName"].to_s
141 @source = json["sourceCode"].to_s
142 @instructionCount = json["instructionCount"].to_i
144 json["bytecode"].each {
146 index = subJson["bytecodeIndex"].to_i
147 @bytecode[index] = Bytecode.new(self, index, subJson["opcode"].to_s, subJson["description"].to_s)
149 @machineInlineSites = {} # maps compilation to a set of origins
162 "#{@inferredName}\##{@codeHash}"
167 $~.post_match == @codeHash
171 pattern == @inferredName or pattern == @codeHash
176 @bytecode.values.sort{|a, b| a.bytecodeIndex <=> b.bytecodeIndex}.each {
182 def bytecode(bytecodeIndex)
183 @bytecode[bytecodeIndex]
186 def addMachineInlineSite(compilation, origin)
187 @machineInlineSites[compilation] = {} unless @machineInlineSites[compilation]
188 @machineInlineSites[compilation][origin] = true
191 def totalMachineInlineSites
193 @machineInlineSites.each_value {
200 def sourceMachineInlineSites
202 @machineInlineSites.each_value {
209 def totalMaxTopExecutionCount
211 @bytecode.each_value {
213 max = [max, bytecode.totalTopExecutionCount].max
218 def maxTopExecutionCount(engine)
220 @bytecode.each_value {
222 max = [max, bytecode.topExecutionCount(engine)].max
227 def totalMaxBottomExecutionCount
229 @bytecode.each_value {
231 max = [max, bytecode.totalBottomExecutionCount].max
236 def maxBottomExecutionCount(engine)
238 @bytecode.each_value {
240 max = [max, bytecode.bottomExecutionCount(engine)].max
249 sum += bytecode.totalExitCount
259 class ProfiledBytecode
260 attr_reader :bytecodeIndex, :description
263 @bytecodeIndex = json["bytecodeIndex"].to_i
264 @description = json["description"].to_s
268 class ProfiledBytecodes
269 attr_reader :header, :bytecodes
272 @header = json["header"]
273 @bytecodes = $bytecodes[json["bytecodesID"].to_i]
274 @sequence = json["bytecode"].map {
276 ProfiledBytecode.new(subJson)
288 def originStackFromJSON(json)
291 $bytecodes[subJson["bytecodesID"].to_i].bytecode(subJson["bytecodeIndex"].to_i)
295 class CompiledBytecode
296 attr_accessor :origin, :description
299 @origin = originStackFromJSON(json["origin"])
300 @description = json["description"].to_s
304 class ExecutionCounter
305 attr_accessor :origin, :engine, :count
307 def initialize(origin, engine, count)
315 attr_reader :compilation, :origin, :codeAddresses, :exitKind, :isWatchpoint, :count
317 def initialize(compilation, origin, codeAddresses, exitKind, isWatchpoint, count)
318 @compilation = compilation
320 @codeAddresses = codeAddresses
322 @isWatchpoint = isWatchpoint
326 def dumpForDisplay(prefix)
327 puts(prefix + "EXIT: due to #{@exitKind}, #{@count} times")
332 attr_accessor :bytecode, :engine, :descriptions, :counters, :compilationIndex
333 attr_accessor :osrExits, :profiledBytecodes, :numInlinedGetByIds, :numInlinedPutByIds
334 attr_accessor :numInlinedCalls, :jettisonReason, :additionalJettisonReason
337 @bytecode = $bytecodes[json["bytecodesID"].to_i]
338 @bytecode.compilations << self
339 @compilationIndex = @bytecode.compilations.size
340 @engine = json["compilationKind"]
341 @descriptions = json["descriptions"].map {
343 CompiledBytecode.new(subJson)
347 next if description.origin.empty?
348 description.origin[1..-1].each_with_index {
350 description.origin[0].addMachineInlinee(self, inlinee.bytecodes)
351 inlinee.bytecodes.addMachineInlineSite(self, description.origin[0...index])
355 json["counters"].each {
357 origin = originStackFromJSON(subJson["origin"])
358 counter = ExecutionCounter.new(origin, @engine, subJson["executionCount"].to_i)
359 @counters[origin] = counter
360 origin[-1].addTopCount(counter)
361 origin[0].addBottomCountForCompilation(counter, self)
364 json["osrExits"].each {
366 osrExit = OSRExit.new(self, originStackFromJSON(subJson["origin"]),
367 json["osrExitSites"][subJson["id"]].map {
370 }, subJson["exitKind"], subJson["isWatchpoint"],
372 osrExit.codeAddresses.each {
374 osrExits[codeAddress] = [] unless osrExits[codeAddress]
375 osrExits[codeAddress] << osrExit
377 osrExit.origin[-1].osrExits << osrExit
379 @profiledBytecodes = []
380 json["profiledBytecodes"].each {
382 @profiledBytecodes << ProfiledBytecodes.new(subJson)
384 @numInlinedGetByIds = json["numInlinedGetByIds"]
385 @numInlinedPutByIds = json["numInlinedPutByIds"]
386 @numInlinedCalls = json["numInlinedCalls"]
387 @jettisonReason = json["jettisonReason"]
388 @additionalJettisonReason = json["additionalJettisonReason"]
392 bytecode.codeHashSortKey + "-" + compilationIndex.to_s
401 @counters.values.each {
410 @counters.values.each {
412 max = [max, value.count].max
418 "#{bytecode}-#{compilationIndex}-#{engine}"
422 class DescriptionLine
423 attr_reader :actualCountsString, :sourceCountsString, :disassembly, :shouldShow
425 def initialize(actualCountsString, sourceCountsString, disassembly, shouldShow)
426 @actualCountsString = actualCountsString
427 @sourceCountsString = sourceCountsString
428 @disassembly = disassembly
429 @shouldShow = shouldShow
433 if @disassembly =~ /^\s*(0x[0-9a-fA-F]+):/
441 def originToPrintStack(origin)
442 (0...(origin.size - 1)).map {
444 "bc\##{origin[index].bytecodeIndex} --> #{origin[index + 1].bytecodes}"
448 def originToString(origin)
449 (originToPrintStack(origin) + ["bc\##{origin[-1].bytecodeIndex}"]).join(" ")
453 $stderr.puts "Usage: display-profiler-output <path to profiler output file>"
455 $stderr.puts "The typical usage pattern for the profiler currently looks something like:"
457 $stderr.puts "Path/To/jsc -p profile.json myprogram.js"
458 $stderr.puts "display-profiler-output profile.json"
462 $json = JSON::parse(IO::read(ARGV[0]))
463 $bytecodes = $json["bytecodes"].map {
465 Bytecodes.new(subJson)
467 $compilations = $json["compilations"].map {
469 Compilation.new(subJson)
471 $engines = ["Baseline", "DFG", "FTL", "FTLForOSREntry"]
473 def isOptimizing(engine)
474 engine == "DFG" or engine == "FTL" or engine == "FTLForOSREntry"
489 while str.length < chars
495 def center(str, chars)
496 while str.length < chars
498 if str.length < chars
506 hash =~ /#/ or hash.size == 6
509 def sourceOnOneLine(source, limit)
510 source.gsub(/\s+/, ' ')[0...limit]
515 HighLine::SystemExtensions.terminal_size[0]
529 puts "Will sort output by code hash instead of compilation time."
530 puts "Use 'sort time' to change back to the default."
532 list.sort { | a, b | a.codeHashSortKey <=> b.codeHashSortKey }
538 def summary(mode, order)
539 remaining = screenWidth
541 # Figure out how many columns we need for the code block names, and for counts
546 maxCount = ([maxCount] + $engines.map {
548 bytecodes.maxTopExecutionCount(engine)
551 bytecodes.maxBottomExecutionCount(engine)
553 maxName = [bytecodes.to_s.size, maxName].max
555 maxCountDigits = maxCount.to_s.size
557 hashCols = [[maxName, 30].min, "CodeBlock".size].max
558 remaining -= hashCols + 1
560 countCols = [maxCountDigits * $engines.size + ($engines.size - 1), "Source Counts".size].max
561 remaining -= countCols + 1
564 instructionCountCols = 6
565 remaining -= instructionCountCols + 1
567 machineCountCols = [maxCountDigits * $engines.size, "Machine Counts".size].max
568 remaining -= machineCountCols + 1
571 remaining -= compilationsCols + 1
574 remaining -= inlinesCols + 1
577 remaining -= exitCountCols + 1
580 remaining -= recentOptsCols + 1
584 sourceCols = remaining
589 print(center("CodeBlock", hashCols))
591 print(" " + center("#Instr", instructionCountCols))
593 print(" " + center("Source Counts", countCols))
595 print(" " + center("Machine Counts", machineCountCols))
596 print(" " + center("#Compil", compilationsCols))
597 print(" " + center("Inlines", inlinesCols))
598 print(" " + center("#Exits", exitCountCols))
599 print(" " + center("Last Opts", recentOptsCols))
602 print(" " + center("Source", sourceCols))
606 print(center("", hashCols))
608 print(" " + (" " * instructionCountCols))
610 print(" " + center("Base/DFG/FTL/FTLOSR", countCols))
612 print(" " + center("Base/DFG/FTL/FTLOSR", machineCountCols))
613 print(" " + (" " * compilationsCols))
614 print(" " + center("Src/Total", inlinesCols))
615 print(" " + (" " * exitCountCols))
616 print(" " + center("Get/Put/Call", recentOptsCols))
623 b.totalMaxTopExecutionCount <=> a.totalMaxTopExecutionCount
625 b.totalMaxBottomExecutionCount <=> a.totalMaxBottomExecutionCount
631 print(center(bytecode.name(hashCols), hashCols))
633 print(" " + center(bytecode.instructionCount.to_s, instructionCountCols))
636 center($engines.map {
638 bytecode.maxTopExecutionCount(engine).to_s
639 }.join("/"), countCols))
641 print(" " + center($engines.map {
643 bytecode.maxBottomExecutionCount(engine).to_s
644 }.join("/"), machineCountCols))
645 print(" " + center(bytecode.compilations.size.to_s, compilationsCols))
646 print(" " + center(bytecode.sourceMachineInlineSites.to_s + "/" + bytecode.totalMachineInlineSites.to_s, inlinesCols))
647 print(" " + center(bytecode.totalExitCount.to_s, exitCountCols))
648 lastCompilation = bytecode.compilations[-1]
650 optData = [lastCompilation.numInlinedGetByIds,
651 lastCompilation.numInlinedPutByIds,
652 lastCompilation.numInlinedCalls]
656 print(" " + center(optData.join('/'), recentOptsCols))
659 print(" " + sourceOnOneLine(bytecode.source, sourceCols))
665 def queryCompilations(command, args)
666 compilationIndex = nil
673 if mayBeHash(args[0])
681 puts "Usage: #{command} <code block hash> <engine>"
685 if hash and hash =~ /-(-?[0-9]+)-/
687 engine = $~.post_match
688 compilationIndex = $1.to_i
691 if engine and not $engines.index(engine)
692 pattern = Regexp.new(Regexp.escape(engine), "i")
696 if myEngine =~ pattern
697 trueEngine = myEngine
702 puts "#{engine} is not a valid engine, try #{$engines.join(' or ')}."
712 sortByMode($compilations).each {
714 next if hash and not compilation.bytecode.matches(hash)
715 next if engine and compilation.engine != engine
717 if compilationIndex < 0
718 next unless compilation.bytecode.compilations[compilationIndex] == compilation
720 next unless compilation.compilationIndex == compilationIndex
728 def executeCommand(*commandArray)
729 command = commandArray[0]
730 args = commandArray[1..-1]
732 when "help", "h", "?"
733 puts "summary (s) Print a summary of code block execution rates."
734 puts "full (f) Same as summary, but prints more information."
735 puts "source Show the source for a code block."
736 puts "bytecode (b) Show the bytecode for a code block, with counts."
737 puts "profiling (p) Show the (internal) profiling data for a code block."
738 puts "log (l) List the compilations, exits, and jettisons involving this code block."
739 puts "display (d) Display details for a code block."
740 puts "inlines Show all inlining stacks that the code block was on."
741 puts "counts Set whether to show counts for 'bytecode' and 'display'."
742 puts "sort Set how to sort compilations before display."
743 puts "help (h) Print this message."
744 puts "quit (q) Quit."
745 when "quit", "q", "exit"
748 summary(:summary, :bytecode)
750 if args[0] and (args[0] == "m" or args[0] == "machine")
751 summary(:full, :machine)
753 summary(:full, :bytecode)
757 puts "Usage: source <code block hash>"
762 if bytecode.matches(args[0])
768 puts "Usage: source <code block hash>"
774 countCols = 10 * $engines.size
775 machineCols = 10 * $engines.size
777 while (countCols + 1 + machineCols + pad) % 8 != 0
781 sortByMode($bytecodes).each {
783 next unless bytecodes.matches(hash)
785 puts(center("Source Counts", countCols) + " " + center("Machine Counts", machineCols) +
786 (" " * pad) + center("Bytecode for #{bytecodes}", screenWidth - pad - countCols - 1 - machineCols))
787 puts(center("Base/DFG/FTL/FTLOSR", countCols) + " " + center("Base/DFG/FTL/FTLOSR", countCols))
789 puts("Bytecode for #{bytecodes}:")
794 if bytecode.shouldHaveCounts?
795 countsString = $engines.map {
797 bytecode.topExecutionCount(myEngine)
799 machineString = $engines.map {
801 bytecode.bottomExecutionCount(myEngine)
807 print(center(countsString, countCols) + " " + center(machineString, machineCols) + (" " * pad))
809 puts(bytecode.description.chomp)
810 bytecode.osrExits.each {
813 print(center("!!!!!", countCols) + " " + center("!!!!!", machineCols) + (" " * pad))
816 puts("EXIT: in #{exit.compilation} due to #{exit.exitKind}, #{exit.count} times")
820 when "profiling", "p"
822 puts "Usage: profiling <code block hash>"
829 sortByMode($compilations).each {
832 compilation.profiledBytecodes.each {
833 | profiledBytecodes |
834 if profiledBytecodes.bytecodes.matches(hash)
841 puts "Compilation #{compilation}:"
842 profiledBytecodes.header.each {
844 puts(" " * 6 + header)
846 profiledBytecodes.each {
848 puts(" " * 8 + bytecode.description)
849 profiledBytecodes.bytecodes.bytecode(bytecode.bytecodeIndex).osrExits.each {
851 if exit.compilation == compilation
852 puts(" !!!!! EXIT: due to #{exit.exitKind}, #{exit.count} times")
860 queryCompilations("log", args) {
862 puts "Compilation #{compilation}:"
863 puts " Total count: #{compilation.totalCount} Max count: #{compilation.maxCount}"
864 compilation.osrExits.values.each {
868 puts " EXIT: at #{originToString(exit.origin)} due to #{exit.exitKind}, #{exit.count} times"
871 if compilation.jettisonReason != "NotJettisoned"
872 puts " Jettisoned due to #{compilation.jettisonReason}"
873 if compilation.additionalJettisonReason
874 puts " #{compilation.additionalJettisonReason}"
880 puts "Usage: inlines <code block hash>"
886 sortByMode($bytecodes).each {
888 next unless bytecodes.matches(hash)
890 # FIXME: print something useful to say more about which code block this is.
895 compilation.descriptions.each {
897 if description.origin.index {
899 bytecodes == myBytecode.bytecodes
901 myOrigins << description.origin
908 [a.size, b.size].min.times {
910 result = a[index].bytecodeIndex <=> b[index].bytecodeIndex
916 next if myOrigins.empty?
921 def printStack(printArray, stack, lastStack)
923 stack.each_with_index {
925 next if stillCommon and entry == lastStack[index]
926 printArray << (" " * (index + 1) + entry)
933 currentPrintStack = originToPrintStack(origin)
934 printStack(printArray, currentPrintStack, lastPrintStack)
935 lastPrintStack = currentPrintStack
938 next if printArray.empty?
940 puts "Compilation #{compilation}:"
949 sourceCountCols = 10 * $engines.size
953 queryCompilations("display", args) {
962 puts("Compilation #{compilation}:")
964 puts(center("Actual Counts", actualCountCols) + " " + center("Source Counts", sourceCountCols) + " " + center("Disassembly in #{compilation.engine}", screenWidth - 1 - sourceCountCols - 1 - actualCountCols))
965 puts((" " * actualCountCols) + " " + center("Base/DFG/FTL/FTLOSR", sourceCountCols))
967 puts("Disassembly in #{compilation.engine}")
972 compilation.descriptions.each {
974 # FIXME: We should have a better way of detecting things like CountExecution nodes
975 # and slow path entries in the baseline JIT.
976 if description.description =~ /CountExecution\(/ and isOptimizing(compilation.engine)
981 if not description.origin.empty? and not description.origin[-1]
983 p description.description
985 if description.origin.empty? or not description.origin[-1].shouldHaveCounts? or (compilation.engine == "Baseline" and description.description =~ /^\s*\(S\)/)
986 actualCountsString = ""
987 sourceCountsString = ""
989 actualCountsString = compilation.counter(description.origin).count.to_s
990 sourceCountsString = $engines.map {
992 description.origin[-1].topExecutionCount(myEngine)
995 description.description.split("\n").each {
997 lines << DescriptionLine.new(actualCountsString, sourceCountsString, line.chomp, shouldShow)
1003 exitPrefix += center("!!!!!", actualCountCols) + " " + center("!!!!!", sourceCountCols) + (" " * 15)
1005 exitPrefix += " !!!!!"
1007 exitPrefix += " " * 10
1009 lines.each_with_index {
1011 codeAddress = line.codeAddress
1013 list = compilation.osrExits[codeAddress]
1017 if exit.isWatchpoint
1018 exit.dumpForDisplay(exitPrefix)
1025 print(center(line.actualCountsString, actualCountCols) + " " + center(line.sourceCountsString, sourceCountCols) + " ")
1027 puts(line.disassembly)
1030 # Find the next disassembly address.
1031 endIndex = index + 1
1033 while endIndex < lines.size
1034 myAddress = lines[endIndex].codeAddress
1036 endAddress = myAddress
1043 list = compilation.osrExits[endAddress]
1047 unless exit.isWatchpoint
1048 exit.dumpForDisplay(exitPrefix)
1058 puts "Usage: counts on|off|toggle"
1060 case args[0].downcase
1066 $showCounts = !$showCounts
1068 puts "Usage: counts on|off|toggle"
1071 puts "Current value: #{$showCounts ? 'on' : 'off'}"
1074 puts "Usage: sort time|hash"
1076 puts "sort time: Sorts by the timestamp of when the code was compiled."
1077 puts " This is the default."
1079 puts "sort hash: Sorts by the code hash. This is more deterministic,"
1080 puts " and is useful for diffs."
1083 case args[0].downcase
1089 puts "Usage: sort time|hash"
1092 puts "Current value: #{$sortMode}"
1094 puts "Invalid command: #{command}"
1099 executeCommand("full")
1102 while commandLine = Readline.readline("> ", true)
1103 executeCommand(*commandLine.split)