1#!/usr/bin/env python3 2# Copyright 2020 The Pigweed Authors 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may not 5# use this file except in compliance with the License. You may obtain a copy of 6# the License at 7# 8# https://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations under 14# the License. 15r""" 16 17Trace module which creates trace files from a list of trace events. 18 19This is a work in progress, future work will look to add: 20 - Config options to customize output. 21 - A method of providing custom data formatters. 22 - Perfetto support. 23""" 24from enum import Enum 25import json 26import logging 27import struct 28from typing import Iterable, NamedTuple 29 30_LOG = logging.getLogger('pw_trace') 31_ORDERING_CHARS = ("@", "=", "<", ">", "!") 32 33 34class TraceType(Enum): 35 INVALID = 0 36 INSTANTANEOUS = 1 37 INSTANTANEOUS_GROUP = 2 38 ASYNC_START = 3 39 ASYNC_STEP = 4 40 ASYNC_END = 5 41 DURATION_START = 6 42 DURATION_END = 7 43 DURATION_GROUP_START = 8 44 DURATION_GROUP_END = 9 45 46 # TODO(hepler): Remove these aliases for the original style-incompliant 47 # names when users have migrated. 48 DurationStart = 6 # pylint: disable=invalid-name 49 DurationEnd = 7 # pylint: disable=invalid-name 50 51 52class TraceEvent(NamedTuple): 53 event_type: TraceType 54 module: str 55 label: str 56 timestamp_us: float 57 group: str = "" 58 trace_id: int = 0 59 flags: int = 0 60 has_data: bool = False 61 data_fmt: str = "" 62 data: bytes = b'' 63 64 65def event_has_trace_id(event_type): 66 return event_type in { 67 "PW_TRACE_EVENT_TYPE_ASYNC_START", 68 "PW_TRACE_EVENT_TYPE_ASYNC_STEP", 69 "PW_TRACE_EVENT_TYPE_ASYNC_END", 70 } 71 72 73def decode_struct_fmt_args(event): 74 """Decodes the trace's event data for struct-formatted data""" 75 args = {} 76 # we assume all data is packed, little-endian ordering if not specified 77 struct_fmt = event.data_fmt[len("@pw_py_struct_fmt:") :] 78 if not struct_fmt.startswith(_ORDERING_CHARS): 79 struct_fmt = "<" + struct_fmt 80 try: 81 # needed in case the buffer is larger than expected 82 assert struct.calcsize(struct_fmt) == len(event.data) 83 items = struct.unpack_from(struct_fmt, event.data) 84 for i, item in enumerate(items): 85 args["data_" + str(i)] = item 86 except (AssertionError, struct.error): 87 args["error"] = ( 88 f"Mismatched struct/data format {event.data_fmt} " 89 f"expected data len {struct.calcsize(struct_fmt)} " 90 f"data {event.data.hex()} " 91 f"data len {len(event.data)}" 92 ) 93 return args 94 95 96def decode_map_fmt_args(event): 97 """Decodes the trace's event data for map-formatted data""" 98 args = {} 99 fmt = event.data_fmt[len("@pw_py_map_fmt:") :] 100 101 # we assume all data is packed, little-endian ordering if not specified 102 if not fmt.startswith(_ORDERING_CHARS): 103 fmt = '<' + fmt 104 105 try: 106 (fmt_bytes, fmt_list) = fmt.split("{") 107 fmt_list = fmt_list.strip("}").split(",") 108 109 names = [] 110 for pair in fmt_list: 111 (name, fmt_char) = (s.strip() for s in pair.split(":")) 112 names.append(name) 113 fmt_bytes += fmt_char 114 except ValueError: 115 args["error"] = f"Invalid map format {event.data_fmt}" 116 else: 117 try: 118 # needed in case the buffer is larger than expected 119 assert struct.calcsize(fmt_bytes) == len(event.data) 120 items = struct.unpack_from(fmt_bytes, event.data) 121 for i, item in enumerate(items): 122 args[names[i]] = item 123 except (AssertionError, struct.error): 124 args["error"] = ( 125 f"Mismatched map/data format {event.data_fmt} " 126 f"expected data len {struct.calcsize(fmt_bytes)} " 127 f"data {event.data.hex()} " 128 f"data len {len(event.data)}" 129 ) 130 return args 131 132 133def generate_trace_json(events: Iterable[TraceEvent]): 134 """Generates a list of JSON lines from provided trace events.""" 135 json_lines = [] 136 for event in events: 137 if ( 138 event.module is None 139 or event.timestamp_us is None 140 or event.event_type is None 141 or event.label is None 142 ): 143 _LOG.error("Invalid sample") 144 continue 145 146 line = { 147 "pid": event.module, 148 "name": (event.label), 149 "ts": event.timestamp_us, 150 } 151 if event.event_type == TraceType.DURATION_START: 152 line["ph"] = "B" 153 line["tid"] = event.label 154 elif event.event_type == TraceType.DURATION_END: 155 line["ph"] = "E" 156 line["tid"] = event.label 157 elif event.event_type == TraceType.DURATION_GROUP_START: 158 line["ph"] = "B" 159 line["tid"] = event.group 160 elif event.event_type == TraceType.DURATION_GROUP_END: 161 line["ph"] = "E" 162 line["tid"] = event.group 163 elif event.event_type == TraceType.INSTANTANEOUS: 164 line["ph"] = "I" 165 line["s"] = "p" 166 elif event.event_type == TraceType.INSTANTANEOUS_GROUP: 167 line["ph"] = "I" 168 line["s"] = "t" 169 line["tid"] = event.group 170 elif event.event_type == TraceType.ASYNC_START: 171 line["ph"] = "b" 172 line["scope"] = event.group 173 line["tid"] = event.group 174 line["cat"] = event.module 175 line["id"] = event.trace_id 176 line["args"] = {"id": line["id"]} 177 elif event.event_type == TraceType.ASYNC_STEP: 178 line["ph"] = "n" 179 line["scope"] = event.group 180 line["tid"] = event.group 181 line["cat"] = event.module 182 line["id"] = event.trace_id 183 line["args"] = {"id": line["id"]} 184 elif event.event_type == TraceType.ASYNC_END: 185 line["ph"] = "e" 186 line["scope"] = event.group 187 line["tid"] = event.group 188 line["cat"] = event.module 189 line["id"] = event.trace_id 190 line["args"] = {"id": line["id"]} 191 else: 192 _LOG.error("Unknown event type, skipping") 193 continue 194 195 # Handle Data 196 if event.has_data: 197 if event.data_fmt == "@pw_arg_label": 198 line["name"] = event.data.decode("utf-8") 199 elif event.data_fmt == "@pw_arg_group": 200 line["tid"] = event.data.decode("utf-8") 201 elif event.data_fmt == "@pw_arg_counter": 202 line["ph"] = "C" 203 line["args"] = { 204 line["name"]: int.from_bytes(event.data, "little") 205 } 206 elif event.data_fmt.startswith("@pw_py_struct_fmt:"): 207 line["args"] = decode_struct_fmt_args(event) 208 elif event.data_fmt.startswith("@pw_py_map_fmt:"): 209 line["args"] = decode_map_fmt_args(event) 210 else: 211 line["args"] = {"data": event.data.hex()} 212 213 # Encode as JSON 214 json_lines.append(json.dumps(line)) 215 216 return json_lines 217