|  | #!/usr/bin/env python | 
|  | # Copyright 2018 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | # This script parses result of build/android/adb_profile_chrome_startup and | 
|  | # prints various information. | 
|  |  | 
|  | from __future__ import print_function | 
|  |  | 
|  | import argparse | 
|  | import collections | 
|  | import glob | 
|  | import itertools | 
|  | import json | 
|  | import os | 
|  | import re | 
|  |  | 
|  |  | 
|  | def HumanSortingKey(string): | 
|  | # From https://goo.gl/65xrva | 
|  | def _ToInt(string): | 
|  | return int(string) if string.isdigit() else string | 
|  |  | 
|  | return [_ToInt(c) for c in re.split('(\d+)', string)] | 
|  |  | 
|  |  | 
|  | class LogIndenter(object): | 
|  | _indentation = 0 | 
|  |  | 
|  | def __init__(self, message=None, *arguments): | 
|  | self._indented = False | 
|  | if message is not None: | 
|  | log(message, *arguments) | 
|  |  | 
|  | def __enter__(self): | 
|  | self.indent() | 
|  | return self | 
|  |  | 
|  | def __exit__(self, type, value, traceback): | 
|  | self.unindent() | 
|  |  | 
|  | def indent(self): | 
|  | if not self._indented: | 
|  | LogIndenter._indentation += 1 | 
|  | self._indented = True | 
|  |  | 
|  | def unindent(self): | 
|  | if self._indented: | 
|  | LogIndenter._indentation -= 1 | 
|  | self._indented = False | 
|  |  | 
|  | @classmethod | 
|  | def indentation(self): | 
|  | return LogIndenter._indentation | 
|  |  | 
|  |  | 
|  | def log(message, *arguments): | 
|  | if not message: | 
|  | print() | 
|  | return | 
|  |  | 
|  | if arguments: | 
|  | message = message.format(*arguments) | 
|  | if LogIndenter.indentation() > 0: | 
|  | message = '  ' * LogIndenter.indentation() + message | 
|  | print(message) | 
|  |  | 
|  |  | 
|  | def ParseTraceDatas(trace_file_path): | 
|  | start_tag = re.compile( | 
|  | '^\s*<script class="trace-data" type="application/text">$') | 
|  | end_tag = re.compile('^(?P<line>.*?)\s*</script>$') | 
|  |  | 
|  | trace_datas = [] | 
|  |  | 
|  | current_trace_lines = None | 
|  | with open(trace_file_path) as trace_file: | 
|  | for line in trace_file: | 
|  | line = line.rstrip() | 
|  | if current_trace_lines is None: | 
|  | if start_tag.match(line): | 
|  | current_trace_lines = [] | 
|  | else: | 
|  | match = end_tag.match(line) | 
|  | if match: | 
|  | current_trace_lines.append(match.group('line')) | 
|  | trace_datas.append('\n'.join(current_trace_lines)) | 
|  | current_trace_lines = None | 
|  | else: | 
|  | current_trace_lines.append(line) | 
|  |  | 
|  | return trace_datas | 
|  |  | 
|  |  | 
|  | class Event(object): | 
|  | PHASE_BEGIN = 'B' | 
|  | PHASE_END = 'E' | 
|  | PHASE_COMPLETE = 'X' | 
|  | PHASE_ASYNC_BEGIN = 'S' | 
|  | PHASE_ASYNC_END = 'F' | 
|  |  | 
|  | def __init__(self, node): | 
|  | self._node = node | 
|  |  | 
|  | @property | 
|  | def pid(self): | 
|  | return int(self._node['pid']) | 
|  |  | 
|  | @property | 
|  | def tid(self): | 
|  | return int(self._node['tid']) | 
|  |  | 
|  | @property | 
|  | def name(self): | 
|  | return self._node.get('name') | 
|  |  | 
|  | @property | 
|  | def phase(self): | 
|  | return self._node['ph'] | 
|  |  | 
|  | @property | 
|  | def category(self): | 
|  | return self._node['cat'] | 
|  |  | 
|  | @property | 
|  | def timestamp_us(self): | 
|  | return long(self._node['ts']) | 
|  |  | 
|  | @property | 
|  | def duration_us(self): | 
|  | return long(self._node['dur']) | 
|  |  | 
|  | @property | 
|  | def args(self): | 
|  | return self._node['args'] | 
|  |  | 
|  |  | 
|  | class EventInterval(object): | 
|  | def __init__(self, from_event=None, to_event=None): | 
|  | self.from_event = from_event | 
|  | self.to_event = to_event | 
|  |  | 
|  | def SetFromEventOnce(self, from_event): | 
|  | if self.from_event is None: | 
|  | self.from_event = from_event | 
|  |  | 
|  | def SetToEventOnce(self, to_event): | 
|  | if self.to_event is None: | 
|  | self.to_event = to_event | 
|  |  | 
|  | def FormatAsMilliseconds(self): | 
|  | if not self.from_event: | 
|  | time_string = "[missing the start of of the interval]" | 
|  | elif not self.to_event: | 
|  | time_string = "[missing the end of the interval]" | 
|  | else: | 
|  | interval_us = self.to_event.timestamp_us - self.from_event.timestamp_us | 
|  | time_string = str(interval_us / 1000.0) | 
|  | return time_string | 
|  |  | 
|  | def UpTo(self, other_iterval): | 
|  | return EventInterval(self.from_event, other_iterval.to_event) | 
|  |  | 
|  |  | 
|  | class Process(object): | 
|  |  | 
|  | BROWSER_NAME = 'Browser' | 
|  |  | 
|  | def __init__(self, pid): | 
|  | self.pid = pid | 
|  | self.name = None | 
|  | self.events_by_name = collections.defaultdict(list) | 
|  | self.time_ns_by_histogram = {} | 
|  | self.malloc_counter_by_name = {} | 
|  | # TODO: move these into Trace | 
|  | self.startup_interval = EventInterval() | 
|  | self.first_ui_interval = EventInterval() | 
|  |  | 
|  |  | 
|  | class Trace(object): | 
|  | def __init__(self, file_path): | 
|  | self.file_path = file_path | 
|  | self.process_by_pid = {} | 
|  | self.startup_event = None | 
|  | self.navigation_start_event = None | 
|  | # TODO: convert these to properties over events | 
|  | self.navigation_to_contentul_paint_interval = EventInterval() | 
|  | self.navigation_to_meaningful_paint_interval = EventInterval() | 
|  | self.navigation_to_commit_interval = None | 
|  |  | 
|  | @property | 
|  | def startup_to_navigation_interval(self): | 
|  | return EventInterval(self.startup_event, self.navigation_start_event) | 
|  |  | 
|  | def Finalize(self): | 
|  | self.startup_event = self.FindFirstEvent(*Trace.STARTUP_EVENT_NAMES) | 
|  | self.navigation_start_event = self.FindFirstEvent( | 
|  | Trace.NAVIGATION_START_EVENT_NAME) | 
|  |  | 
|  | def _FindNavigationToCommitInterval(): | 
|  | events = self.FindAllEvents(Trace.NAVIGATION_COMMIT_EVENT_NAME) | 
|  | interval = EventInterval() | 
|  | for event in events: | 
|  | if event.phase == Event.PHASE_ASYNC_BEGIN: | 
|  | interval.SetFromEventOnce(event) | 
|  | elif event.phase == Event.PHASE_ASYNC_END: | 
|  | interval.SetToEventOnce(event) | 
|  | return interval | 
|  | self.navigation_to_commit_interval = _FindNavigationToCommitInterval() | 
|  |  | 
|  | def FindAllEvents(self, *names): | 
|  | events = [] | 
|  | for process in self.process_by_pid.itervalues(): | 
|  | for name in names: | 
|  | process_events = process.events_by_name.get(name) | 
|  | if process_events: | 
|  | events.extend(process_events) | 
|  | events.sort(key=lambda e: e.timestamp_us) | 
|  | return events | 
|  |  | 
|  | def FindFirstEvent(self, *names): | 
|  | events = self.FindAllEvents(*names) | 
|  | return events[0] if events else None | 
|  |  | 
|  | NAVIGATION_START_EVENT_NAME = 'NavigationTiming navigationStart' | 
|  | NAVIGATION_COMMIT_EVENT_NAME = 'Navigation StartToCommit' | 
|  |  | 
|  | STARTUP_EVENT_NAMES = [ | 
|  | 'Startup.BrowserMainEntryPoint', 'ChromeApplication.onCreate', | 
|  | 'ContentShellApplication.onCreate' | 
|  | ] | 
|  |  | 
|  |  | 
|  | def ParseTrace(file_path): | 
|  | trace_datas = ParseTraceDatas(file_path) | 
|  | if not trace_datas: | 
|  | raise Exception("The file doesn't have any trace-data elements.") | 
|  |  | 
|  | trace_json = None | 
|  | for trace_data in trace_datas: | 
|  | try: | 
|  | trace_json = json.loads(trace_data) | 
|  | except ValueError: | 
|  | continue | 
|  |  | 
|  | if not trace_json: | 
|  | raise Exception("Couldn't parse trace-data json.") | 
|  |  | 
|  | trace = Trace(file_path) | 
|  |  | 
|  | for event_node in trace_json['traceEvents']: | 
|  | event = Event(event_node) | 
|  |  | 
|  | pid = event.pid | 
|  | process = trace.process_by_pid.get(event.pid) | 
|  | if not process: | 
|  | process = Process(pid) | 
|  | trace.process_by_pid[pid] = process | 
|  |  | 
|  | name = event.name | 
|  | if not name: | 
|  | continue | 
|  |  | 
|  | process.events_by_name[name].append(event) | 
|  |  | 
|  | phase = event.phase | 
|  | category = event.category | 
|  |  | 
|  | if name == 'process_name': | 
|  | process.name = event.args['name'] | 
|  |  | 
|  | if (category == 'disabled-by-default-uma-addtime' and | 
|  | name not in process.time_ns_by_histogram): | 
|  | process.time_ns_by_histogram[name] = int(event.args['value_ns']) | 
|  |  | 
|  | if name in Trace.STARTUP_EVENT_NAMES: | 
|  | process.startup_interval.SetFromEventOnce(event) | 
|  | elif name == 'BenchmarkInstrumentation::ImplThreadRenderingStats': | 
|  | process.startup_interval.SetToEventOnce(event) | 
|  |  | 
|  | if name == Trace.NAVIGATION_START_EVENT_NAME: | 
|  | trace.navigation_to_contentul_paint_interval.SetFromEventOnce(event) | 
|  | trace.navigation_to_meaningful_paint_interval.SetFromEventOnce(event) | 
|  | elif name == 'firstContentfulPaint': | 
|  | trace.navigation_to_contentul_paint_interval.SetToEventOnce(event) | 
|  | elif name == 'firstMeaningfulPaint': | 
|  | trace.navigation_to_meaningful_paint_interval.SetToEventOnce(event) | 
|  |  | 
|  | if (name == 'AsyncInitializationActivity.onCreate()' and | 
|  | phase == Event.PHASE_END): | 
|  | process.first_ui_interval.SetFromEventOnce(event) | 
|  | elif name == 'ChromeBrowserInitializer.startChromeBrowserProcessesAsync': | 
|  | process.first_ui_interval.SetToEventOnce(event) | 
|  |  | 
|  | if category == 'malloc' and name == 'malloc_counter': | 
|  | counter_name, counter_value = next(event.args.iteritems()) | 
|  | process.malloc_counter_by_name[counter_name] = long(counter_value) | 
|  |  | 
|  | trace.Finalize() | 
|  | return trace | 
|  |  | 
|  |  | 
|  | EventSummary = collections.namedtuple('EventSummary', [ | 
|  | 'trace', | 
|  | 'event', | 
|  | 'startup_to_event_ms', | 
|  | 'navigation_to_event_ms', | 
|  | 'duration_ms' | 
|  | ]) | 
|  |  | 
|  | def SummarizeEvents(event_name_regex, trace, process): | 
|  | summaries = [] | 
|  | def _AddSummary(event, start_us, duration_us): | 
|  | startup_to_event_ms = ( | 
|  | None if trace.startup_event is None else | 
|  | (start_us - trace.startup_event.timestamp_us) / 1000.0) | 
|  | navigation_to_event_ms = ( | 
|  | None if trace.navigation_start_event is None else | 
|  | (start_us - trace.navigation_start_event.timestamp_us) / 1000.0) | 
|  | summaries.append(EventSummary( | 
|  | trace, event, startup_to_event_ms, navigation_to_event_ms, | 
|  | duration_us / 1000.0)) | 
|  |  | 
|  | for name, events in process.events_by_name.iteritems(): | 
|  | if event_name_regex.search(name): | 
|  | sorted_events = sorted(events, | 
|  | key=lambda e: (e.tid, e.timestamp_us)) | 
|  | begin_event = None | 
|  | for event in sorted_events: | 
|  | if event.phase == Event.PHASE_COMPLETE: | 
|  | _AddSummary(event, event.timestamp_us, event.duration_us) | 
|  | elif (event.phase == Event.PHASE_BEGIN or | 
|  | event.phase == Event.PHASE_ASYNC_BEGIN): | 
|  | begin_event = event | 
|  | elif (event.phase == Event.PHASE_END or | 
|  | event.phase == Event.PHASE_ASYNC_END): | 
|  | if begin_event is not None: | 
|  | duration_us = event.timestamp_us - begin_event.timestamp_us | 
|  | _AddSummary(event, begin_event.timestamp_us, duration_us) | 
|  | begin_event = None | 
|  |  | 
|  | return summaries | 
|  |  | 
|  |  | 
|  | def PrintReport(file_paths, options): | 
|  | # TODO: don't accumulate traces, build report on the fly | 
|  | traces = [] | 
|  | for file_path in file_paths: | 
|  | log('Parsing {}...', file_path) | 
|  | try: | 
|  | traces.append(ParseTrace(file_path)) | 
|  | except Exception as e: | 
|  | log('Oops: {}', e.message) | 
|  |  | 
|  | log('Parsed {} trace(s).', len(traces)) | 
|  |  | 
|  | event_name_regex = None | 
|  | event_summaries_by_name = collections.defaultdict(list) | 
|  | if options.print_events: | 
|  | event_name_regex = re.compile(options.print_events) | 
|  |  | 
|  | def _TraceSortingKey(trace): | 
|  | return HumanSortingKey(os.path.basename(trace.file_path)) | 
|  |  | 
|  | traces.sort(key=lambda t: _TraceSortingKey(t)) | 
|  |  | 
|  | if options.csv: | 
|  | separator = ',' | 
|  | gap = '' | 
|  | else: | 
|  | separator = '\t' | 
|  | # Make it less likely for terminals to eat tabs when wrapping a line. | 
|  | gap = '    ' | 
|  |  | 
|  | table = [[ | 
|  | 'File', | 
|  | 'Startup (ms)', | 
|  | 'StartupToNavigation (ms)', | 
|  | 'NavigationToCommit (ms)', | 
|  | 'NavigationToContentfulPaint (ms)', | 
|  | 'StartupToContentfulPaint (ms)', | 
|  | 'NavigationToMeaningfulPaint (ms)', | 
|  | 'StartupToMeaningfulPaint (ms)' | 
|  | ]] | 
|  | for trace in traces: | 
|  | browser_process = None | 
|  | for process in trace.process_by_pid.itervalues(): | 
|  | if process.name == Process.BROWSER_NAME: | 
|  | browser_process = process | 
|  | break | 
|  | if browser_process is None: | 
|  | continue | 
|  |  | 
|  | table.append([ | 
|  | os.path.basename(trace.file_path), | 
|  | browser_process.startup_interval.FormatAsMilliseconds(), | 
|  | trace.startup_to_navigation_interval.FormatAsMilliseconds(), | 
|  | trace.navigation_to_commit_interval.FormatAsMilliseconds(), | 
|  | trace.navigation_to_contentul_paint_interval.FormatAsMilliseconds(), | 
|  | browser_process.startup_interval.UpTo(trace.navigation_to_contentul_paint_interval).\ | 
|  | FormatAsMilliseconds(), | 
|  | trace.navigation_to_meaningful_paint_interval.FormatAsMilliseconds(), | 
|  | browser_process.startup_interval.UpTo(trace.navigation_to_meaningful_paint_interval).\ | 
|  | FormatAsMilliseconds() | 
|  | ]) | 
|  |  | 
|  | if event_name_regex: | 
|  | event_summaries = SummarizeEvents( | 
|  | event_name_regex, trace, browser_process) | 
|  | for summary in event_summaries: | 
|  | event_summaries_by_name[summary.event.name].append(summary) | 
|  |  | 
|  | for name, event_summaries in event_summaries_by_name.iteritems(): | 
|  | table.append([]) | 
|  |  | 
|  | summaries_by_trace = collections.defaultdict(list) | 
|  | for summary in event_summaries: | 
|  | summaries_by_trace[summary.trace].append(summary) | 
|  |  | 
|  | width = max(len(s) for s in summaries_by_trace.itervalues()) | 
|  | summary_headers = [ | 
|  | 'StartupToEvent (ms)', | 
|  | 'NavigationToEvent (ms)', | 
|  | 'Duration (ms)' | 
|  | ] | 
|  |  | 
|  | table.append( | 
|  | [name] + | 
|  | ([gap] * len(summary_headers)) + | 
|  | list(itertools.chain.from_iterable( | 
|  | ['#{}'.format(i)] + [gap] * (len(summary_headers) - 1) | 
|  | for i in range(1, width)))) | 
|  | table.append( | 
|  | ['File'] + | 
|  | summary_headers * width) | 
|  |  | 
|  | trace_summaries = sorted(summaries_by_trace.iteritems(), | 
|  | key=lambda t_s: _TraceSortingKey(t_s[0])) | 
|  | for trace, summaries in trace_summaries: | 
|  | row = [os.path.basename(trace.file_path)] | 
|  | for summary in summaries: | 
|  | row += [ | 
|  | (gap if summary.startup_to_event_ms is None | 
|  | else summary.startup_to_event_ms), | 
|  | (gap if summary.navigation_to_event_ms is None | 
|  | else summary.navigation_to_event_ms), | 
|  | summary.duration_ms | 
|  | ] | 
|  | table.append(row) | 
|  |  | 
|  | print() | 
|  | print('\n'.join(separator.join(str(v) for v in row) for row in table)) | 
|  |  | 
|  |  | 
|  | def PrintTrace(trace_file_path, options): | 
|  | trace = ParseTrace(trace_file_path) | 
|  |  | 
|  | def _PrintInterval(name, interval): | 
|  | log('{} (ms): {}', name, interval.FormatAsMilliseconds()) | 
|  |  | 
|  | def _PrintHistogramTime(process, name): | 
|  | time_ns = process.time_ns_by_histogram.get(name) | 
|  | time_ms = None if time_ns is None else time_ns / 1e6 | 
|  | if time_ms is not None or options.print_none_histograms: | 
|  | log('{} (ms): {}', name, time_ms) | 
|  |  | 
|  | histogram_names = [ | 
|  | 'Startup.FirstCommitNavigationTime3.ZoomedIn', | 
|  | 'MobileStartup.ToolbarFirstDrawTime.ChromeTabbedActivity', | 
|  | 'ChromeGeneratedCustomTab.IntentToFirstCommitNavigationTime2.ZoomedIn', | 
|  | 'CustomTabs.IntentToFirstCommitNavigationTime2.ZoomedIn', | 
|  | 'PageLoad.PaintTiming.NavigationToFirstPaint', | 
|  | 'PageLoad.PaintTiming.NavigationToFirstContentfulPaint', | 
|  | 'PageLoad.Experimental.PaintTiming.NavigationToFirstMeaningfulPaint', | 
|  | 'SessionRestore.ForegroundTabFirstPaint3', | 
|  | ] | 
|  |  | 
|  | processes = sorted(trace.process_by_pid.itervalues(), key=lambda p: p.name) | 
|  |  | 
|  | events_regex = None | 
|  | if options.print_events: | 
|  | events_regex = re.compile(options.print_events) | 
|  |  | 
|  | for process in processes: | 
|  | log('{} timings:', process.name) | 
|  | indenter = LogIndenter() | 
|  | indenter.indent() | 
|  |  | 
|  | if process.name == Process.BROWSER_NAME: | 
|  | _PrintInterval('Startup', process.startup_interval) | 
|  | _PrintInterval('StartupToNavigation', | 
|  | trace.startup_to_navigation_interval) | 
|  | _PrintInterval('NavigationToCommit', trace.navigation_to_commit_interval) | 
|  | _PrintInterval('NavigationToContentfulPaint', | 
|  | trace.navigation_to_contentul_paint_interval) | 
|  | _PrintInterval('StartupToContentfulPaint', process.startup_interval.UpTo( | 
|  | trace.navigation_to_contentul_paint_interval)) | 
|  | _PrintInterval('NavigationToMeaningfulPaint', | 
|  | trace.navigation_to_meaningful_paint_interval) | 
|  | _PrintInterval('StartupToMeaningfulPaint', process.startup_interval.UpTo( | 
|  | trace.navigation_to_meaningful_paint_interval)) | 
|  |  | 
|  | if options.experimental: | 
|  | _PrintInterval('First UI interval', process.first_ui_interval) | 
|  |  | 
|  | if process.malloc_counter_by_name: | 
|  | def _PrintMallocCounter(title, value_name, factor): | 
|  | value = process.malloc_counter_by_name.get(value_name) | 
|  | if value is not None: | 
|  | value /= factor | 
|  | log('{}: {}', title, value) | 
|  |  | 
|  | log('Malloc counters:') | 
|  | with LogIndenter(): | 
|  | _PrintMallocCounter('Total time (ms)', 'malloc_time_ns', 1000000) | 
|  | _PrintMallocCounter('Total allocated (KiB)', 'allocated_bytes', 1024) | 
|  | _PrintMallocCounter('Allocations', 'allocation_count', 1) | 
|  | _PrintMallocCounter('Frees', 'free_count', 1) | 
|  |  | 
|  | for histogram_name in histogram_names: | 
|  | _PrintHistogramTime(process, histogram_name) | 
|  |  | 
|  | if events_regex: | 
|  | event_summaries = SummarizeEvents(events_regex, trace, process) | 
|  | if event_summaries: | 
|  | with LogIndenter('Events matching "{}":', events_regex.pattern): | 
|  | for event_summary in event_summaries: | 
|  | with LogIndenter('{}:', event_summary.event.name): | 
|  | if event_summary.startup_to_event_ms is not None: | 
|  | log('StartupToEvent (ms): {}', | 
|  | event_summary.startup_to_event_ms) | 
|  | if event_summary.navigation_to_event_ms is not None: | 
|  | log('NavigationToEvent (ms): {}', | 
|  | event_summary.navigation_to_event_ms) | 
|  | log('Duration (ms): {}', event_summary.duration_ms) | 
|  |  | 
|  | indenter.unindent() | 
|  | log('') | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | parser = argparse.ArgumentParser() | 
|  | parser.add_argument('file_or_glob') | 
|  | parser.add_argument('--print-none-histograms', | 
|  | help='Print histograms with None values.', | 
|  | default=False, action='store_true') | 
|  | # TODO: introduce a variant that takes a list of event names, as escaping | 
|  | #       event names can be tedious. | 
|  | # TODO: match regex against '<index>|<event name>' to allow selecting | 
|  | #       events by index (complicated for begin/end pairs). | 
|  | parser.add_argument('--print-events', | 
|  | help='Print events matching the specified regex.') | 
|  | parser.add_argument('--experimental', | 
|  | default=False, action='store_true', | 
|  | help='Enable experimental stuff.') | 
|  | parser.add_argument('--report', | 
|  | default=False, action='store_true', | 
|  | help='Present information as a tab-separated table.') | 
|  | parser.add_argument('--csv', | 
|  | default=False, action='store_true', | 
|  | help=('Separate report values by commas (not tabs).')) | 
|  |  | 
|  | options = parser.parse_args() | 
|  |  | 
|  | globbed = False | 
|  | if os.path.isfile(options.file_or_glob): | 
|  | trace_file_paths = [options.file_or_glob] | 
|  | else: | 
|  | globbed = True | 
|  | file_pattern = options.file_or_glob | 
|  | trace_file_paths = glob.glob(file_pattern) | 
|  | if not trace_file_paths: | 
|  | file_pattern += '*html' | 
|  | trace_file_paths = glob.glob(file_pattern) | 
|  | if not trace_file_paths: | 
|  | log("'{}' didn't match anything.", file_pattern) | 
|  | return | 
|  | log("'{}' matched {} file(s).", file_pattern, len(trace_file_paths)) | 
|  | log('') | 
|  |  | 
|  | if options.report: | 
|  | PrintReport(trace_file_paths, options) | 
|  | else: | 
|  | for file_path in trace_file_paths: | 
|  | if globbed: | 
|  | log('_' * len(file_path)) | 
|  | log(file_path) | 
|  | log('') | 
|  | PrintTrace(file_path, options) | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | main() |