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()