#!/usr/bin/env python3 import sys import os import re import argparse import statistics import logging import bisect from typing import Any, Dict, List, Optional from collections import defaultdict logger = logging.getLogger("ggml-hexagon-trace") op_pattern = re.compile( r"profile-op\s+(?P[A-Z_0-9+]+):\s+.*?\s+:\s+(?P[\d:x\s\->!]+)\s+:\s+(?P[a-z\d_\s\->x]+)\s+:\s+(?P[\d:x\s\->!]+?)\s+:\s+(?:(?P.*?)\s+:\s+)?(?:op-)?usec\s+(?P\d+)\s+(?:op-)?cycles\s+(?P\d+)(?:\s+start\s+(?P\d+))?(?:\s+mhz\s+(?P[\d.]+))?(?:\s+pmu\s+\[(?P[\d,\s]+)\])?(?:\s+evt\s+\[(?P[\d,\s]+)\])?" ) trace_pattern = re.compile( r"trace-evt\s+(?P[A-Z_0-9\-]+):\s+thread\s+(?P\d+)\s+info\s+(?P\d+)\s+(?Pstart|stop)\s+(?P\d+)" ) device_pattern = re.compile(r"\b(HTP\d+(?::\d+)?)\s+(?:profile-op|trace-evt)\b") def extract_device(line): m = device_pattern.search(line) if m: return m.group(1) return "HTP0" def device_matches(record_device, target_device): targets = [t.strip() for t in target_device.split(',')] for target in targets: if record_device == target: return True if record_device.startswith(target + ":"): return True return False def get_split_output_path(base_path, device_name): safe_device = device_name.replace(':', '_') root, ext = os.path.splitext(base_path) return f"{root}-{safe_device}{ext}" def normalize_event_name(evt_type, info=0): if evt_type == "HVX_COMP": return "V-COMP" if evt_type == "HMX_COMP": return "M-COMP" name = evt_type if name.startswith("HVX_") or name.startswith("HMX_"): name = name[4:] return name.replace("_", "-") class CycleUnwrapper: def __init__(self, initial_val=None): if initial_val is not None: self.last_raw = initial_val & 0xFFFFFFFF self.high_part = initial_val & 0xFFFFFFFF00000000 else: self.last_raw = None self.high_part = 0 def unwrap(self, raw): if self.last_raw is None: self.last_raw = raw return raw diff = raw - self.last_raw if diff < -0x80000000: self.high_part += 0x100000000 elif diff > 0x80000000: self.high_part -= 0x100000000 self.last_raw = raw return raw + self.high_part class DeviceTimeMapper: def __init__(self, dev, ops): self.dev = dev self.batches = [] for op in ops: if op.get('device') == dev and op.get('name') == 'OPBATCH' and op.get('unwrapped_cycles_start') is not None: cycles = op.get('cycles', 0) usec = op.get('usec', 0) start_cyc = op['unwrapped_cycles_start'] freq = (cycles / usec) if usec > 0 and cycles > 0 else 1000.0 if freq <= 0: freq = 1000.0 self.batches.append({ 'start_cycles': start_cyc, 'cycles': cycles, 'end_cycles': start_cyc + cycles, 'usec': usec, 'dur_ns': usec * 1000, 'freq_mhz': freq, }) self.batches.sort(key=lambda b: b['start_cycles']) for i, b in enumerate(self.batches): if i == 0: b['start_time_ns'] = 0 else: prev = self.batches[i - 1] idle_cyc = max(0, b['start_cycles'] - prev['end_cycles']) idle_ns = int(round((idle_cyc / prev['freq_mhz']) * 1000)) b['start_time_ns'] = prev['start_time_ns'] + prev['dur_ns'] + idle_ns self.batch_starts = [b['start_cycles'] for b in self.batches] valid_starts = [op['unwrapped_cycles_start'] for op in ops if op.get('device') == dev and op.get('unwrapped_cycles_start') is not None] self.min_cyc = min(valid_starts) if valid_starts else 0 if self.batches: self.default_freq = self.batches[0]['freq_mhz'] else: freqs = [op['cycles'] / op['usec'] for op in ops if op.get('device') == dev and op.get('usec', 0) > 0 and op.get('cycles', 0) > 0] self.default_freq = statistics.mean(freqs) if freqs else 1000.0 def get_batch(self, cyc): if not self.batches: return None idx = bisect.bisect_right(self.batch_starts, cyc) - 1 if idx >= 0: return self.batches[idx] return self.batches[0] def get_freq(self, cyc=None): if cyc is not None: b = self.get_batch(cyc) if b is not None: return b['freq_mhz'] return self.default_freq def cycle_to_ns(self, cyc): if cyc is None: return 0 b = self.get_batch(cyc) if b is not None: return b['start_time_ns'] + int(round(((cyc - b['start_cycles']) / b['freq_mhz']) * 1000)) return int(round(((cyc - self.min_cyc) / self.default_freq) * 1000)) def dur_cycles_to_ns(self, cyc_start, cyc_dur): if cyc_dur is None: return 0 freq = self.get_freq(cyc_start) return int(round((cyc_dur / freq) * 1000)) def parse_log(file_path, limit=None, device_filter=None, op_filter_re=None): try: if file_path != "-": f = open(file_path, 'r', encoding='utf-8', errors='ignore') else: f = os.fdopen(0, 'r', encoding='utf-8', errors='ignore') except FileNotFoundError: logger.error(f"file '{file_path}' not found.") sys.exit(1) all_ops: List[Dict[str, Any]] = [] all_traces: List[Dict[str, Any]] = [] current_op: Optional[Dict[str, Any]] = None ops_count_per_device = {} if device_filter is not None: for target in device_filter.split(','): ops_count_per_device[target.strip()] = 0 limit_reached = False unwrappers = {} last_batch_start = {} trace_unwrappers = {} line_idx = 0 for line in f: line_idx += 1 if "profile-op" not in line and "trace-evt" not in line: continue device = extract_device(line) idx = line.find("profile-op") if idx != -1 and "|" in line[idx:]: parts = [p.strip() for p in line[idx:].split("|")] prefix = parts[0] prefix_match = re.search(r"profile-op\s+(?P[A-Z_0-9+]+)", prefix) if not prefix_match: continue names = parts[1] if len(parts) == 7: dims, types, strides, params, timings = parts[2], parts[3], parts[4], parts[5], parts[6] elif len(parts) == 6: dims, types, strides, params, timings = parts[2], parts[3], parts[4], "", parts[5] else: continue timing_match = re.search( r"(?:op-)?usec\s+(?P\d+)\s+(?:op-)?cycles\s+(?P\d+)(?:\s+start\s+(?P\d+))?(?:\s+mhz\s+(?P[\d.]+))?(?:\s+pmu\s+\[(?P[\d,\s]+)\])?(?:\s+evt\s+\[(?P[\d,\s]+)\])?", timings ) if not timing_match: continue op_match = timing_match op_name = prefix_match.group("op_name") else: op_match = op_pattern.search(line) if op_match: op_name = op_match.group('op_name') names = "" dims = op_match.group('dims').strip() if op_match.group('dims') else '' types = op_match.group('types').strip() if op_match.group('types') else '' strides = op_match.group('strides').strip() if op_match.group('strides') else '' params = op_match.group('params').strip() if ('params' in op_match.groupdict() and op_match.group('params')) else '' else: op_match = None if op_match: cycles_start_raw = op_match.group('start') unwrapped_cycles_start = None if op_name == "OPBATCH": if cycles_start_raw: unwrapped_cycles_start = int(cycles_start_raw) unwrappers[device] = CycleUnwrapper(unwrapped_cycles_start) last_batch_start[device] = unwrapped_cycles_start for k in list(trace_unwrappers.keys()): if k[0] == device: del trace_unwrappers[k] else: if cycles_start_raw: device_unwrapper = unwrappers.get(device) if device_unwrapper is not None: unwrapped_cycles_start = device_unwrapper.unwrap(int(cycles_start_raw)) op_text = re.sub(r"^profile-op\s+", "", line[idx:]).strip() if idx != -1 else line.strip() evt_str = None if types.startswith("evt-cnt "): evt_str = types[8:].strip() current_op = { 'name': op_name, 'names': names, 'dims': dims, 'types': types, 'strides': strides, 'params': params, 'evt': evt_str, 'op_text': op_text, 'usec': int(op_match.group('usec')), 'cycles': int(op_match.group('cycles')), 'cycles_start': int(cycles_start_raw) if cycles_start_raw else None, 'unwrapped_cycles_start': unwrapped_cycles_start, 'trace_events': [], 'line_num': line_idx, 'device': device } all_ops.append(current_op) # Check if matching early exit criteria matched = False matched_target = None if device_filter is not None: targets = [t.strip() for t in device_filter.split(',')] for target in targets: if device == target or device.startswith(target + ":"): matched = True matched_target = target break else: matched = True matched_target = device if op_filter_re is not None and not op_filter_re.search(op_text): matched = False if matched: if matched_target not in ops_count_per_device: ops_count_per_device[matched_target] = 0 ops_count_per_device[matched_target] += 1 if limit is not None and len(ops_count_per_device) > 0 and all(count >= limit for count in ops_count_per_device.values()): limit_reached = True if limit_reached and op_name == "OPBATCH": break continue trace_match = trace_pattern.search(line) if trace_match: thread = int(trace_match.group('thread')) raw_cyc = int(trace_match.group('cycles')) unwrapped_cyc = None th_key = (device, thread) if th_key not in trace_unwrappers: batch_start = last_batch_start.get(device) trace_unwrappers[th_key] = CycleUnwrapper(batch_start) unwrapped_cyc = trace_unwrappers[th_key].unwrap(raw_cyc) all_traces.append({ 'thread': thread, 'event': trace_match.group('event'), 'info': int(trace_match.group('info')), 'cycles': raw_cyc, 'unwrapped_cycles': unwrapped_cyc, 'state': trace_match.group('state'), 'line_num': line_idx, 'device': device }) f.close() return all_ops, all_traces # --- Simple protobuf encoder --- def write_varint(val): if val < 0: val = (1 << 64) + val res = bytearray() while True: towrite = val & 0x7f val >>= 7 if val > 0: res.append(towrite | 0x80) else: res.append(towrite) break return bytes(res) def pb_field(num, wire, data): return write_varint((num << 3) | wire) + data def pb_varint(num, val): return pb_field(num, 0, write_varint(val)) def pb_length_delimited(num, data): return pb_field(num, 2, write_varint(len(data)) + data) def pb_string(num, text): return pb_length_delimited(num, text.encode('utf-8')) # Message Encoders def make_process_descriptor(pid, name): return pb_varint(1, pid) + pb_string(6, name) def make_thread_descriptor(pid, tid, name, sort_index=None): payload = pb_varint(1, pid) + pb_varint(2, tid) + pb_string(5, name) if sort_index is not None: payload += pb_varint(3, sort_index) return payload def make_track_descriptor(uuid, name=None, parent_uuid=None, thread=None, process=None, sibling_merge_behavior=None, child_ordering=None, sibling_order_rank=None): payload = pb_varint(1, uuid) if name is not None: payload += pb_string(2, name) if parent_uuid is not None: payload += pb_varint(5, parent_uuid) if process is not None: payload += pb_length_delimited(3, process) if thread is not None: payload += pb_length_delimited(4, thread) if sibling_merge_behavior is not None: payload += pb_varint(15, sibling_merge_behavior) if child_ordering is not None: payload += pb_varint(11, child_ordering) if sibling_order_rank is not None: payload += pb_varint(12, sibling_order_rank) return payload def make_debug_annotation(name, string_val=None, int_val=None): payload = pb_string(10, name) if string_val is not None: payload += pb_string(6, string_val) elif int_val is not None: payload += pb_varint(4, int_val) return payload def make_track_event(event_type, track_uuid, name=None, category=None, debug_annotations=None): payload = pb_varint(9, event_type) payload += pb_varint(11, track_uuid) if name is not None: payload += pb_string(23, name) if category is not None: payload += pb_string(22, category) if debug_annotations is not None: for da in debug_annotations: payload += pb_length_delimited(4, da) return payload def make_trace_packet(timestamp, track_event=None, track_descriptor=None, seq_id=1): payload = pb_varint(8, timestamp) payload += pb_varint(10, seq_id) if track_event is not None: payload += pb_length_delimited(11, track_event) if track_descriptor is not None: payload += pb_length_delimited(60, track_descriptor) return payload def write_trace_packet_to_file(f, packet_bytes): # Write as field 1 of top-level Trace message f.write(pb_length_delimited(1, packet_bytes)) # --- End Protobuf Encoder --- def generate_perfetto_trace(filtered_ops, trace_events, output_path): if not filtered_ops: logger.warning("No operators found after filtering.") return # Assign start and end cycles to each operator for op in filtered_ops: op['start_cycles'] = op['unwrapped_cycles_start'] op['end_cycles'] = op['start_cycles'] + op['cycles'] if op['start_cycles'] is not None else None # Get list of unique devices present in the operations unique_devices = sorted(list(set(op['device'] for op in filtered_ops))) device_to_idx = {dev: idx for idx, dev in enumerate(unique_devices)} time_mappers = {dev: DeviceTimeMapper(dev, filtered_ops) for dev in unique_devices} # Process events completed_events = [] if trace_events: trace_events = sorted(trace_events, key=lambda e: e['unwrapped_cycles']) one_usec_cycles = {} for dev in unique_devices: one_usec_cycles[dev] = max(time_mappers[dev].get_freq(), 1.0) active_starts = {} for e in trace_events: t = e['thread'] evt = e['event'] info = e['info'] state = e['state'] cyc = e['unwrapped_cycles'] dev = e['device'] key = (dev, t, evt, info) if state == 'start': # Handle missing stop (start followed by another start) if key in active_starts: prev_e = active_starts[key] completed_events.append({ 'thread': t, 'event': evt, 'info': info, 'start_cyc': prev_e['unwrapped_cycles'], 'end_cyc': prev_e['unwrapped_cycles'] + one_usec_cycles.get(dev, 1000.0), 'line_num': prev_e.get('line_num'), 'missing_stop': True, 'device': dev }) active_starts[key] = e elif state == 'stop': if key in active_starts: prev_e = active_starts[key] del active_starts[key] completed_events.append({ 'thread': t, 'event': evt, 'info': info, 'start_cyc': prev_e['unwrapped_cycles'], 'end_cyc': cyc, 'line_num': prev_e.get('line_num'), 'device': dev }) else: # Handle missing start (stop without start) completed_events.append({ 'thread': t, 'event': evt, 'info': info, 'start_cyc': cyc - one_usec_cycles.get(dev, 1000.0), 'end_cyc': cyc, 'line_num': e.get('line_num'), 'missing_start': True, 'device': dev }) # Clear remaining unmatched starts for key, prev_e in active_starts.items(): dev, t, evt, info = key completed_events.append({ 'thread': t, 'event': evt, 'info': info, 'start_cyc': prev_e['unwrapped_cycles'], 'end_cyc': prev_e['unwrapped_cycles'] + one_usec_cycles.get(dev, 1000.0), 'line_num': prev_e.get('line_num'), 'missing_stop': True, 'device': dev }) completed_events.sort(key=lambda e: e['start_cyc']) # Convert event times to nanoseconds using per-device / per-batch time mapper for e in completed_events: dev = e['device'] tm = time_mappers[dev] e['ts_ns'] = tm.cycle_to_ns(e['start_cyc']) dur_ns = tm.dur_cycles_to_ns(e['start_cyc'], e['end_cyc'] - e['start_cyc']) e['dur_ns'] = max(dur_ns, 100) # Allocate slots (sub-tracks) to prevent overlaps on same virtual track active_slots = defaultdict(list) for e in completed_events: t = e['thread'] evt = e['event'] ts = e['ts_ns'] dur = e['dur_ns'] dev = e['device'] norm_evt = normalize_event_name(evt, e['info']) if norm_evt == "DMA": track_key = (dev, t, "DMA") elif t == 10: track_key = (dev, t, "HMX") else: track_key = (dev, t, "HVX") slots = active_slots[track_key] allocated_slot = -1 for idx, slot_end_ns in enumerate(slots): if ts >= slot_end_ns: slots[idx] = ts + dur allocated_slot = idx break if allocated_slot == -1: slots.append(ts + dur) allocated_slot = len(slots) - 1 e['slot'] = allocated_slot # Generate Track IDs and track definitions used_tracks = {} for e in completed_events: t = e['thread'] evt = e['event'] slot = e['slot'] dev = e['device'] norm_evt = normalize_event_name(evt, e['info']) if norm_evt == "DMA": track_evt = "DMA" evt_id = 1 elif t == 10: track_evt = "HMX" evt_id = 3 else: track_evt = "HVX" evt_id = 2 t_sort = 1 if t == 10 else t + 2 dev_idx = device_to_idx[dev] # Unique UUID for each sub-track if t == 10: uuid = dev_idx * 10000000 + 20 # HMX thread track UUID else: uuid = int(dev_idx * 10000000 + t_sort * 1000000 + evt_id * 1000 + slot) e['uuid'] = uuid used_tracks[uuid] = (dev, t, track_evt, slot) with open(output_path, "wb") as f: for dev in unique_devices: dev_idx = device_to_idx[dev] pid = dev_idx + 1 proc_uuid = dev_idx * 10000000 + 1 # Define Process with EXPLICIT child sorting proc_name = dev proc_desc = make_process_descriptor(pid, proc_name) proc_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(proc_uuid, process=proc_desc, child_ordering=3)) write_trace_packet_to_file(f, proc_packet) # Define Operators Track as a thread track op_track_uuid = dev_idx * 10000000 + 2 op_tid = pid * 100 + 8 op_thread_desc = make_thread_descriptor(pid, op_tid, "Ops", sort_index=1) op_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(op_track_uuid, parent_uuid=proc_uuid, thread=op_thread_desc)) write_trace_packet_to_file(f, op_packet) # Define HMX Thread Track at rank 2 hmx_track_uuid = dev_idx * 10000000 + 20 hmx_tid = pid * 100 + 9 hmx_thread_desc = make_thread_descriptor(pid, hmx_tid, "HMX", sort_index=2) hmx_packet = make_trace_packet(0, track_descriptor=make_track_descriptor(hmx_track_uuid, parent_uuid=proc_uuid, thread=hmx_thread_desc)) write_trace_packet_to_file(f, hmx_packet) # Define Thread Tracks (T0, T1, ..., T9) for this device dev_used_tracks = {uuid: val for uuid, val in used_tracks.items() if val[0] == dev} unique_threads = sorted(list(set(t for (_, t, _, _) in dev_used_tracks.values() if t != 10))) for t in unique_threads: thread_uuid = dev_idx * 10000000 + 10 + t thread_name = f"T{t}" sort_index = 3 + t tid = pid * 100 + 10 + t thread_desc = make_thread_descriptor(pid, tid, thread_name, sort_index=sort_index) thread_packet = make_trace_packet(0, track_descriptor=make_track_descriptor( thread_uuid, parent_uuid=proc_uuid, thread=thread_desc, sibling_order_rank=sort_index, child_ordering=3 # Explicit child sorting for sub-tracks )) write_trace_packet_to_file(f, thread_packet) # Define Track descriptors for sub-tracks parented to thread tracks for uuid in sorted(used_tracks.keys()): dev, t, evt, slot = used_tracks[uuid] dev_idx = device_to_idx[dev] if t == 10: continue name = f"T{t} {evt}" rank = 0 if evt == "HVX" else 1 parent_thread_uuid = dev_idx * 10000000 + 10 + t # Sibling merge behavior: 1 (SIBLING_MERGE_BEHAVIOR_BY_TRACK_NAME) track_desc = make_track_descriptor( uuid=uuid, name=name, parent_uuid=parent_thread_uuid, sibling_merge_behavior=1, sibling_order_rank=rank ) track_packet = make_trace_packet(0, track_descriptor=track_desc) write_trace_packet_to_file(f, track_packet) # Emit Operators last_op_end_ns = defaultdict(int) for op in filtered_ops: dev = op['device'] dev_idx = device_to_idx[dev] tm = time_mappers[dev] op_start_ns = tm.cycle_to_ns(op['start_cycles']) op_dur_ns = tm.dur_cycles_to_ns(op['start_cycles'], op['cycles']) if op['name'] != "OPBATCH": if op_start_ns < last_op_end_ns[dev]: op_start_ns = last_op_end_ns[dev] clamped_dur = max(op_dur_ns, 100) # Clamp to 100ns (0.1us) last_op_end_ns[dev] = op_start_ns + clamped_dur else: clamped_dur = max(op_dur_ns, 100) # Debug annotations for Ops debug_annots = [] if 'line_num' in op: debug_annots.append(make_debug_annotation("line", int_val=op['line_num'])) if 'names' in op and op['names'] and op['names'] != '----': debug_annots.append(make_debug_annotation("names", string_val=op['names'])) if 'strides' in op and op['strides'] and op['strides'] != '----': debug_annots.append(make_debug_annotation("strides", string_val=op['strides'])) if 'params' in op and op['params'] and op['params'] != '----': debug_annots.append(make_debug_annotation("params", string_val=op['params'])) if 'evt' in op and op['evt']: debug_annots.append(make_debug_annotation("evt", string_val=op['evt'])) op_track_uuid = dev_idx * 10000000 + 2 # Slice Begin evt_begin = make_track_event(1, op_track_uuid, name=f"{op['name']} ({op['dims']})", category="operator", debug_annotations=debug_annots) packet_begin = make_trace_packet(op_start_ns, track_event=evt_begin) write_trace_packet_to_file(f, packet_begin) # Slice End evt_end = make_track_event(2, op_track_uuid) packet_end = make_trace_packet(op_start_ns + clamped_dur, track_event=evt_end) write_trace_packet_to_file(f, packet_end) # Emit Thread Trace Events for e in completed_events: norm_name = normalize_event_name(e['event'], e['info']) if norm_name == "DMA": name = f"DMA {e['info']}" elif norm_name == "FENCE": name = f"FENCE {e['info']}" if e.get('info') is not None and e['info'] != 0 else "FENCE" else: name = norm_name if e.get('missing_start') or e.get('missing_stop'): name += "!" debug_annots = [] if 'line_num' in e and e['line_num'] is not None: debug_annots.append(make_debug_annotation("line", int_val=e['line_num'])) if norm_name == "FENCE" and e.get('info') is not None: debug_annots.append(make_debug_annotation("seq", int_val=e['info'])) elif norm_name == "DMA" and e.get('info') is not None: debug_annots.append(make_debug_annotation("channel", int_val=e['info'])) elif e.get('info') is not None and e['info'] != 0: debug_annots.append(make_debug_annotation("info", int_val=e['info'])) if e.get('missing_start'): debug_annots.append(make_debug_annotation("missing_start", string_val="true")) if e.get('missing_stop'): debug_annots.append(make_debug_annotation("missing_stop", string_val="true")) # Slice Begin evt_begin = make_track_event(1, e['uuid'], name=name, category="trace", debug_annotations=debug_annots if debug_annots else None) packet_begin = make_trace_packet(e['ts_ns'], track_event=evt_begin) write_trace_packet_to_file(f, packet_begin) # Slice End evt_end = make_track_event(2, e['uuid']) packet_end = make_trace_packet(e['ts_ns'] + e['dur_ns'], track_event=evt_end) write_trace_packet_to_file(f, packet_end) logger.info(f"Successfully generated Perfetto trace at {output_path}") def main(): parser = argparse.ArgumentParser(description="Convert Hexagon Op profile logs to native Perfetto Protobuf traces.") parser.add_argument("logfile", help="Path to hex-log profile file") parser.add_argument("-o", "--output", default="optrace.perfetto-trace", help="Output trace file path (default: optrace.perfetto-trace)") parser.add_argument("--filter", type=str, help="Regex filter matching against the original profile-op line") parser.add_argument("--device", type=str, help="Device to filter by (e.g. HTP0, HTP0:0) or 'split' to generate separate files per device") group = parser.add_mutually_exclusive_group() group.add_argument("--head", type=int, help="Limit to first N ops") group.add_argument("--tail", type=int, help="Limit to last N ops") args = parser.parse_args() logging.basicConfig(level=logging.INFO, format='%(message)s') op_filter_re = None if args.filter: try: op_filter_re = re.compile(args.filter) except re.error as e: logger.error(f"Invalid regex filter: {e}") sys.exit(1) limit = args.head if args.head is not None else None device_filter = args.device if (args.device and args.device != "split") else None ops, traces = parse_log(args.logfile, limit=limit, device_filter=device_filter, op_filter_re=op_filter_re) if args.device and args.device != "split": ops = [op for op in ops if device_matches(op['device'], args.device)] traces = [t for t in traces if device_matches(t['device'], args.device)] if args.filter: try: filter_re = re.compile(args.filter) except re.error as e: logger.error(f"Invalid regex filter: {e}") sys.exit(1) ops = [op for op in ops if filter_re.search(op['op_text'])] if args.head is not None or args.tail is not None: ops_by_dev = defaultdict(list) for op in ops: ops_by_dev[op['device']].append(op) filtered_ops = [] for dev in sorted(ops_by_dev.keys()): dev_ops = ops_by_dev[dev] if args.head is not None: dev_ops = dev_ops[:args.head] elif args.tail is not None: dev_ops = dev_ops[-args.tail:] filtered_ops.extend(dev_ops) ops = filtered_ops if args.filter or args.head is not None or args.tail is not None: # Group valid ranges by device valid_ranges_by_dev = defaultdict(list) for op in ops: start_cyc = op['unwrapped_cycles_start'] end_cyc = start_cyc + op['cycles'] if start_cyc is not None else None if start_cyc is not None and end_cyc is not None: valid_ranges_by_dev[op['device']].append((start_cyc, end_cyc)) for dev in valid_ranges_by_dev: valid_ranges_by_dev[dev].sort(key=lambda r: r[0]) range_starts_by_dev = {dev: [r[0] for r in ranges] for dev, ranges in valid_ranges_by_dev.items()} filtered_traces = [] for e in traces: cyc = e['unwrapped_cycles'] if cyc is None: continue dev = e['device'] range_starts = range_starts_by_dev.get(dev) if not range_starts: continue idx = bisect.bisect_right(range_starts, cyc) - 1 if idx >= 0: start, end = valid_ranges_by_dev[dev][idx] if start <= cyc <= end: filtered_traces.append(e) traces = filtered_traces if args.device == "split": unique_devices = sorted(list(set(op['device'] for op in ops))) for dev in unique_devices: dev_ops = [op for op in ops if device_matches(op['device'], dev)] dev_traces = [t for t in traces if device_matches(t['device'], dev)] out_path = get_split_output_path(args.output, dev) generate_perfetto_trace(dev_ops, dev_traces, out_path) else: generate_perfetto_trace(ops, traces, args.output) if __name__ == "__main__": main()