123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- #!/usr/bin/env python3
- # Copyright 2015 gRPC authors.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- import argparse
- import collections
- import hashlib
- import json
- import math
- import sys
- import time
- from six.moves import zip
- import tabulate
- SELF_TIME = object()
- TIME_FROM_SCOPE_START = object()
- TIME_TO_SCOPE_END = object()
- TIME_FROM_STACK_START = object()
- TIME_TO_STACK_END = object()
- TIME_FROM_LAST_IMPORTANT = object()
- argp = argparse.ArgumentParser(
- description='Process output of basic_prof builds')
- argp.add_argument('--source', default='latency_trace.txt', type=str)
- argp.add_argument('--fmt', choices=tabulate.tabulate_formats, default='simple')
- argp.add_argument('--out', default='-', type=str)
- args = argp.parse_args()
- class LineItem(object):
- def __init__(self, line, indent):
- self.tag = line['tag']
- self.indent = indent
- self.start_time = line['t']
- self.end_time = None
- self.important = line['imp']
- self.filename = line['file']
- self.fileline = line['line']
- self.times = {}
- class ScopeBuilder(object):
- def __init__(self, call_stack_builder, line):
- self.call_stack_builder = call_stack_builder
- self.indent = len(call_stack_builder.stk)
- self.top_line = LineItem(line, self.indent)
- call_stack_builder.lines.append(self.top_line)
- self.first_child_pos = len(call_stack_builder.lines)
- def mark(self, line):
- line_item = LineItem(line, self.indent + 1)
- line_item.end_time = line_item.start_time
- self.call_stack_builder.lines.append(line_item)
- def finish(self, line):
- assert line['tag'] == self.top_line.tag, (
- 'expected %s, got %s; thread=%s; t0=%f t1=%f' %
- (self.top_line.tag, line['tag'], line['thd'],
- self.top_line.start_time, line['t']))
- final_time_stamp = line['t']
- assert self.top_line.end_time is None
- self.top_line.end_time = final_time_stamp
- self.top_line.important = self.top_line.important or line['imp']
- assert SELF_TIME not in self.top_line.times
- self.top_line.times[
- SELF_TIME] = final_time_stamp - self.top_line.start_time
- for line in self.call_stack_builder.lines[self.first_child_pos:]:
- if TIME_FROM_SCOPE_START not in line.times:
- line.times[
- TIME_FROM_SCOPE_START] = line.start_time - self.top_line.start_time
- line.times[TIME_TO_SCOPE_END] = final_time_stamp - line.end_time
- class CallStackBuilder(object):
- def __init__(self):
- self.stk = []
- self.signature = hashlib.md5()
- self.lines = []
- def finish(self):
- start_time = self.lines[0].start_time
- end_time = self.lines[0].end_time
- self.signature = self.signature.hexdigest()
- last_important = start_time
- for line in self.lines:
- line.times[TIME_FROM_STACK_START] = line.start_time - start_time
- line.times[TIME_TO_STACK_END] = end_time - line.end_time
- line.times[
- TIME_FROM_LAST_IMPORTANT] = line.start_time - last_important
- if line.important:
- last_important = line.end_time
- last_important = end_time
- def add(self, line):
- line_type = line['type']
- self.signature.update(line_type.encode('UTF-8'))
- self.signature.update(line['tag'].encode('UTF-8'))
- if line_type == '{':
- self.stk.append(ScopeBuilder(self, line))
- return False
- elif line_type == '}':
- assert self.stk, (
- 'expected non-empty stack for closing %s; thread=%s; t=%f' %
- (line['tag'], line['thd'], line['t']))
- self.stk.pop().finish(line)
- if not self.stk:
- self.finish()
- return True
- return False
- elif line_type == '.' or line_type == '!':
- if self.stk:
- self.stk[-1].mark(line)
- return False
- else:
- raise Exception('Unknown line type: \'%s\'' % line_type)
- class CallStack(object):
- def __init__(self, initial_call_stack_builder):
- self.count = 1
- self.signature = initial_call_stack_builder.signature
- self.lines = initial_call_stack_builder.lines
- for line in self.lines:
- for key, val in list(line.times.items()):
- line.times[key] = [val]
- def add(self, call_stack_builder):
- assert self.signature == call_stack_builder.signature
- self.count += 1
- assert len(self.lines) == len(call_stack_builder.lines)
- for lsum, line in zip(self.lines, call_stack_builder.lines):
- assert lsum.tag == line.tag
- assert list(lsum.times.keys()) == list(line.times.keys())
- for k, lst in list(lsum.times.items()):
- lst.append(line.times[k])
- def finish(self):
- for line in self.lines:
- for lst in list(line.times.values()):
- lst.sort()
- builder = collections.defaultdict(CallStackBuilder)
- call_stacks = collections.defaultdict(CallStack)
- lines = 0
- start = time.time()
- with open(args.source) as f:
- for line in f:
- lines += 1
- inf = json.loads(line)
- thd = inf['thd']
- cs = builder[thd]
- if cs.add(inf):
- if cs.signature in call_stacks:
- call_stacks[cs.signature].add(cs)
- else:
- call_stacks[cs.signature] = CallStack(cs)
- del builder[thd]
- time_taken = time.time() - start
- call_stacks = sorted(list(call_stacks.values()),
- key=lambda cs: cs.count,
- reverse=True)
- total_stacks = 0
- for cs in call_stacks:
- total_stacks += cs.count
- cs.finish()
- def percentile(N, percent, key=lambda x: x):
- """
- Find the percentile of an already sorted list of values.
- @parameter N - is a list of values. MUST be already sorted.
- @parameter percent - a float value from [0.0,1.0].
- @parameter key - optional key function to compute value from each element of N.
- @return - the percentile of the values
- """
- if not N:
- return None
- float_idx = (len(N) - 1) * percent
- idx = int(float_idx)
- result = key(N[idx])
- if idx < len(N) - 1:
- # interpolate with the next element's value
- result += (float_idx - idx) * (key(N[idx + 1]) - key(N[idx]))
- return result
- def tidy_tag(tag):
- if tag[0:10] == 'GRPC_PTAG_':
- return tag[10:]
- return tag
- def time_string(values):
- num_values = len(values)
- return '%.1f/%.1f/%.1f' % (1e6 * percentile(values, 0.5), 1e6 * percentile(
- values, 0.9), 1e6 * percentile(values, 0.99))
- def time_format(idx):
- def ent(line, idx=idx):
- if idx in line.times:
- return time_string(line.times[idx])
- return ''
- return ent
- BANNER = {'simple': 'Count: %(count)d', 'html': '<h1>Count: %(count)d</h1>'}
- FORMAT = [
- ('TAG', lambda line: '..' * line.indent + tidy_tag(line.tag)),
- ('LOC', lambda line: '%s:%d' %
- (line.filename[line.filename.rfind('/') + 1:], line.fileline)),
- ('IMP', lambda line: '*' if line.important else ''),
- ('FROM_IMP', time_format(TIME_FROM_LAST_IMPORTANT)),
- ('FROM_STACK_START', time_format(TIME_FROM_STACK_START)),
- ('SELF', time_format(SELF_TIME)),
- ('TO_STACK_END', time_format(TIME_TO_STACK_END)),
- ('FROM_SCOPE_START', time_format(TIME_FROM_SCOPE_START)),
- ('SELF', time_format(SELF_TIME)),
- ('TO_SCOPE_END', time_format(TIME_TO_SCOPE_END)),
- ]
- out = sys.stdout
- if args.out != '-':
- out = open(args.out, 'w')
- if args.fmt == 'html':
- out.write('<html>')
- out.write('<head>')
- out.write('<title>Profile Report</title>')
- out.write('</head>')
- accounted_for = 0
- for cs in call_stacks:
- out.write('\n')
- if args.fmt in BANNER:
- out.write(BANNER[args.fmt] % {
- 'count': cs.count,
- })
- header, _ = list(zip(*FORMAT))
- table = []
- for line in cs.lines:
- fields = []
- for _, fn in FORMAT:
- fields.append(fn(line))
- table.append(fields)
- out.write(tabulate.tabulate(table, header, tablefmt=args.fmt))
- accounted_for += cs.count
- if accounted_for > .99 * total_stacks:
- break
- if args.fmt == 'html':
- print('</html>')
|