gh-154060: Preserve replay duration and rate (#154101) · python/cpython@7f0ccd6

GitHub

@@ -34,7 +34,7 @@ by 10-50x compared to text formats while also enabling faster I/O.

34343535## File Layout

363637-The file consists of five sections:

37+The file consists of five required sections and one optional extension:

38383939```

4040+------------------+ Offset 0

@@ -47,6 +47,8 @@ The file consists of five sections:

4747| String Table | Variable size

4848+------------------+ frame_table_offset

4949| Frame Table | Variable size

50++------------------+ file_size - 64 (when stats are present)

51+| Profile Stats | 32 bytes (optional)

5052+------------------+ file_size - 32

5153| Footer | 32 bytes (fixed)

5254+------------------+ file_size

@@ -354,6 +356,38 @@ location. Zigzag encoding ensures these small negative values encode

354356efficiently (−1 becomes 1, which is one byte) rather than requiring the

355357maximum varint length.

356358359+## Profile Statistics

360+361+New files can store measured duration, sampling rate, error rate, and missed

362+sample percentage in an optional 56-byte extension immediately before the

363+footer. Older readers ignore these

364+bytes after parsing the declared number of frame-table entries, and newer

365+readers treat a missing extension as unavailable statistics.

366+367+```

368+ Offset Size Type Description

369++--------+------+---------+----------------------------------------+

370+| 0 | 8 | double | Measured duration (seconds) |

371+| 8 | 8 | double | Measured sample rate (samples/second) |

372+| 16 | 8 | double | Failed sample percentage |

373+| 24 | 8 | double | Missed sample percentage |

374+| 32 | 4 | uint32 | Optional field presence flags |

375+| 36 | 4 | uint32 | Reserved |

376+| 40 | 8 | bytes | Signature ("TACHSTAT") |

377+| 48 | 4 | uint32 | Extension version (1) |

378+| 52 | 4 | uint32 | Extension size (56) |

379++--------+------+---------+----------------------------------------+

380+```

381+382+Putting the signature, version, and size at the end lets readers discover

383+the extension from its fixed position relative to the footer while allowing

384+future versions to add fields before that trailer. Multi-byte values use the

385+same native byte order as the rest of the file and are byte-swapped by

386+cross-endian readers.

387+388+Readers also accept the original 32-byte extension, which only contains the

389+duration and sampling rate.

390+357391## Footer

358392359393```

@@ -448,8 +482,9 @@ compress less; higher levels (6+) compress more but slow down writing. Level

4484824. Flush remaining buffered data and finalize compression

4494835. Write the string table (length-prefixed strings in index order)

4504846. Write the frame table (varint-encoded entries in index order)

451-7. Write the footer with final counts

452-8. Seek to offset 0 and write the header with actual values

485+7. Write measured profile statistics, when available

486+8. Write the footer with final counts

487+9. Seek to offset 0 and write the header with actual values

453488454489The writer maintains two dictionaries: one mapping strings to indices, one

455490mapping (filename_idx, funcname_idx, lineno) tuples to frame indices. These

@@ -461,12 +496,13 @@ enable O(1) lookup during interning.

461496 if the magic appears byte-swapped)

4624972. Validate version and read remaining header fields (byte-swapping if needed)

4634983. Seek to end − 32 and read the footer (byte-swapping counts if needed)

464-4. Allocate string array of `string_count` elements

465-5. Parse the string table, populating the array

466-6. Allocate frame array of `frame_count * 3` uint32 elements

467-7. Parse the frame table, populating the array

468-8. If compressed, decompress the sample data region

469-9. Iterate through samples, resolving indices to strings/frames

499+4. Read measured profile statistics when the optional extension is present

500+5. Allocate string array of `string_count` elements

501+6. Parse the string table, populating the array

502+7. Allocate the frame array

503+8. Parse the frame table, populating the array

504+9. If compressed, decompress the sample data region

505+10. Iterate through samples, resolving indices to strings/frames

470506 (byte-swapping thread_id and interpreter_id if needed)

471507472508The reader builds lookup arrays rather than dictionaries since it only needs

@@ -530,11 +566,10 @@ one write() call (or feeds through the compression stream).

530566531567## Future Considerations

532568533-The format reserves space for future extensions. The 12 reserved bytes in

534-the header could hold additional metadata. The 16-byte checksum field in

535-the footer is currently unused. The version field allows incompatible

536-changes with graceful rejection. New compression types could be added

537-(compression_type > 1).

569+The optional profile-statistics block provides an extensible metadata area.

570+The 16-byte checksum field in the footer is currently unused. The version

571+field allows incompatible changes with graceful rejection. New compression

572+types could be added (compression_type > 1).

538573539574Any changes that alter the meaning of existing fields or the parsing logic

540575should increment the version number to prevent older readers from