@@ -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