3
# Copyright 2010 the V8 project authors. All rights reserved.
4
# Redistribution and use in source and binary forms, with or without
5
# modification, are permitted provided that the following conditions are
8
# * Redistributions of source code must retain the above copyright
9
# notice, this list of conditions and the following disclaimer.
10
# * Redistributions in binary form must reproduce the above
11
# copyright notice, this list of conditions and the following
12
# disclaimer in the documentation and/or other materials provided
13
# with the distribution.
14
# * Neither the name of Google Inc. nor the names of its
15
# contributors may be used to endorse or promote products derived
16
# from this software without specific prior written permission.
18
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
# This is an utility for plotting charts based on GC traces produced by V8 when
33
# run with flags --trace-gc --trace-gc-nvp. Relies on gnuplot for actual
36
# Usage: gc-nvp-trace-processor.py <GC-trace-filename>
40
from __future__ import with_statement
41
import sys, types, re, subprocess, math
45
for i in l: flat.extend(i)
50
for (name, value) in re.findall(r"(\w+)=([-\w]+)", s):
58
def parse_gc_trace(input):
60
with open(input) as f:
62
info = split_nvp(line)
63
if info and 'pause' in info and info['pause'] > 0:
64
info['i'] = len(trace)
68
def extract_field_names(script):
69
fields = { 'data': true, 'in': true }
71
for m in re.finditer(r"$(\w+)", script):
72
field_name = m.group(1)
73
if field_name not in fields:
74
fields[field] = field_count
75
field_count = field_count + 1
80
gnuplot = subprocess.Popen(["gnuplot"], stdin=subprocess.PIPE)
81
gnuplot.stdin.write(script)
91
def __init__(self, title, field, axis = x1y1, **keywords):
95
if type(field) is types.ListType:
103
def to_gnuplot(self, context):
104
args = ['"%s"' % context.datafile,
105
'using %s' % context.format_fieldref(self.field),
106
'title "%s"' % self.title,
107
'axis %s' % self.axis]
108
if 'style' in self.props:
109
args.append('with %s' % self.props['style'])
110
if 'lc' in self.props:
111
args.append('lc rgb "%s"' % self.props['lc'])
112
if 'fs' in self.props:
113
args.append('fs %s' % self.props['fs'])
114
return ' '.join(args)
117
def __init__(self, *items):
121
return flatten([item.fieldrefs() for item in self.items])
123
def to_gnuplot(self, ctx):
124
return 'plot ' + ', '.join([item.to_gnuplot(ctx) for item in self.items])
127
def __init__(self, value):
130
def to_gnuplot(self, ctx):
131
return 'set ' + self.value
136
class Context(object):
137
def __init__(self, datafile, field_to_index):
138
self.datafile = datafile
139
self.field_to_index = field_to_index
141
def format_fieldref(self, fieldref):
142
return ':'.join([str(self.field_to_index[field]) for field in fieldref])
144
def collect_fields(plot):
148
def add_field(field):
149
if field not in field_to_index:
151
field_to_index[field] = len(fields)
153
for field in flatten([item.fieldrefs() for item in plot]):
156
return (fields, field_to_index)
158
def is_y2_used(plot):
160
if isinstance(subplot, Plot):
161
for item in subplot.items:
162
if item.axis == x1y2 or item.axis == x2y2:
166
def get_field(trace_line, field):
168
if t is types.StringType:
169
return trace_line[field]
170
elif t is types.FunctionType:
171
return field(trace_line)
173
def generate_datafile(datafile_name, trace, fields):
174
with open(datafile_name, 'w') as datafile:
176
data_line = [str(get_field(line, field)) for field in fields]
177
datafile.write('\t'.join(data_line))
180
def generate_script_and_datafile(plot, trace, datafile, output):
181
(fields, field_to_index) = collect_fields(plot)
182
generate_datafile(datafile, trace, fields)
185
'set output "%s"' % output,
187
'set ytics nomirror',
188
'set xtics nomirror',
193
script.append('set autoscale y2')
194
script.append('set y2tics')
196
context = Context(datafile, field_to_index)
199
script.append(item.to_gnuplot(context))
201
return '\n'.join(script)
203
def plot_all(plots, trace, prefix):
207
outfilename = "%s_%d.png" % (prefix, len(charts))
208
charts.append(outfilename)
209
script = generate_script_and_datafile(plot, trace, '~datafile', outfilename)
210
print 'Plotting %s...' % outfilename
215
def reclaimed_bytes(row):
216
return row['total_size_before'] - row['total_size_after']
220
# there is no 'other' scope for scavenging collections.
222
return r['pause'] - r['mark'] - r['sweep'] - r['external']
224
def scavenge_scope(r):
226
return r['pause'] - r['external']
231
return r['mutator'] - r['stepstook']
235
Set('style fill solid 0.5 noborder'),
236
Set('style histogram rowstacked'),
237
Set('style data histograms'),
238
Plot(Item('Scavenge', scavenge_scope, lc = 'green'),
239
Item('Marking', 'mark', lc = 'purple'),
240
Item('Sweep', 'sweep', lc = 'blue'),
241
Item('External', 'external', lc = '#489D43'),
242
Item('Other', other_scope, lc = 'grey'),
243
Item('IGC Steps', 'stepstook', lc = '#FF6347'))
246
Set('style fill solid 0.5 noborder'),
247
Set('style histogram rowstacked'),
248
Set('style data histograms'),
249
Plot(Item('Scavenge', scavenge_scope, lc = 'green'),
250
Item('Marking', 'mark', lc = 'purple'),
251
Item('Sweep', 'sweep', lc = 'blue'),
252
Item('External', 'external', lc = '#489D43'),
253
Item('Other', other_scope, lc = '#ADD8E6'),
254
Item('External', 'external', lc = '#D3D3D3'))
258
Plot(Item('Mutator', real_mutator, lc = 'black', style = 'lines'))
261
Set('style histogram rowstacked'),
262
Set('style data histograms'),
263
Plot(Item('Heap Size (before GC)', 'total_size_before', x1y2,
264
fs = 'solid 0.4 noborder',
266
Item('Total holes (after GC)', 'holes_size_before', x1y2,
267
fs = 'solid 0.4 noborder',
269
Item('GC Time', ['i', 'pause'], style = 'lines', lc = 'red'))
272
Set('style histogram rowstacked'),
273
Set('style data histograms'),
274
Plot(Item('Heap Size (after GC)', 'total_size_after', x1y2,
275
fs = 'solid 0.4 noborder',
277
Item('Total holes (after GC)', 'holes_size_after', x1y2,
278
fs = 'solid 0.4 noborder',
280
Item('GC Time', ['i', 'pause'],
285
Set('style fill solid 0.5 noborder'),
286
Set('style data histograms'),
287
Plot(Item('Allocated', 'allocated'),
288
Item('Reclaimed', reclaimed_bytes),
289
Item('Promoted', 'promoted', style = 'lines', lc = 'black'))
293
def freduce(f, field, trace, init):
294
return reduce(lambda t,r: f(t, r[field]), trace, init)
296
def calc_total(trace, field):
297
return freduce(lambda t,v: t + long(v), field, trace, long(0))
299
def calc_max(trace, field):
300
return freduce(lambda t,r: max(t, r), field, trace, 0)
302
def count_nonzero(trace, field):
303
return freduce(lambda t,r: t if r == 0 else t + 1, field, trace, 0)
306
def process_trace(filename):
307
trace = parse_gc_trace(filename)
309
marksweeps = filter(lambda r: r['gc'] == 'ms', trace)
310
scavenges = filter(lambda r: r['gc'] == 's', trace)
311
globalgcs = filter(lambda r: r['gc'] != 's', trace)
314
charts = plot_all(plots, trace, filename)
316
def stats(out, prefix, trace, field):
318
total = calc_total(trace, field)
319
max = calc_max(trace, field)
325
dev = math.sqrt(freduce(lambda t,r: t + (r - avg) ** 2, field, trace, 0) /
330
out.write('<tr><td>%s</td><td>%d</td><td>%d</td>'
331
'<td>%d</td><td>%d [dev %f]</td></tr>' %
332
(prefix, n, total, max, avg, dev))
334
def HumanReadable(size):
335
suffixes = ['bytes', 'kB', 'MB', 'GB']
337
for i in range(len(suffixes)):
338
if size < power*1024:
339
return "%.1f" % (float(size) / power) + " " + suffixes[i]
342
def throughput(name, trace):
343
total_live_after = calc_total(trace, 'total_size_after')
344
total_live_before = calc_total(trace, 'total_size_before')
345
total_gc = calc_total(trace, 'pause')
348
out.write('GC %s Throughput (after): %s / %s ms = %s/ms<br/>' %
350
HumanReadable(total_live_after),
352
HumanReadable(total_live_after / total_gc)))
353
out.write('GC %s Throughput (before): %s / %s ms = %s/ms<br/>' %
355
HumanReadable(total_live_before),
357
HumanReadable(total_live_before / total_gc)))
360
with open(filename + '.html', 'w') as out:
361
out.write('<html><body>')
363
out.write('<tr><td>Phase</td><td>Count</td><td>Time (ms)</td>')
364
out.write('<td>Max</td><td>Avg</td></tr>')
365
stats(out, 'Total in GC', trace, 'pause')
366
stats(out, 'Scavenge', scavenges, 'pause')
367
stats(out, 'MarkSweep', marksweeps, 'pause')
368
stats(out, 'Mark', filter(lambda r: r['mark'] != 0, trace), 'mark')
369
stats(out, 'Sweep', filter(lambda r: r['sweep'] != 0, trace), 'sweep')
372
filter(lambda r: r['external'] != 0, trace),
374
out.write('</table>')
375
throughput('TOTAL', trace)
376
throughput('MS', marksweeps)
377
throughput('OLDSPACE', globalgcs)
380
out.write('<img src="%s">' % chart)
381
out.write('</body></html>')
383
print "%s generated." % (filename + '.html')
385
if len(sys.argv) != 2:
386
print "Usage: %s <GC-trace-filename>" % sys.argv[0]
389
process_trace(sys.argv[1])