blob: a78f84e5e6496b3aa450f5e36b30f0363da2057d [file] [log] [blame]
Craig Tillerbd9f9242015-05-01 16:24:41 -07001#!/usr/bin/env python
2# Copyright 2015, Google Inc.
3# All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions are
7# met:
8#
9# * Redistributions of source code must retain the above copyright
10# notice, this list of conditions and the following disclaimer.
11# * Redistributions in binary form must reproduce the above
12# copyright notice, this list of conditions and the following disclaimer
13# in the documentation and/or other materials provided with the
14# distribution.
15# * Neither the name of Google Inc. nor the names of its
16# contributors may be used to endorse or promote products derived from
17# this software without specific prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31"""
32Read GRPC basic profiles, analyze the data.
33
34Usage:
35 bins/basicprof/qps_smoke_test > log
36 cat log | tools/profile_analyzer/profile_analyzer.py
37"""
38
39
40import collections
David Garcia Quintas5b2ea292015-05-06 09:57:49 -070041import itertools
David Garcia Quintas776075a2015-05-06 12:59:23 -070042import math
Craig Tillerbd9f9242015-05-01 16:24:41 -070043import re
Craig Tillerbd9f9242015-05-01 16:24:41 -070044
45# Create a regex to parse output of the C core basic profiler,
46# as defined in src/core/profiling/basic_timers.c.
47_RE_LINE = re.compile(r'GRPC_LAT_PROF ' +
David Garcia Quintas5b2ea292015-05-06 09:57:49 -070048 r'([0-9]+\.[0-9]+) 0x([0-9a-f]+) ([{}.!]) ([0-9]+) ' +
Craig Tillerbd9f9242015-05-01 16:24:41 -070049 r'([^ ]+) ([^ ]+) ([0-9]+)')
50
51Entry = collections.namedtuple(
52 'Entry',
53 ['time', 'thread', 'type', 'tag', 'id', 'file', 'line'])
54
David Garcia Quintas5b2ea292015-05-06 09:57:49 -070055
56class ImportantMark(object):
57 def __init__(self, entry, stack):
58 self._entry = entry
59 self._pre_stack = stack
60 self._post_stack = list()
61 self._n = len(stack) # we'll also compute times to that many closing }s
62
63 @property
64 def entry(self):
65 return self._entry
66
David Garcia Quintas9b0a94b2015-05-06 20:50:49 -070067 @property
68 def max_depth(self):
69 return self._n
70
David Garcia Quintas7cff3ee2015-05-06 15:30:08 -070071 def append_post_entry(self, post_entry):
72 if self._n > 0 and post_entry.thread == self._entry.thread:
73 self._post_stack.append(post_entry)
David Garcia Quintas5b2ea292015-05-06 09:57:49 -070074 self._n -= 1
75
76 def get_deltas(self):
77 pre_and_post_stacks = itertools.chain(self._pre_stack, self._post_stack)
78 return collections.OrderedDict((stack_entry,
David Garcia Quintas776075a2015-05-06 12:59:23 -070079 abs(self._entry.time - stack_entry.time))
David Garcia Quintas5b2ea292015-05-06 09:57:49 -070080 for stack_entry in pre_and_post_stacks)
81
David Garcia Quintas776075a2015-05-06 12:59:23 -070082
David Garcia Quintas9b0a94b2015-05-06 20:50:49 -070083def print_block_statistics(block_times):
84 print '{:<12s} {:>12s} {:>12s} {:>12s} {:>12s}'.format(
85 'Block tag', '50th p.', '90th p.', '95th p.', '99th p.')
86 for tag, tag_times in sorted(block_times.iteritems()):
87 times = sorted(tag_times)
88 print '{:<12d}: {:>12.3f} {:>12.3f} {:>12.3f} {:>12.3f}'.format(
89 tag, percentile(times, 50), percentile(times, 90),
90 percentile(times, 95), percentile(times, 99))
91 print
92
David Garcia Quintas776075a2015-05-06 12:59:23 -070093def print_grouped_imark_statistics(group_key, imarks_group):
94 values = collections.OrderedDict()
95 for imark in imarks_group:
96 deltas = imark.get_deltas()
97 for relative_entry, time_delta_us in deltas.iteritems():
98 key = '{tag} {type} ({file}:{line})'.format(**relative_entry._asdict())
99 l = values.setdefault(key, list())
100 l.append(time_delta_us)
101
102 print group_key
David Garcia Quintas9b0a94b2015-05-06 20:50:49 -0700103 print '{:<50s} {:>12s} {:>12s} {:>12s} {:>12s}'.format(
David Garcia Quintas776075a2015-05-06 12:59:23 -0700104 'Relative mark', '50th p.', '90th p.', '95th p.', '99th p.')
105 for key, time_values in values.iteritems():
David Garcia Quintas975efdc2015-05-06 14:42:46 -0700106 time_values = sorted(time_values)
David Garcia Quintas9b0a94b2015-05-06 20:50:49 -0700107 print '{:<50s}: {:>12.3f} {:>12.3f} {:>12.3f} {:>12.3f}'.format(
David Garcia Quintas776075a2015-05-06 12:59:23 -0700108 key, percentile(time_values, 50), percentile(time_values, 90),
109 percentile(time_values, 95), percentile(time_values, 99))
110 print
111
David Garcia Quintas776075a2015-05-06 12:59:23 -0700112def percentile(vals, percent):
David Garcia Quintas975efdc2015-05-06 14:42:46 -0700113 """ Calculates the interpolated percentile given a sorted sequence and a
114 percent (in the usual 0-100 range)."""
David Garcia Quintas776075a2015-05-06 12:59:23 -0700115 assert vals, "Empty input sequence."
David Garcia Quintas776075a2015-05-06 12:59:23 -0700116 percent /= 100.0
117 k = (len(vals)-1) * percent
118 f = math.floor(k)
119 c = math.ceil(k)
120 if f == c:
121 return vals[int(k)]
122 # else, interpolate
123 d0 = vals[int(f)] * (c-k)
124 d1 = vals[int(c)] * (k-f)
125 return d0 + d1
Craig Tillerbd9f9242015-05-01 16:24:41 -0700126
David Garcia Quintas9b0a94b2015-05-06 20:50:49 -0700127def entries(f):
128 for line in f:
129 m = _RE_LINE.match(line)
130 if not m: continue
131 yield Entry(time=float(m.group(1)),
132 thread=m.group(2),
133 type=m.group(3),
134 tag=int(m.group(4)),
135 id=m.group(5),
136 file=m.group(6),
137 line=m.group(7))
David Garcia Quintas5b2ea292015-05-06 09:57:49 -0700138
David Garcia Quintas9b0a94b2015-05-06 20:50:49 -0700139def main(f):
140 percentiles = (50, 90, 95, 99)
141 threads = collections.defaultdict(lambda: collections.defaultdict(list))
142 times = collections.defaultdict(list)
143 important_marks = collections.defaultdict(list)
144 stack_depth = collections.defaultdict(int)
145 for entry in entries(f):
146 thread = threads[entry.thread]
147 if entry.type == '{':
148 thread[entry.tag].append(entry)
149 stack_depth[entry.thread] += 1
150 if entry.type == '!':
151 # Save a snapshot of the current stack inside a new ImportantMark instance.
152 # Get all entries _for any tag in the thread_.
153 stack = [e for entries_for_tag in thread.itervalues()
154 for e in entries_for_tag]
155 imark_group_key = '{tag}/{thread}@{file}:{line}'.format(**entry._asdict())
156 important_marks[imark_group_key].append(ImportantMark(entry, stack))
157 elif entry.type == '}':
158 last = thread[entry.tag].pop()
159 times[entry.tag].append(entry.time - last.time)
160 # only access the last "depth" imarks for the tag.
161 depth = stack_depth[entry.thread]
162 for imarks_group in important_marks.itervalues():
163 for imark in imarks_group[-depth:]:
164 # if at a '}' deeper than where the current "imark" was found, ignore.
165 if depth > imark.max_depth: continue
166 imark.append_post_entry(entry)
167 stack_depth[entry.thread] -= 1
168
169 print
170 print 'Block marks:'
171 print '============'
172 print_block_statistics(times)
173
174 print
175 print 'Important marks:'
176 print '================'
177 for group_key, imarks_group in important_marks.iteritems():
178 print_grouped_imark_statistics(group_key, imarks_group)
179
180
181if __name__ == '__main__':
182 # If invoked without arguments, read off sys.stdin. If one argument is given,
183 # take it as a file name and open it for reading.
184 import sys
185 f = sys.stdin
186 if len(sys.argv) == 2:
187 f = open(sys.argv[1], 'r')
188 main(f)