1#! /usr/bin/python
2#
3# Copyright 2016 the V8 project authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6#
7
8import argparse
9import heapq
10import json
11from matplotlib import colors
12from matplotlib import pyplot
13import numpy
14import struct
15import sys
16
17
18__DESCRIPTION = """
19Process v8.ignition_dispatches_counters.json and list top counters,
20or plot a dispatch heatmap.
21
22Please note that those handlers that may not or will never dispatch
23(e.g. Return or Throw) do not show up in the results.
24"""
25
26
27__HELP_EPILOGUE = """
28examples:
29  # Print the hottest bytecodes in descending order, reading from
30  # default filename v8.ignition_dispatches_counters.json (default mode)
31  $ tools/ignition/bytecode_dispatches_report.py
32
33  # Print the hottest 15 bytecode dispatch pairs reading from data.json
34  $ tools/ignition/bytecode_dispatches_report.py -t -n 15 data.json
35
36  # Save heatmap to default filename v8.ignition_dispatches_counters.svg
37  $ tools/ignition/bytecode_dispatches_report.py -p
38
39  # Save heatmap to filename data.svg
40  $ tools/ignition/bytecode_dispatches_report.py -p -o data.svg
41
42  # Open the heatmap in an interactive viewer
43  $ tools/ignition/bytecode_dispatches_report.py -p -i
44
45  # Display the top 5 sources and destinations of dispatches to/from LdaZero
46  $ tools/ignition/bytecode_dispatches_report.py -f LdaZero -n 5
47"""
48
49__COUNTER_BITS = struct.calcsize("P") * 8  # Size in bits of a pointer
50__COUNTER_MAX = 2**__COUNTER_BITS - 1
51
52
53def warn_if_counter_may_have_saturated(dispatches_table):
54  for source, counters_from_source in iteritems(dispatches_table):
55    for destination, counter in iteritems(counters_from_source):
56      if counter == __COUNTER_MAX:
57        print "WARNING: {} -> {} may have saturated.".format(source,
58                                                             destination)
59
60
61def find_top_bytecode_dispatch_pairs(dispatches_table, top_count):
62  def flattened_counters_generator():
63    for source, counters_from_source in iteritems(dispatches_table):
64      for destination, counter in iteritems(counters_from_source):
65        yield source, destination, counter
66
67  return heapq.nlargest(top_count, flattened_counters_generator(),
68                        key=lambda x: x[2])
69
70
71def print_top_bytecode_dispatch_pairs(dispatches_table, top_count):
72  top_bytecode_dispatch_pairs = (
73    find_top_bytecode_dispatch_pairs(dispatches_table, top_count))
74  print "Top {} bytecode dispatch pairs:".format(top_count)
75  for source, destination, counter in top_bytecode_dispatch_pairs:
76    print "{:>12d}\t{} -> {}".format(counter, source, destination)
77
78
79def find_top_bytecodes(dispatches_table):
80  top_bytecodes = []
81  for bytecode, counters_from_bytecode in iteritems(dispatches_table):
82    top_bytecodes.append((bytecode, sum(itervalues(counters_from_bytecode))))
83
84  top_bytecodes.sort(key=lambda x: x[1], reverse=True)
85  return top_bytecodes
86
87
88def print_top_bytecodes(dispatches_table):
89  top_bytecodes = find_top_bytecodes(dispatches_table)
90  print "Top bytecodes:"
91  for bytecode, counter in top_bytecodes:
92    print "{:>12d}\t{}".format(counter, bytecode)
93
94
95def find_top_dispatch_sources_and_destinations(
96    dispatches_table, bytecode, top_count, sort_source_relative):
97  sources = []
98  for source, destinations in iteritems(dispatches_table):
99    total = float(sum(itervalues(destinations)))
100    if bytecode in destinations:
101      count = destinations[bytecode]
102      sources.append((source, count, count / total))
103
104  destinations = []
105  bytecode_destinations = dispatches_table[bytecode]
106  bytecode_total = float(sum(itervalues(bytecode_destinations)))
107  for destination, count in iteritems(bytecode_destinations):
108    destinations.append((destination, count, count / bytecode_total))
109
110  return (heapq.nlargest(top_count, sources,
111                         key=lambda x: x[2 if sort_source_relative else 1]),
112          heapq.nlargest(top_count, destinations, key=lambda x: x[1]))
113
114
115def print_top_dispatch_sources_and_destinations(dispatches_table, bytecode,
116                                                top_count, sort_relative):
117  top_sources, top_destinations = find_top_dispatch_sources_and_destinations(
118      dispatches_table, bytecode, top_count, sort_relative)
119  print "Top sources of dispatches to {}:".format(bytecode)
120  for source_name, counter, ratio in top_sources:
121    print "{:>12d}\t{:>5.1f}%\t{}".format(counter, ratio * 100, source_name)
122
123  print "\nTop destinations of dispatches from {}:".format(bytecode)
124  for destination_name, counter, ratio in top_destinations:
125    print "{:>12d}\t{:>5.1f}%\t{}".format(counter, ratio * 100, destination_name)
126
127
128def build_counters_matrix(dispatches_table):
129  labels = sorted(dispatches_table.keys())
130
131  counters_matrix = numpy.empty([len(labels), len(labels)], dtype=int)
132  for from_index, from_name in enumerate(labels):
133    current_row = dispatches_table[from_name];
134    for to_index, to_name in enumerate(labels):
135      counters_matrix[from_index, to_index] = current_row.get(to_name, 0)
136
137  # Reverse y axis for a nicer appearance
138  xlabels = labels
139  ylabels = list(reversed(xlabels))
140  counters_matrix = numpy.flipud(counters_matrix)
141
142  return counters_matrix, xlabels, ylabels
143
144
145def plot_dispatches_table(dispatches_table, figure, axis):
146  counters_matrix, xlabels, ylabels = build_counters_matrix(dispatches_table)
147
148  image = axis.pcolor(
149    counters_matrix,
150    cmap="jet",
151    norm=colors.LogNorm(),
152    edgecolor="grey",
153    linestyle="dotted",
154    linewidth=0.5
155  )
156
157  axis.xaxis.set(
158    ticks=numpy.arange(0.5, len(xlabels)),
159    label="From bytecode handler"
160  )
161  axis.xaxis.tick_top()
162  axis.set_xlim(0, len(xlabels))
163  axis.set_xticklabels(xlabels, rotation="vertical")
164
165  axis.yaxis.set(
166    ticks=numpy.arange(0.5, len(ylabels)),
167    label="To bytecode handler",
168    ticklabels=ylabels
169  )
170  axis.set_ylim(0, len(ylabels))
171
172  figure.colorbar(
173    image,
174    ax=axis,
175    fraction=0.01,
176    pad=0.01
177  )
178
179
180def parse_command_line():
181  command_line_parser = argparse.ArgumentParser(
182    formatter_class=argparse.RawDescriptionHelpFormatter,
183    description=__DESCRIPTION,
184    epilog=__HELP_EPILOGUE
185  )
186  command_line_parser.add_argument(
187    "--plot-size", "-s",
188    metavar="N",
189    default=30,
190    help="shorter side in inches of the output plot (default 30)"
191  )
192  command_line_parser.add_argument(
193    "--plot", "-p",
194    action="store_true",
195    help="plot dispatch pairs heatmap"
196  )
197  command_line_parser.add_argument(
198    "--interactive", "-i",
199    action="store_true",
200    help="open the heatmap in an interactive viewer, instead of writing to file"
201  )
202  command_line_parser.add_argument(
203    "--top-bytecode-dispatch-pairs", "-t",
204    action="store_true",
205    help="print the top bytecode dispatch pairs"
206  )
207  command_line_parser.add_argument(
208    "--top-entries-count", "-n",
209    metavar="N",
210    type=int,
211    default=10,
212    help="print N top entries when running with -t or -f (default 10)"
213  )
214  command_line_parser.add_argument(
215    "--top-dispatches-for-bytecode", "-f",
216    metavar="<bytecode name>",
217    help="print top dispatch sources and destinations to the specified bytecode"
218  )
219  command_line_parser.add_argument(
220    "--output-filename", "-o",
221    metavar="<output filename>",
222    default="v8.ignition_dispatches_table.svg",
223    help=("file to save the plot file to. File type is deduced from the "
224          "extension. PDF, SVG, PNG supported")
225  )
226  command_line_parser.add_argument(
227    "--sort-sources-relative", "-r",
228    action="store_true",
229    help=("print top sources in order to how often they dispatch to the "
230          "specified bytecode, only applied when using -f")
231  )
232  command_line_parser.add_argument(
233    "input_filename",
234    metavar="<input filename>",
235    default="v8.ignition_dispatches_table.json",
236    nargs='?',
237    help="Ignition counters JSON file"
238  )
239
240  return command_line_parser.parse_args()
241
242
243def itervalues(d):
244  return d.values() if sys.version_info[0] > 2 else d.itervalues()
245
246
247def iteritems(d):
248  return d.items() if sys.version_info[0] > 2 else d.iteritems()
249
250
251def main():
252  program_options = parse_command_line()
253
254  with open(program_options.input_filename) as stream:
255    dispatches_table = json.load(stream)
256
257  warn_if_counter_may_have_saturated(dispatches_table)
258
259  if program_options.plot:
260    figure, axis = pyplot.subplots()
261    plot_dispatches_table(dispatches_table, figure, axis)
262
263    if program_options.interactive:
264      pyplot.show()
265    else:
266      figure.set_size_inches(program_options.plot_size,
267                             program_options.plot_size)
268      pyplot.savefig(program_options.output_filename)
269  elif program_options.top_bytecode_dispatch_pairs:
270    print_top_bytecode_dispatch_pairs(
271      dispatches_table, program_options.top_entries_count)
272  elif program_options.top_dispatches_for_bytecode:
273    print_top_dispatch_sources_and_destinations(
274      dispatches_table, program_options.top_dispatches_for_bytecode,
275      program_options.top_entries_count, program_options.sort_sources_relative)
276  else:
277    print_top_bytecodes(dispatches_table)
278
279
280if __name__ == "__main__":
281  main()
282