1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198

tools / mojo_messages_log.py [blame]

#!/usr/bin/env python3
# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os
import sys
from collections import defaultdict

DESCRIPTION = \
'''This script takes in a Chromium trace file and extracts info about Mojo
messages that were sent/received.

Trace files can be created using chrome://tracing or from passing
'--enable-tracing' to a Chrome or browser test executable. In the
chrome://tracing UI, ensure that the 'mojom' and 'toplevel' categories are
selected when setting up a new trace. Also, the trace events available will
have much more information (including message contents and return values) if
the executable generating the trace file is built with the
`extended_tracing_enabled = true` gn arg.
'''

PERFETTO_NOT_FOUND_HELP_TEXT = \
'''Error: perfetto module not found.

This script requires the perfetto Python module. To install it, use something
like `pip install perfetto`, or for Googlers on gLinux use the following (in a
Chromium checkout):
```
sudo apt-get install python3-venv
python3 -m venv venv
./venv/bin/python3 -mpip install perfetto
./venv/bin/python3 tools/mojo_messages_log.py <script args>
```
'''

# Note: Ignore 'mojo::Message::Message' (from the disabled by default 'mojom'
# category) because there is usually higher-level information that's more
# helpful, even in release builds.

# TODO(awillia): The 'Send mojo message' and 'Receive mojo sync reply' trace
# events (both from the toplevel.flow category) should have a message ID
# associated with them but I'm not sure how to access it. With the former we
# could figure out the sender of a message, but without the message ID the
# events aren't very helpful.
MOJO_EVENTS_QUERY = \
'''INCLUDE PERFETTO MODULE slices.with_context;
SELECT
  (ts - (SELECT start_ts FROM trace_bounds)) / 1000000000.0 AS ts_delta,
  process_name,
  pid, -- Useful for distinguishing renderer processes
  thread_name,
  name,
  category AS event_category,
  GROUP_CONCAT(args.key || ": " ||
               COALESCE(args.int_value,
                        args.string_value,
                        args.real_value)) AS parameters
  -- Note that we could get argument type info as well if that's worthwhile
  FROM thread_slice
  LEFT JOIN args on args.arg_set_id = thread_slice.arg_set_id
  WHERE (category IS 'mojom' AND name GLOB 'Send *') OR
        (category IS 'mojom' AND name GLOB 'Call *') OR
        (category IS 'toplevel' AND name GLOB 'Receive *') OR
        (category IS 'toplevel' AND name IS 'Closed mojo endpoint')
  GROUP BY thread_slice.id, args.arg_set_id
  ORDER BY ts;
'''

SUMMARY_FIELDS = ['ts_delta', 'process_name', 'name']

VERBOSE_FIELDS = ['ts_delta', 'process_name', 'pid', 'thread_name', 'name']
ADDITIONAL_DATA_FIELDS = ['name', 'event_category', 'parameters']


def is_valid_path(parser, path):
  if not os.path.exists(path):
    parser.error("Invalid path: %s" % (path))
  else:
    return path


def process_mojo_msg_info(extra, spacing=2):
  if not extra or len(extra) != len(ADDITIONAL_DATA_FIELDS):
    return
  output = ''
  spacer = ' ' * spacing
  event_name, event_category, parameters = extra

  # The parameters exist as a single comma separated line, so break it into
  # separate lines. Each if statement block here corresponds to a WHERE
  # condition in the SQL query.
  if (event_category == 'mojom' and event_name.startswith("Send ")) or \
     (event_category == 'mojom' and event_name.startswith("Call ")):
    if parameters is None:
      # The call has no parameters
      parameters = []
    else:
      assert (parameters.startswith('debug.'))
      parameters = parameters.replace('debug.', '', 1)
      parameters = parameters.split(',debug.')

  elif (event_category == 'toplevel' and event_name.startswith("Receive ")) or \
       (event_category == 'toplevel' and event_name == "Closed mojo endpoint"):
    if parameters is None:
      parameters = []
    elif parameters.startswith('chrome_mojo_event_info.'):
      parameters = parameters.replace('chrome_mojo_event_info.', '', 1)
      parameters = parameters.split(',chrome_mojo_event_info.')
      parameters = ['chrome_mojo_event_info.' + x for x in parameters]
    else:
      assert (parameters.startswith('args.'))
      parameters = parameters.replace('args.', '', 1)
      parameters = parameters.split(',args.')

  results = defaultdict(lambda: [])
  for parameter in parameters:
    info_type, info = parameter.split('.', 1)
    results[info_type].append(info)

  for info_type in results:
    output += spacer + info_type + ':\n'
    for entry in results[info_type]:
      output += spacer * 2 + entry + '\n'
  return output


# Formats the event data into the structured data that can be shown in the
# displayed table and additional unstructured data that should be shown
# underneath each event.
def process_events(args, events):
  rows = []
  extras = []
  for row_data in events:
    row = []
    extra = []
    if args.summary:
      for field in SUMMARY_FIELDS:
        row.append(str(getattr(row_data, field)))
    else:
      for field in VERBOSE_FIELDS:
        row.append(str(getattr(row_data, field)))

      for field in ADDITIONAL_DATA_FIELDS:
        extra.append(getattr(row_data, field))
      extra = process_mojo_msg_info(extra)
    rows.append(row)
    extras.append(extra)
  return rows, extras


try:
  from perfetto.trace_processor import TraceProcessor
except ModuleNotFoundError:
  print(PERFETTO_NOT_FOUND_HELP_TEXT)
  sys.exit(1)


def main():
  import argparse
  parser = argparse.ArgumentParser(
      formatter_class=argparse.RawDescriptionHelpFormatter,
      description=DESCRIPTION)
  parser.add_argument('tracefile',
                      type=lambda path: is_valid_path(parser, path))
  parser.add_argument('--summary', action="store_true")
  args = parser.parse_args()

  tp = TraceProcessor(file_path=args.tracefile)

  results = tp.query(MOJO_EVENTS_QUERY)

  rows, extras = process_events(args, results)

  # Add headers for the table.
  if args.summary:
    rows.insert(0, SUMMARY_FIELDS)
  else:
    rows.insert(0, VERBOSE_FIELDS)
  # Keep `extras` the same length as `rows`.
  extras.insert(0, None)

  # Calculate the appropriate widths of each column.
  widths = [max(map(len, column)) for column in zip(*rows)]

  for i in range(len(rows)):
    row = rows[i]
    extra = extras[i]
    # Format the structured data so the fields align with the table headers.
    out = (value.ljust(width) for value, width in zip(row, widths))
    out = "  ".join(out).rstrip()
    print(out)
    if extra:
      print(extra)


if __name__ == '__main__':
  sys.exit(main())