EXPERIMENT: evaluating compression vs interning on Wakelock
NOT FOR REVIEW
Prompt
We are going to do some investigation about interning vs compression.
You'll have to write some python or C++ throwaway scripts to help me with some research on the topic.
Pre-work:
Take the long_trace.pftrace file and extract out only the TracePacket that contain an AppWakelockBundle (defined in app_wakelock_data.proto). Save that in wakelock.pftrace.
Context:
you'll notice that wakelocks are stored in an efficient manner via a union of interning + columnar encoding
- There is an AppWakelockInfo structure which is interned.
- When a wakelock is detected and stored into the trace:
- If the wakelock is new, a new AppWakelockInfo is created and appended to TracePacket.interned_data.app_wakelock_info (see interned_data.proto)
- The index of the newly created intern entry is appended to intern_id
- The timestamp of the even is appended to encoded_ts
- Ignore the other two fiels AppWakelockInfo info = 3; and acquired = 4
So the way you interpret the data is that encoded_ts and intern_id have the same cardinality. intern_id refers to the entry previously emitted in interned_data
Assignment:
I want you to de-intern the trace, and pretend that the AppWakelockInfo was written as a record, in a dumb way, without any interning.
SO I want you to do this:
- Modify TrackEvent and add AppWakelockInfo as one of the possible fields
- Write a python/c++ script that parses the input trace and de-interns the data. FOr each entry in encoded_ts + intern_id, you shall create a new TrackEvent of type instant, with the appopriate timestamp, and same AppWakelockInfo fields (skip the iid field because it's only used for interning)
- Hence create a wakelock_uninterned.pftrace, taking the previous wakelock.pftrace and expanding each wakelock as described above
- Then I want you to update the importer code in trace_processor to import also this file. I want you to verify that you did a good job ensuring that at the trace_processor level, the data looks identical. You'll have to join the slice table with the tracks table, and filter for track of type 'app_wakelock_events', and then check the slice and its args
After you have done all this, I want you to take both traces (wakelock.pftrace and wakelock_uninterned.pftrace) and apply compression as follows. You have to try the combination of the following matrix
zstd levels={10, 15, 19} with block sizes of 512K, 1MB, 2MB, 8MB (that is, apply compression indpendently to blocks of these sizes)
then write me a report with comparing the reults of this compression matrix on both traces in a tabular format
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index ef8620d..e3ede53 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -15429,7 +15429,7 @@
// their default track association) can be emitted as part of a
// TrackEventDefaults message.
//
-// Next reserved id: 13 (up to 15). Next id: 57.
+// Next reserved id: 13 (up to 15). Next id: 58.
message TrackEvent {
// Names of categories of the event. In the client library, categories are a
// way to turn groups of individual events on or off.
@@ -15702,6 +15702,9 @@
optional ChromeMessagePump chrome_message_pump = 35;
optional ChromeMojoEventInfo chrome_mojo_event_info = 38;
+ // De-interned app wakelock info, embedded directly in the TrackEvent.
+ optional AppWakelockInfo app_wakelock_info = 57;
+
// New argument types go here :)
// Extension range for typed events defined in external projects.
diff --git a/protos/perfetto/trace/track_event/track_event.proto b/protos/perfetto/trace/track_event/track_event.proto
index 319c2e2..b5c57b3 100644
--- a/protos/perfetto/trace/track_event/track_event.proto
+++ b/protos/perfetto/trace/track_event/track_event.proto
@@ -16,6 +16,7 @@
syntax = "proto2";
+import "protos/perfetto/trace/android/app_wakelock_data.proto";
import "protos/perfetto/trace/track_event/debug_annotation.proto";
import "protos/perfetto/trace/track_event/log_message.proto";
import "protos/perfetto/trace/track_event/task_execution.proto";
@@ -103,7 +104,7 @@
// their default track association) can be emitted as part of a
// TrackEventDefaults message.
//
-// Next reserved id: 13 (up to 15). Next id: 57.
+// Next reserved id: 13 (up to 15). Next id: 58.
message TrackEvent {
// Names of categories of the event. In the client library, categories are a
// way to turn groups of individual events on or off.
@@ -376,6 +377,9 @@
optional ChromeMessagePump chrome_message_pump = 35;
optional ChromeMojoEventInfo chrome_mojo_event_info = 38;
+ // De-interned app wakelock info, embedded directly in the TrackEvent.
+ optional AppWakelockInfo app_wakelock_info = 57;
+
// New argument types go here :)
// Extension range for typed events defined in external projects.
diff --git a/src/trace_processor/importers/proto/track_event_parser.h b/src/trace_processor/importers/proto/track_event_parser.h
index d7acfa8..3929f3e 100644
--- a/src/trace_processor/importers/proto/track_event_parser.h
+++ b/src/trace_processor/importers/proto/track_event_parser.h
@@ -39,7 +39,7 @@
// TODO(ddrone): replace with a predicate on field id to import new fields
// automatically
static constexpr uint16_t kReflectFields[] = {
- 24, 25, 26, 27, 28, 29, 32, 33, 34, 35, 38, 39, 40, 41, 43, 49, 50};
+ 24, 25, 26, 27, 28, 29, 32, 33, 34, 35, 38, 39, 40, 41, 43, 49, 50, 57};
class PacketSequenceStateGeneration;
class TraceProcessorContext;
diff --git a/tools/compression_benchmark.py b/tools/compression_benchmark.py
new file mode 100644
index 0000000..398b097
--- /dev/null
+++ b/tools/compression_benchmark.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+"""Benchmark compression on trace files with various compressors and parameters.
+
+Compresses input files using zstd/xz/bzip2 at multiple compression levels and
+block sizes (plus full-file), then reports the results in tabular format.
+
+Usage:
+ python3 tools/compression_benchmark.py wakelock.pftrace wakelock_uninterned.pftrace
+"""
+
+import os
+import subprocess
+import sys
+import tempfile
+import time
+
+
+def compress_blocks(input_path, compressor, level, block_size_bytes):
+ """Compress a file block-by-block and return total compressed size.
+
+ Each block is compressed independently. For full-file mode pass
+ block_size_bytes=None.
+ """
+ input_data = open(input_path, 'rb').read()
+ input_size = len(input_data)
+
+ if block_size_bytes is None:
+ blocks = [input_data]
+ else:
+ blocks = [
+ input_data[i:i + block_size_bytes]
+ for i in range(0, input_size, block_size_bytes)
+ ]
+
+ total_compressed = 0
+
+ with tempfile.NamedTemporaryFile(suffix='.cmp', delete=True) as tmp_out, \
+ tempfile.NamedTemporaryFile(suffix='.bin', delete=True) as tmp_in:
+ for block in blocks:
+ tmp_in.seek(0)
+ tmp_in.truncate()
+ tmp_in.write(block)
+ tmp_in.flush()
+
+ if compressor == 'zstd':
+ cmd = ['zstd', f'-{level}', '-f', '-o', tmp_out.name, tmp_in.name]
+ elif compressor == 'xz':
+ cmd = ['xz', f'-{level}', '--keep', '--force', '--stdout', tmp_in.name]
+ elif compressor == 'bzip2':
+ cmd = [
+ 'bzip2', f'-{level}', '--keep', '--force', '--stdout', tmp_in.name
+ ]
+ else:
+ raise ValueError(f"Unknown compressor: {compressor}")
+
+ if compressor in ('xz', 'bzip2'):
+ with open(tmp_out.name, 'wb') as out_f:
+ result = subprocess.run(cmd, stdout=out_f, stderr=subprocess.PIPE)
+ else:
+ result = subprocess.run(cmd, capture_output=True)
+
+ if result.returncode != 0:
+ print(f"{compressor} error: {result.stderr.decode()}", file=sys.stderr)
+ return None
+
+ total_compressed += os.path.getsize(tmp_out.name)
+
+ return total_compressed
+
+
+def format_size(size_bytes):
+ if size_bytes < 1024:
+ return f"{size_bytes} B"
+ elif size_bytes < 1024 * 1024:
+ return f"{size_bytes / 1024:.1f} KB"
+ else:
+ return f"{size_bytes / (1024 * 1024):.2f} MB"
+
+
+def main():
+ if len(sys.argv) < 3:
+ print(f"Usage: {sys.argv[0]} <file1> <file2> [...]", file=sys.stderr)
+ sys.exit(1)
+
+ input_files = sys.argv[1:]
+
+ # (compressor, level, block_size_bytes, block_label)
+ configs = []
+
+ block_sizes = [
+ (512 * 1024, "512K"),
+ (1 * 1024 * 1024, "1MB"),
+ (2 * 1024 * 1024, "2MB"),
+ (8 * 1024 * 1024, "8MB"),
+ (None, "full"),
+ ]
+
+ for comp in ['zstd', 'xz', 'bzip2']:
+ if comp == 'zstd':
+ levels = [10, 15, 19]
+ elif comp == 'xz':
+ levels = [6, 9] # xz default is 6, max useful is 9
+ elif comp == 'bzip2':
+ levels = [9] # bzip2 -9 is the standard choice
+ for level in levels:
+ for bs_bytes, bs_label in block_sizes:
+ configs.append((comp, level, bs_bytes, bs_label))
+
+ # results[fname][(comp, level, bs_label)] = compressed_size
+ results = {}
+ file_sizes = {}
+
+ for input_path in input_files:
+ fname = os.path.basename(input_path)
+ file_size = os.path.getsize(input_path)
+ file_sizes[fname] = file_size
+ results[fname] = {}
+ print(f"\nBenchmarking: {fname} ({format_size(file_size)})")
+
+ for comp, level, bs_bytes, bs_label in configs:
+ config_key = (comp, level, bs_label)
+ label = f"{comp}-{level} / {bs_label}"
+ sys.stdout.write(f" {label:<22} ")
+ sys.stdout.flush()
+
+ t0 = time.time()
+ compressed = compress_blocks(input_path, comp, level, bs_bytes)
+ elapsed = time.time() - t0
+
+ results[fname][config_key] = compressed
+ ratio = compressed / file_size if compressed else 0
+ print(f"{format_size(compressed):>10} "
+ f"({ratio:.4f}x) [{elapsed:.1f}s]")
+
+ # ---- Summary table ----
+ print("\n" + "=" * 90)
+ print("COMPRESSION BENCHMARK RESULTS")
+ print("=" * 90)
+
+ fnames = [os.path.basename(f) for f in input_files]
+
+ # Column widths
+ cfg_w = 22
+ col_w = 20
+
+ # Header row 1: file names
+ print(f"{'':>{cfg_w}}", end="")
+ for fn in fnames:
+ short = fn[:col_w]
+ print(f" {short:>{col_w}}", end="")
+ if len(fnames) == 2:
+ print(f" {'ratio':>{col_w}}", end="")
+ print()
+
+ # Header row 2: Size / Ratio
+ print(f"{'Config':<{cfg_w}}", end="")
+ for _ in fnames:
+ print(f" {'Size':>9} {'Ratio':>9}", end="")
+ if len(fnames) == 2:
+ print(f" {'unintern/intern':>{col_w}}", end="")
+ print()
+ print("-" * (cfg_w + (col_w + 1) * (len(fnames) +
+ (1 if len(fnames) == 2 else 0))))
+
+ # Original row
+ print(f"{'Original':<{cfg_w}}", end="")
+ for fn in fnames:
+ s = file_sizes[fn]
+ print(f" {format_size(s):>9} {'1.0000':>9}", end="")
+ if len(fnames) == 2:
+ r = file_sizes[fnames[1]] / file_sizes[fnames[0]]
+ print(f" {r:>18.2f}x", end="")
+ print()
+
+ prev_comp = None
+ for comp, level, bs_bytes, bs_label in configs:
+ config_key = (comp, level, bs_label)
+ label = f"{comp}-{level} / {bs_label}"
+
+ # Separator between compressors
+ if prev_comp is not None and comp != prev_comp:
+ print()
+ prev_comp = comp
+
+ print(f"{label:<{cfg_w}}", end="")
+ vals = []
+ for fn in fnames:
+ c = results[fn][config_key]
+ vals.append(c)
+ orig = file_sizes[fn]
+ ratio = c / orig
+ print(f" {format_size(c):>9} {ratio:>8.4f}x", end="")
+ if len(fnames) == 2:
+ r = vals[1] / vals[0] if vals[0] else 0
+ print(f" {r:>18.2f}x", end="")
+ print()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/wakelock_extract.py b/tools/wakelock_extract.py
new file mode 100644
index 0000000..442f4d8
--- /dev/null
+++ b/tools/wakelock_extract.py
@@ -0,0 +1,377 @@
+#!/usr/bin/env python3
+"""Extract TracePackets containing AppWakelockBundle from a trace file.
+
+Also de-interns the data, producing a second trace where each wakelock event
+is a standalone TrackEvent with AppWakelockInfo embedded directly.
+
+Usage:
+ python3 tools/wakelock_extract.py long_trace.pftrace
+ # Produces: wakelock.pftrace (interned packets only)
+ # wakelock_uninterned.pftrace (de-interned as TrackEvents)
+"""
+
+import struct
+import sys
+import os
+
+# ---- Low-level protobuf wire format helpers ----
+
+
+def encode_varint(value):
+ """Encode an unsigned integer as a varint."""
+ buf = b''
+ while value > 0x7F:
+ buf += bytes([0x80 | (value & 0x7F)])
+ value >>= 7
+ buf += bytes([value & 0x7F])
+ return buf
+
+
+def decode_varint(data, pos):
+ """Decode a varint from data at pos. Returns (value, new_pos)."""
+ result = 0
+ shift = 0
+ while True:
+ b = data[pos]
+ result |= (b & 0x7F) << shift
+ pos += 1
+ if not (b & 0x80):
+ break
+ shift += 7
+ return result, pos
+
+
+def encode_field_varint(field_num, value):
+ """Encode a varint field."""
+ tag = (field_num << 3) | 0 # wire type 0 = varint
+ return encode_varint(tag) + encode_varint(value)
+
+
+def encode_field_signed_varint(field_num, value):
+ """Encode a signed varint field (using two's complement for negatives)."""
+ if value < 0:
+ value = value + (1 << 64)
+ return encode_field_varint(field_num, value)
+
+
+def encode_field_bytes(field_num, data):
+ """Encode a length-delimited field."""
+ tag = (field_num << 3) | 2 # wire type 2 = length-delimited
+ return encode_varint(tag) + encode_varint(len(data)) + data
+
+
+def encode_field_string(field_num, s):
+ """Encode a string field."""
+ return encode_field_bytes(field_num, s.encode('utf-8'))
+
+
+def zigzag_encode(value):
+ """Encode a signed int as a zigzag varint."""
+ return (value << 1) ^ (value >> 63)
+
+
+def zigzag_decode(value):
+ """Decode a zigzag varint to a signed int."""
+ return (value >> 1) ^ -(value & 1)
+
+
+def parse_proto_fields(data):
+ """Parse protobuf fields from raw bytes. Yields (field_num, wire_type, value)."""
+ pos = 0
+ while pos < len(data):
+ tag, pos = decode_varint(data, pos)
+ field_num = tag >> 3
+ wire_type = tag & 0x7
+ if wire_type == 0: # varint
+ value, pos = decode_varint(data, pos)
+ yield field_num, wire_type, value
+ elif wire_type == 2: # length-delimited
+ length, pos = decode_varint(data, pos)
+ value = data[pos:pos + length]
+ pos += length
+ yield field_num, wire_type, value
+ elif wire_type == 5: # 32-bit
+ value = data[pos:pos + 4]
+ pos += 4
+ yield field_num, wire_type, value
+ elif wire_type == 1: # 64-bit
+ value = data[pos:pos + 8]
+ pos += 8
+ yield field_num, wire_type, value
+ else:
+ raise ValueError(f"Unknown wire type {wire_type} at pos {pos}")
+
+
+def parse_packed_varints(data):
+ """Parse packed repeated varints from bytes."""
+ values = []
+ pos = 0
+ while pos < len(data):
+ v, pos = decode_varint(data, pos)
+ values.append(v)
+ return values
+
+
+def parse_trace_packets(data):
+ """Parse a trace file into individual TracePacket raw bytes.
+ A Perfetto trace is a sequence of field-1 (Trace.packet) entries."""
+ pos = 0
+ while pos < len(data):
+ tag, pos = decode_varint(data, pos)
+ field_num = tag >> 3
+ wire_type = tag & 0x7
+ assert wire_type == 2 and field_num == 1, \
+ f"Expected Trace.packet (field 1, wire type 2), got field={field_num}, wire_type={wire_type}"
+ length, pos = decode_varint(data, pos)
+ packet_bytes = data[pos:pos + length]
+ pos += length
+ yield packet_bytes
+
+
+def write_trace_packet(f, packet_bytes):
+ """Write a single TracePacket to a trace file (as Trace.packet field)."""
+ f.write(encode_field_bytes(1, packet_bytes))
+
+
+# ---- Field number constants ----
+
+# TracePacket fields
+TP_TIMESTAMP = 8
+TP_TIMESTAMP_CLOCK_ID = 58
+TP_TRUSTED_PACKET_SEQUENCE_ID = 10
+TP_SEQUENCE_FLAGS = 13
+TP_INTERNED_DATA = 12
+TP_APP_WAKELOCK_BUNDLE = 116
+TP_TRACK_EVENT = 11
+TP_TRACK_DESCRIPTOR = 60
+
+# AppWakelockBundle fields
+AWB_INTERN_ID = 1
+AWB_ENCODED_TS = 2
+AWB_INFO = 3
+AWB_ACQUIRED = 4
+
+# AppWakelockInfo fields
+AWI_IID = 1
+AWI_TAG = 2
+AWI_FLAGS = 3
+AWI_OWNER_PID = 4
+AWI_OWNER_UID = 5
+AWI_WORK_UID = 6
+
+# InternedData fields
+ID_APP_WAKELOCK_INFO = 42
+
+# TrackEvent fields
+TE_TYPE = 9
+TE_TRACK_UUID = 11
+TE_NAME = 23
+TE_APP_WAKELOCK_INFO = 57 # Added to track_event.proto
+
+# TrackEvent.Type
+TE_TYPE_SLICE_BEGIN = 1
+TE_TYPE_SLICE_END = 2
+TE_TYPE_INSTANT = 3
+
+# TrackDescriptor fields
+TD_UUID = 1
+TD_NAME = 2
+
+
+def parse_app_wakelock_info(data):
+ """Parse an AppWakelockInfo message. Returns dict of fields."""
+ info = {}
+ for field_num, wire_type, value in parse_proto_fields(data):
+ if field_num == AWI_IID:
+ info['iid'] = value
+ elif field_num == AWI_TAG:
+ info['tag'] = value.decode('utf-8') if isinstance(value, bytes) else value
+ elif field_num == AWI_FLAGS:
+ info['flags'] = value
+ elif field_num == AWI_OWNER_PID:
+ info['owner_pid'] = value
+ elif field_num == AWI_OWNER_UID:
+ info['owner_uid'] = value
+ elif field_num == AWI_WORK_UID:
+ info['work_uid'] = value
+ return info
+
+
+def serialize_app_wakelock_info_no_iid(info):
+ """Serialize AppWakelockInfo without the iid field."""
+ buf = b''
+ if 'tag' in info:
+ buf += encode_field_string(AWI_TAG, info['tag'])
+ if 'flags' in info:
+ buf += encode_field_varint(AWI_FLAGS, info['flags'])
+ if 'owner_pid' in info:
+ buf += encode_field_varint(AWI_OWNER_PID, info['owner_pid'])
+ if 'owner_uid' in info:
+ buf += encode_field_varint(AWI_OWNER_UID, info['owner_uid'])
+ if 'work_uid' in info:
+ buf += encode_field_varint(AWI_WORK_UID, info['work_uid'])
+ return buf
+
+
+def main():
+ if len(sys.argv) < 2:
+ print(f"Usage: {sys.argv[0]} <input.pftrace>", file=sys.stderr)
+ sys.exit(1)
+
+ input_path = sys.argv[1]
+ base_dir = os.path.dirname(input_path) or '.'
+ wakelock_path = os.path.join(base_dir, 'wakelock.pftrace')
+ uninterned_path = os.path.join(base_dir, 'wakelock_uninterned.pftrace')
+
+ print(f"Reading {input_path}...")
+ with open(input_path, 'rb') as f:
+ trace_data = f.read()
+ print(f" Read {len(trace_data)} bytes")
+
+ # First pass: extract packets with AppWakelockBundle, build intern table
+ wakelock_packets = []
+ intern_table = {} # iid -> AppWakelockInfo dict
+ total_packets = 0
+ wakelock_event_count = 0
+ seq_id_to_interns = {} # per-sequence interning
+
+ for packet_bytes in parse_trace_packets(trace_data):
+ total_packets += 1
+ has_wakelock_bundle = False
+ packet_ts = None
+ packet_seq_id = None
+ interned_infos = []
+
+ for field_num, wire_type, value in parse_proto_fields(packet_bytes):
+ if field_num == TP_APP_WAKELOCK_BUNDLE:
+ has_wakelock_bundle = True
+ elif field_num == TP_TIMESTAMP:
+ packet_ts = value
+ elif field_num == TP_TRUSTED_PACKET_SEQUENCE_ID:
+ packet_seq_id = value
+ elif field_num == TP_INTERNED_DATA:
+ # Parse interned data for AppWakelockInfo entries
+ for id_field_num, id_wire_type, id_value in parse_proto_fields(value):
+ if id_field_num == ID_APP_WAKELOCK_INFO:
+ info = parse_app_wakelock_info(id_value)
+ interned_infos.append(info)
+
+ if interned_infos:
+ if packet_seq_id not in seq_id_to_interns:
+ seq_id_to_interns[packet_seq_id] = {}
+ for info in interned_infos:
+ if 'iid' in info:
+ seq_id_to_interns[packet_seq_id][info['iid']] = info
+ intern_table[info['iid']] = info
+
+ if has_wakelock_bundle:
+ wakelock_packets.append((packet_bytes, packet_ts, packet_seq_id))
+
+ print(f" Total packets: {total_packets}")
+ print(f" Packets with AppWakelockBundle: {len(wakelock_packets)}")
+ print(f" Total interned AppWakelockInfo entries: {len(intern_table)}")
+
+ # Write wakelock.pftrace (just the wakelock packets, including their interned data)
+ # We need to include all interned data that the wakelock packets reference.
+ # The simplest approach: write the packets as-is (they already contain the interned data).
+ print(f"\nWriting {wakelock_path}...")
+ with open(wakelock_path, 'wb') as f:
+ for packet_bytes, _, _ in wakelock_packets:
+ write_trace_packet(f, packet_bytes)
+ wakelock_size = os.path.getsize(wakelock_path)
+ print(f" Written {wakelock_size} bytes")
+
+ # Second pass: create de-interned trace
+ # For each AppWakelockBundle, expand each (encoded_ts, intern_id) pair into
+ # a separate TrackEvent with the AppWakelockInfo embedded directly.
+ print(f"\nCreating de-interned trace {uninterned_path}...")
+
+ TRACK_UUID = 0x57414B454C4F434B # "WAKELOCK" in hex, arbitrary UUID
+
+ with open(uninterned_path, 'wb') as f:
+ # First, write a TrackDescriptor for the wakelock track
+ td_bytes = b''
+ td_bytes += encode_field_varint(TD_UUID, TRACK_UUID)
+ td_bytes += encode_field_string(TD_NAME, 'app_wakelock_events')
+
+ pkt_bytes = b''
+ pkt_bytes += encode_field_bytes(TP_TRACK_DESCRIPTOR, td_bytes)
+ pkt_bytes += encode_field_varint(TP_TRUSTED_PACKET_SEQUENCE_ID, 1)
+ # SEQ_INCREMENTAL_STATE_CLEARED = 2
+ pkt_bytes += encode_field_varint(TP_SEQUENCE_FLAGS, 2)
+ write_trace_packet(f, pkt_bytes)
+
+ total_events = 0
+
+ for packet_bytes, packet_ts, packet_seq_id in wakelock_packets:
+ # Get the intern table for this sequence
+ seq_interns = seq_id_to_interns.get(packet_seq_id, intern_table)
+
+ for field_num, wire_type, value in parse_proto_fields(packet_bytes):
+ if field_num != TP_APP_WAKELOCK_BUNDLE:
+ continue
+
+ # Parse the bundle
+ intern_ids = []
+ encoded_timestamps = []
+ for b_field_num, b_wire_type, b_value in parse_proto_fields(value):
+ if b_field_num == AWB_INTERN_ID:
+ if isinstance(b_value, bytes):
+ intern_ids = parse_packed_varints(b_value)
+ else:
+ intern_ids.append(b_value)
+ elif b_field_num == AWB_ENCODED_TS:
+ if isinstance(b_value, bytes):
+ encoded_timestamps = parse_packed_varints(b_value)
+ else:
+ encoded_timestamps.append(b_value)
+
+ if len(intern_ids) != len(encoded_timestamps):
+ print(f" WARNING: intern_ids ({len(intern_ids)}) != "
+ f"encoded_ts ({len(encoded_timestamps)})")
+ continue
+
+ for iid, enc_ts in zip(intern_ids, encoded_timestamps):
+ real_ts = (packet_ts or 0) + (enc_ts >> 1)
+ acquired = bool(enc_ts & 1)
+
+ info = seq_interns.get(iid)
+ if info is None:
+ print(f" WARNING: Unknown intern id {iid}")
+ continue
+
+ # Build TrackEvent
+ te_bytes = b''
+ if acquired:
+ te_bytes += encode_field_varint(TE_TYPE, TE_TYPE_SLICE_BEGIN)
+ else:
+ te_bytes += encode_field_varint(TE_TYPE, TE_TYPE_SLICE_END)
+ te_bytes += encode_field_varint(TE_TRACK_UUID, TRACK_UUID)
+ if 'tag' in info:
+ te_bytes += encode_field_string(TE_NAME, info['tag'])
+
+ # Embed AppWakelockInfo (without iid) as field 57
+ wl_info_bytes = serialize_app_wakelock_info_no_iid(info)
+ te_bytes += encode_field_bytes(TE_APP_WAKELOCK_INFO, wl_info_bytes)
+
+ # Build TracePacket
+ pkt = b''
+ pkt += encode_field_varint(TP_TIMESTAMP, real_ts)
+ pkt += encode_field_bytes(TP_TRACK_EVENT, te_bytes)
+ # trusted_packet_sequence_id is required
+ pkt += encode_field_varint(TP_TRUSTED_PACKET_SEQUENCE_ID, 1)
+
+ write_trace_packet(f, pkt)
+ total_events += 1
+
+ uninterned_size = os.path.getsize(uninterned_path)
+ print(f" Written {uninterned_size} bytes ({total_events} events)")
+
+ print(f"\nSummary:")
+ print(f" wakelock.pftrace: {wakelock_size:>10,} bytes")
+ print(f" wakelock_uninterned.pftrace: {uninterned_size:>10,} bytes")
+ print(f" Ratio: {uninterned_size/wakelock_size:.2f}x")
+
+
+if __name__ == '__main__':
+ main()