
This CL adds a script to print detailed info about Mojo messages given a trace file. Below are some examples of what the script shows (taken from https://docs.google.com/presentation/d/1D2riladajllLuNHkQgiJhEZVlaXavV4KJjldouGVgBo/edit?usp=sharing) For a drag and drop (summary view): ``` ts_delta process_name name ... 0.890756 Renderer Send viz::mojom::CompositorFrameSink::SubmitCompositorFrame 0.891466 GPU Process Receive viz::mojom::CompositorFrameSink::SubmitCompositorFrame 0.892181 Browser Send blink::mojom::FrameWidget::DragTargetDragEnter 0.892277 GPU Process Send viz::mojom::FrameSinkManagerClient::OnFrameTokenChanged 0.892521 Browser Send blink::mojom::FrameWidget::DragTargetDragOver 0.893285 Renderer Receive blink::mojom::FrameWidget::DragTargetDragEnter 0.893322 Browser Send blink::mojom::FrameWidget::DragTargetDrop 0.893826 Renderer Send content::mojom::ChildProcessHost::BindHostReceiver 0.893971 Renderer Call blink::mojom::MimeRegistry::GetMimeTypeFromExtension (sync) ... ``` For a CreateURLLoaderFactory call (detailed view): ``` ts_delta process_name pid thread_name name ... 0.764298 Browser 1 CrBrowserMain Send network::mojom::NetworkContext::CreateURLLoaderFactory input_parameters: url_loader_factory: <value of type ::mojo::PendingReceiver<::network::mojom::URLLoaderFactory>> params.process_id: 5 params.request_initiator_origin_lock: http://127.0.0.1:34789 params.is_corb_enabled: 1 params.ignore_isolated_world_origin: 1 params.unsafe_non_webby_initiator: 0 params.disable_web_security: 0 params.header_client: <value of type ::mojo::PendingRemote<TrustedURLLoaderHeaderClient>> params.isolation_info: <value of type const ::net::IsolationInfo&> params.disable_secure_dns: 0 params.is_trusted: 0 params.automatically_assign_isolation_info: 0 params.provide_loading_state_updates: 1 params.top_frame_id: EDDCF5B62240FE18E3A2899304FFD89F params.factory_override: 0 params.coep_reporter: <value of type ::mojo::PendingRemote<::network::mojom::CrossOriginEmbedderPolicyReporter>> params.cookie_observer: <value of type ::mojo::PendingRemote<::network::mojom::CookieAccessObserver>> params.url_loader_network_observer: <value of type ::mojo::PendingRemote<::network::mojom::URLLoaderNetworkServiceObserver>> params.devtools_observer: <value of type ::mojo::PendingRemote<::network::mojom::DevToolsObserver>> params.trust_token_redemption_policy: kPotentiallyPermit params.debug_tag: RFHI::CommitNavigation params.client_security_state.cross_origin_embedder_policy: <value of type const ::network::CrossOriginEmbedderPolicy&> params.client_security_state.is_web_secure_context: 1 params.client_security_state.ip_address_space: kLocal params.client_security_state.private_network_request_policy: kPreflightBlock ... ``` Bug: 336984033 Change-Id: I0140dcd052e63975d1bea045584462d5f684f4ed Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3370782 Reviewed-by: Alexander Timin <altimin@chromium.org> Commit-Queue: Andrew Williams <awillia@chromium.org> Reviewed-by: Bruce Dawson <brucedawson@chromium.org> Cr-Commit-Position: refs/heads/main@{#1292760}
199 lines
6.5 KiB
Python
Executable File
199 lines
6.5 KiB
Python
Executable File
#!/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())
|