pw_trace: Add python chrome trace generator
Add a python tool to generate traces which can be viewed using
chrome://tracing.
Change-Id: Ibe2d248e287a9329d00abec6002defb907a1b64e
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/14804
Commit-Queue: Rob Oliver <rgoliver@google.com>
Reviewed-by: (☞゚∀゚)☞ Tennessee Carmel-Veilleux <tennessee@google.com>
diff --git a/pw_trace/docs.rst b/pw_trace/docs.rst
index 39beda7..37e7d89 100644
--- a/pw_trace/docs.rst
+++ b/pw_trace/docs.rst
@@ -179,6 +179,19 @@
- *data* - A pointer to a buffer of arbitrary caller-provided data (void*).
- *size* - The size of the data (size_t).
+Currently the included python tool supports a few different options for
+*data_format_string*:
+
+- *@pw_arg_label* - Uses the string in the data as the trace event label.
+- *@pw_arg_group* - Uses the string in the data as the trace event group.
+- *@pw_arg_counter* - Uses the data as a little endian integer value, and
+ visualizes it as a counter value in the trace (on a graph).
+- *@pw_py_struct_fmt:* - Interprets the string after the ":" as a python struct
+ format string, and uses that format string to unpack the data elements. This
+ can be used to either provide a single value type, or provide multiple
+ different values with a variety of types. Options for format string types can
+ be found here: https://docs.python.org/3/library/struct.html#format-characters
+
.. tip::
It is ok for only one event of a start/end pair to contain data, as long the
@@ -244,8 +257,8 @@
provided.
- *PW_TRACE_FLAGS_DEFAULT*: Default value if no flags are provided.
-- *PW_TRACE_TRACE_ID_DEFAULT*: Default value if not trace_id provided.
-- *PW_TRACE_GROUP_LABEL_DEFAULT*: Default value if not group_label provided.
+- *PW_TRACE_TRACE_ID_DEFAULT*: Default value if no trace_id provided.
+- *PW_TRACE_GROUP_LABEL_DEFAULT*: Default value if no group_label provided.
----------
Sample App
@@ -267,3 +280,18 @@
Jobs are intentionally made to have multiple stages of processing (simulated by
being re-added to the work-queue). This results in multiple jobs being handled
at the same time, the trace_id is used to separate these traces.
+
+----------------------
+Python Trace Generator
+----------------------
+The Python tool is still in early developments, but currently it supports
+generating a list of json lines from a list of trace events.
+
+To view the trace, these lines can be saved to a file and loaded into
+chrome://tracing.
+
+Future work will look to add:
+
+- Config options to customize output.
+- A method of providing custom data formatters.
+- Perfetto support.
diff --git a/pw_trace/py/pw_trace/__init__.py b/pw_trace/py/pw_trace/__init__.py
new file mode 100644
index 0000000..af9ef88
--- /dev/null
+++ b/pw_trace/py/pw_trace/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2020 The Pigweed 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
+#
+# https://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.
+"""Package that supports traces."""
diff --git a/pw_trace/py/pw_trace/trace.py b/pw_trace/py/pw_trace/trace.py
new file mode 100755
index 0000000..f30ba2f
--- /dev/null
+++ b/pw_trace/py/pw_trace/trace.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed 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
+#
+# https://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.
+r"""
+
+Trace module which creates trace files from a list of trace events.
+
+This is a work in progress, future work will look to add:
+ - Config options to customize output.
+ - A method of providing custom data formatters.
+ - Perfetto support.
+"""
+from enum import Enum
+import json
+import logging
+import struct
+from typing import Iterable, NamedTuple
+
+_LOG = logging.getLogger('pw_trace')
+
+
+class TraceType(Enum):
+ Invalid = 0
+ Instantaneous = 1
+ InstantaneousGroup = 2
+ AsyncStart = 3
+ AsyncStep = 4
+ AsyncEnd = 5
+ DurationStart = 6
+ DurationEnd = 7
+ DurationGroupStart = 8
+ DurationGroupEnd = 9
+
+
+class TraceEvent(NamedTuple):
+ event_type: TraceType
+ module: str
+ label: str
+ timestamp: int
+ group: str = ""
+ trace_id: int = 0
+ flags: int = 0
+ has_data: bool = False
+ data_fmt: str = ""
+ data: bytes = b''
+
+
+def event_has_trace_id(event_type):
+ return event_type in {
+ "kPwTraceEvent_AsyncStart", "kPwTraceEvent_AsyncStep",
+ "kPwTraceEvent_AsyncEnd"
+ }
+
+
+def generate_trace_json(events: Iterable[TraceEvent]):
+ """Generates a list of JSON lines from provided trace events."""
+ json_lines = []
+ for event in events:
+ if event.module is None or event.timestamp is None or \
+ event.event_type is None or event.label is None:
+ _LOG.error("Invalid sample")
+ continue
+
+ line = {
+ "pid": event.module,
+ "name": (event.label),
+ "ts": event.timestamp
+ }
+ if event.event_type == TraceType.DurationStart:
+ line["ph"] = "B"
+ line["tid"] = event.label
+ elif event.event_type == TraceType.DurationEnd:
+ line["ph"] = "E"
+ line["tid"] = event.label
+ elif event.event_type == TraceType.DurationGroupStart:
+ line["ph"] = "B"
+ line["tid"] = event.group
+ elif event.event_type == TraceType.DurationGroupEnd:
+ line["ph"] = "E"
+ line["tid"] = event.group
+ elif event.event_type == TraceType.Instantaneous:
+ line["ph"] = "I"
+ line["s"] = "p"
+ elif event.event_type == TraceType.InstantaneousGroup:
+ line["ph"] = "I"
+ line["s"] = "t"
+ line["tid"] = event.group
+ elif event.event_type == TraceType.AsyncStart:
+ line["ph"] = "b"
+ line["scope"] = event.group
+ line["tid"] = event.group
+ line["cat"] = event.module
+ line["id"] = event.trace_id
+ line["args"] = {"id": line["id"]}
+ elif event.event_type == TraceType.AsyncStep:
+ line["ph"] = "n"
+ line["scope"] = event.group
+ line["tid"] = event.group
+ line["cat"] = event.module
+ line["id"] = event.trace_id
+ line["args"] = {"id": line["id"]}
+ elif event.event_type == TraceType.AsyncEnd:
+ line["ph"] = "e"
+ line["scope"] = event.group
+ line["tid"] = event.group
+ line["cat"] = event.module
+ line["id"] = event.trace_id
+ line["args"] = {"id": line["id"]}
+ else:
+ _LOG.error("Unknown event type, skipping")
+ continue
+
+ # Handle Data
+ if event.has_data:
+ if event.data_fmt == "@pw_arg_label":
+ line["name"] = event.data.decode("utf-8")
+ elif event.data_fmt == "@pw_arg_group":
+ line["tid"] = event.data.decode("utf-8")
+ elif event.data_fmt == "@pw_arg_counter":
+ line["ph"] = "C"
+ line["args"] = {
+ line["name"]: int.from_bytes(event.data, "little")
+ }
+ elif event.data_fmt.startswith("@pw_py_struct_fmt:"):
+ items = struct.unpack_from(
+ event.data_fmt[len("@pw_py_struct_fmt:"):], event.data)
+ args = {}
+ for i, item in enumerate(items):
+ args["data_" + str(i)] = item
+ line["args"] = args
+
+ # Encode as JSON
+ json_lines.append(json.dumps(line))
+
+ return json_lines
diff --git a/pw_trace/py/setup.py b/pw_trace/py/setup.py
new file mode 100644
index 0000000..65e6126
--- /dev/null
+++ b/pw_trace/py/setup.py
@@ -0,0 +1,26 @@
+# Copyright 2020 The Pigweed 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
+#
+# https://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.
+"""The pw_trace package."""
+
+import setuptools
+
+setuptools.setup(
+ name='pw_trace',
+ version='0.0.1',
+ author='Pigweed Authors',
+ author_email='pigweed-developers@googlegroups.com',
+ description='Tools for dealing with trace data',
+ packages=setuptools.find_packages(),
+ test_suite='setup.test_suite',
+)
diff --git a/pw_trace/py/trace_test.py b/pw_trace/py/trace_test.py
new file mode 100755
index 0000000..f9bab22
--- /dev/null
+++ b/pw_trace/py/trace_test.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed 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
+#
+# https://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.
+"""Tests the trace module."""
+
+import json
+import struct
+import unittest
+
+from pw_trace import trace
+
+test_events = [
+ trace.TraceEvent(trace.TraceType.Instantaneous, "m1", "L1", 1),
+ trace.TraceEvent(trace.TraceType.InstantaneousGroup, "m2", "L2", 2, "G2"),
+ trace.TraceEvent(trace.TraceType.AsyncStep, "m3", "L3", 3, "G3", 103),
+ trace.TraceEvent(trace.TraceType.DurationStart, "m4", "L4", 4),
+ trace.TraceEvent(trace.TraceType.DurationGroupStart, "m5", "L5", 5, "G5"),
+ trace.TraceEvent(trace.TraceType.AsyncStart, "m6", "L6", 6, "G6", 106),
+ trace.TraceEvent(trace.TraceType.DurationEnd, "m7", "L7", 7),
+ trace.TraceEvent(trace.TraceType.DurationGroupEnd, "m8", "L8", 8, "G8"),
+ trace.TraceEvent(trace.TraceType.AsyncEnd, "m9", "L9", 9, "G9", 109)
+]
+
+test_json = [
+ {"ph": "I", "pid": "m1", "name": "L1", "ts": 1, "s": "p"},
+ {"ph": "I", "pid": "m2", "tid": "G2", "name": "L2", "ts": 2, "s": "t"},
+ {"ph": "n", "pid": "m3", "tid": "G3", "name": "L3", "ts": 3, \
+ "scope": "G3", "cat": "m3", "id": 103, "args": {"id": 103}},
+ {"ph": "B", "pid": "m4", "tid": "L4", "name": "L4", "ts": 4},
+ {"ph": "B", "pid": "m5", "tid": "G5", "name": "L5", "ts": 5},
+ {"ph": "b", "pid": "m6", "tid": "G6", "name": "L6", "ts": 6, \
+ "scope": "G6", "cat": "m6", "id": 106, "args": {"id": 106}},
+ {"ph": "E", "pid": "m7", "tid": "L7", "name": "L7", "ts": 7},
+ {"ph": "E", "pid": "m8", "tid": "G8", "name": "L8", "ts": 8},
+ {"ph": "e", "pid": "m9", "tid": "G9", "name": "L9", "ts": 9, \
+ "scope": "G9", "cat": "m9", "id": 109, "args": {"id": 109}},
+]
+
+
+class TestTraceGenerateJson(unittest.TestCase):
+ """Tests generate json with various events."""
+ def test_generate_single_json_event(self):
+ event = trace.TraceEvent(event_type=trace.TraceType.Instantaneous,
+ module="module",
+ label="label",
+ timestamp=10)
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(json.loads(json_lines[0]), {
+ "ph": "I",
+ "pid": "module",
+ "name": "label",
+ "ts": 10,
+ "s": "p"
+ })
+
+ def test_generate_multiple_json_events(self):
+ json_lines = trace.generate_trace_json(test_events)
+ self.assertEqual(len(test_json), len(json_lines))
+ for actual, expected in zip(json_lines, test_json):
+ self.assertEqual(expected, json.loads(actual))
+
+ def test_generate_json_data_arg_label(self):
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.Instantaneous,
+ module="module",
+ label="", # Is replaced by data string
+ timestamp=10,
+ has_data=True,
+ data_fmt="@pw_arg_label",
+ data=bytes("arg", "utf-8"))
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(json.loads(json_lines[0]), {
+ "ph": "I",
+ "pid": "module",
+ "name": "arg",
+ "ts": 10,
+ "s": "p"
+ })
+
+ def test_generate_json_data_arg_group(self):
+ event = trace.TraceEvent(event_type=trace.TraceType.InstantaneousGroup,
+ module="module",
+ label="label",
+ timestamp=10,
+ has_data=True,
+ data_fmt="@pw_arg_group",
+ data=bytes("arg", "utf-8"))
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]), {
+ "ph": "I",
+ "pid": "module",
+ "name": "label",
+ "tid": "arg",
+ "ts": 10,
+ "s": "t"
+ })
+
+ def test_generate_json_data_counter(self):
+ event = trace.TraceEvent(event_type=trace.TraceType.Instantaneous,
+ module="module",
+ label="counter",
+ timestamp=10,
+ has_data=True,
+ data_fmt="@pw_arg_counter",
+ data=(5).to_bytes(4, byteorder="little"))
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]), {
+ "ph": "C",
+ "pid": "module",
+ "name": "counter",
+ "ts": 10,
+ "s": "p",
+ "args": {
+ "counter": 5
+ }
+ })
+
+ def test_generate_json_data_struct_fmt_single(self):
+ event = trace.TraceEvent(event_type=trace.TraceType.Instantaneous,
+ module="module",
+ label="counter",
+ timestamp=10,
+ has_data=True,
+ data_fmt="@pw_py_struct_fmt:H",
+ data=(5).to_bytes(2, byteorder="little"))
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]), {
+ "ph": "I",
+ "pid": "module",
+ "name": "counter",
+ "ts": 10,
+ "s": "p",
+ "args": {
+ "data_0": 5
+ }
+ })
+
+ def test_generate_json_data_struct_fmt_multi(self):
+ event = trace.TraceEvent(event_type=trace.TraceType.Instantaneous,
+ module="module",
+ label="counter",
+ timestamp=10,
+ has_data=True,
+ data_fmt="@pw_py_struct_fmt:Hl",
+ data=struct.pack("Hl", 5, 2))
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]), {
+ "ph": "I",
+ "pid": "module",
+ "name": "counter",
+ "ts": 10,
+ "s": "p",
+ "args": {
+ "data_0": 5,
+ "data_1": 2
+ }
+ })
+
+
+if __name__ == '__main__':
+ unittest.main()