Skip to content

Commit 0d61331

Browse files
fixup! Store Tachyon samples in bounded columnar blocks
Stack delta state resets at each block boundary, but it was stored on file-wide thread entries. As a result, each thread kept a previous-stack buffer until the writer was destroyed, and block reset scanned every thread seen in the capture. Keep previous stacks, timestamps, and repeat state in the open block's stream entries. File-wide thread entries now only store identity and their current block index, while reset visits only streams from the block being flushed.
1 parent 0730393 commit 0d61331

3 files changed

Lines changed: 67 additions & 42 deletions

File tree

‎Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,20 @@ def test_samples_span_multiple_blocks(self):
11111111
self.assertEqual(reader.replay_samples(collector), len(samples))
11121112
self.assert_samples_equal(samples, collector)
11131113

1114+
def test_thread_returns_in_later_block(self):
1115+
"""A thread from a full block gets a new stream in the next block."""
1116+
samples, _ = self._write_block_spanning_file()
1117+
# Thread 4097 starts the second block in stream 0, which thread 1
1118+
# used in the first block.
1119+
samples.append(samples[0])
1120+
filename = self.create_binary_file(samples)
1121+
self.assertEqual(len(self.read_block_columns(filename)), 2)
1122+
1123+
collector = RawCollector()
1124+
with BinaryReader(filename) as reader:
1125+
self.assertEqual(reader.replay_samples(collector), len(samples))
1126+
self.assert_samples_equal(samples, collector)
1127+
11141128
def test_block_size_limit_starts_new_block(self):
11151129
"""A thread whose samples exceed 1 MiB is split across blocks."""
11161130
# Each sample writes a new 256-frame stack, about 1 KiB while

‎Modules/_remote_debugging/binary_io.h‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ static_assert(PROFILE_STATS_SIZE == 56,
180180
#define INITIAL_FRAME_CAPACITY 4096
181181
#define INITIAL_THREAD_CAPACITY 256
182182
#define INITIAL_BLOCK_CAPACITY 256
183+
#define INITIAL_BLOCK_STREAM_CAPACITY 16
183184

184185
/* ============================================================================
185186
* STATISTICS STRUCTURES
@@ -288,20 +289,24 @@ typedef struct {
288289
uint8_t opcode;
289290
} FrameKey;
290291

291-
/* Thread entry - tracks per-thread state for delta encoding */
292+
/* Thread entry - one per thread seen while writing */
292293
typedef struct {
293294
uint64_t thread_id;
294-
uint64_t prev_timestamp;
295295
uint32_t interpreter_id;
296296

297+
/* Stream index in the current block */
297298
uint32_t block_stream_index;
299+
} ThreadEntry;
300+
301+
typedef struct {
302+
uint64_t prev_timestamp;
303+
size_t thread_index;
298304
uint32_t current_repeat;
299305

300306
/* Previous stack for delta encoding (frame indices, innermost first) */
301-
uint32_t *prev_stack;
302307
size_t prev_stack_depth;
303-
size_t prev_stack_capacity;
304-
} ThreadEntry;
308+
uint32_t prev_stack[MAX_STACK_DEPTH];
309+
} BlockStream;
305310

306311
/* Limit errors occur before emitting an incomplete sample. Other write
307312
* failures may leave partial records and must prevent finalization. */
@@ -356,6 +361,8 @@ typedef struct {
356361
ByteBuffer compressed;
357362
ByteBuffer output;
358363
StackCacheEntry cache[STACK_CACHE_BUCKET_COUNT][STACK_CACHE_BUCKET_SIZE];
364+
BlockStream *streams;
365+
size_t stream_capacity;
359366
uint64_t base_timestamp;
360367
uint32_t sample_count;
361368
uint32_t stream_count;

‎Modules/_remote_debugging/binary_io_writer.c‎

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -482,14 +482,6 @@ writer_get_or_create_thread_entry(BinaryWriter *writer, uint64_t thread_id,
482482
entry->thread_id = thread_id;
483483
entry->interpreter_id = interpreter_id;
484484
entry->block_stream_index = UINT32_MAX;
485-
entry->current_repeat = UINT32_MAX;
486-
entry->prev_stack_capacity = MAX_STACK_DEPTH;
487-
488-
entry->prev_stack = PyMem_Calloc(entry->prev_stack_capacity, sizeof(uint32_t));
489-
if (!entry->prev_stack) {
490-
PyErr_NoMemory();
491-
return NULL;
492-
}
493485

494486
writer->thread_count++;
495487
if (is_new) {
@@ -637,6 +629,11 @@ static void
637629
block_reset(BinaryWriter *writer)
638630
{
639631
SampleBlock *block = writer->block;
632+
/* Only threads with a stream in this block need resetting */
633+
for (uint32_t i = 0; i < block->stream_count; i++) {
634+
size_t thread = block->streams[i].thread_index;
635+
writer->thread_entries[thread].block_stream_index = UINT32_MAX;
636+
}
640637
for (unsigned column = 0; column < COLUMN_COUNT; column++) {
641638
block->columns[column].size = 0;
642639
}
@@ -646,10 +643,6 @@ block_reset(BinaryWriter *writer)
646643
block->max_frame = 0;
647644
block->size = 0;
648645
memset(block->cache, 0, sizeof(block->cache));
649-
for (size_t thread = 0; thread < writer->thread_count; thread++) {
650-
writer->thread_entries[thread].block_stream_index = UINT32_MAX;
651-
writer->thread_entries[thread].current_repeat = UINT32_MAX;
652-
}
653646
}
654647

655648
static unsigned
@@ -780,13 +773,13 @@ stack_hash(const uint32_t *stack, size_t depth)
780773
}
781774

782775
static size_t
783-
common_outer_frames(const ThreadEntry *entry, const uint32_t *stack,
776+
common_outer_frames(const BlockStream *stream, const uint32_t *stack,
784777
size_t depth)
785778
{
786779
size_t shared = 0;
787-
while (shared < depth && shared < entry->prev_stack_depth &&
780+
while (shared < depth && shared < stream->prev_stack_depth &&
788781
stack[depth - 1 - shared] ==
789-
entry->prev_stack[entry->prev_stack_depth - 1 - shared]) {
782+
stream->prev_stack[stream->prev_stack_depth - 1 - shared]) {
790783
shared++;
791784
}
792785
return shared;
@@ -831,24 +824,24 @@ stack_cache_insert(StackCacheEntry *bucket, StackCacheEntry entry)
831824
}
832825

833826
static int
834-
write_stack_repeat(BinaryWriter *writer, ThreadEntry *entry, size_t depth)
827+
write_stack_repeat(BinaryWriter *writer, BlockStream *stream, size_t depth)
835828
{
836829
SampleBlock *block = writer->block;
837-
if (entry->current_repeat == UINT32_MAX) {
830+
if (stream->current_repeat == UINT32_MAX) {
838831
Repeat repeat = {
839832
(uint32_t)block->columns[COLUMN_STACK_INSTRUCTIONS].size, 1};
840833
if (buffer_append(&block->repeats,
841834
&repeat, sizeof(repeat)) < 0) {
842835
return -1;
843836
}
844837
block->size += sizeof(repeat);
845-
entry->current_repeat =
838+
stream->current_repeat =
846839
(uint32_t)(block->repeats.size / sizeof(repeat) - 1);
847840
writer->stats.repeat_records++;
848841
}
849842
else {
850843
Repeat *repeats = (Repeat *)block->repeats.data;
851-
repeats[entry->current_repeat].count++;
844+
repeats[stream->current_repeat].count++;
852845
}
853846
writer->stats.repeat_samples++;
854847
writer->stats.frames_saved += depth;
@@ -923,15 +916,15 @@ write_stack_replace_suffix(BinaryWriter *writer, StackCacheEntry *bucket,
923916
}
924917

925918
static int
926-
write_stack(BinaryWriter *writer, ThreadEntry *entry, int new_stream,
919+
write_stack(BinaryWriter *writer, BlockStream *stream, int new_stream,
927920
const uint32_t *stack, size_t depth)
928921
{
929922
SampleBlock *block = writer->block;
930-
size_t shared = common_outer_frames(entry, stack, depth);
923+
size_t shared = common_outer_frames(stream, stack, depth);
931924
/* Same stack as this stream's previous sample: extend its REPEAT */
932925
if (!new_stream && shared == depth &&
933-
shared == entry->prev_stack_depth) {
934-
return write_stack_repeat(writer, entry, depth);
926+
shared == stream->prev_stack_depth) {
927+
return write_stack_repeat(writer, stream, depth);
935928
}
936929

937930
/* Enough new frames to be worth looking up an earlier complete stack */
@@ -958,9 +951,9 @@ write_stack(BinaryWriter *writer, ThreadEntry *entry, int new_stream,
958951
if (result < 0) {
959952
return -1;
960953
}
961-
entry->current_repeat = UINT32_MAX;
962-
memcpy(entry->prev_stack, stack, depth * sizeof(uint32_t));
963-
entry->prev_stack_depth = depth;
954+
stream->current_repeat = UINT32_MAX;
955+
memcpy(stream->prev_stack, stack, depth * sizeof(uint32_t));
956+
stream->prev_stack_depth = depth;
964957
return 0;
965958
}
966959

@@ -995,6 +988,10 @@ static int
995988
block_add_stream(BinaryWriter *writer, ThreadEntry *entry)
996989
{
997990
SampleBlock *block = writer->block;
991+
if (GROW_ARRAY(block->streams, block->stream_count,
992+
block->stream_capacity, BlockStream) < 0) {
993+
return -1;
994+
}
998995
/* Stream table entry: thread_id (8 bytes) + interpreter_id (4 bytes) */
999996
uint8_t stream_entry[STREAM_ENTRY_SIZE];
1000997
memcpy(stream_entry + STM_OFF_THREAD_ID,
@@ -1005,9 +1002,12 @@ block_add_stream(BinaryWriter *writer, ThreadEntry *entry)
10051002
stream_entry, sizeof(stream_entry)) < 0) {
10061003
return -1;
10071004
}
1005+
BlockStream *stream = &block->streams[block->stream_count];
1006+
stream->thread_index = (size_t)(entry - writer->thread_entries);
1007+
stream->prev_timestamp = block->base_timestamp - writer->sample_interval_us;
1008+
stream->current_repeat = UINT32_MAX;
1009+
stream->prev_stack_depth = 0;
10081010
entry->block_stream_index = block->stream_count++;
1009-
entry->prev_timestamp = block->base_timestamp - writer->sample_interval_us;
1010-
entry->prev_stack_depth = 0;
10111011
return 0;
10121012
}
10131013

@@ -1036,9 +1036,10 @@ block_add_sample(BinaryWriter *writer, ThreadEntry *entry,
10361036
if (new_stream && block_add_stream(writer, entry) < 0) {
10371037
return -1;
10381038
}
1039+
BlockStream *stream = &block->streams[entry->block_stream_index];
10391040
/* Store the difference from the expected timestamp */
10401041
uint64_t residual =
1041-
timestamp - entry->prev_timestamp - writer->sample_interval_us;
1042+
timestamp - stream->prev_timestamp - writer->sample_interval_us;
10421043
uint8_t residual_bytes[MAX_VARINT_SIZE];
10431044
size_t residual_size =
10441045
encode_varint_i64(residual_bytes, (int64_t)residual);
@@ -1050,10 +1051,10 @@ block_add_sample(BinaryWriter *writer, ThreadEntry *entry,
10501051
&status, sizeof(status)) < 0) {
10511052
return -1;
10521053
}
1053-
if (write_stack(writer, entry, new_stream, stack, depth) < 0) {
1054+
if (write_stack(writer, stream, new_stream, stack, depth) < 0) {
10541055
return -1;
10551056
}
1056-
entry->prev_timestamp = timestamp;
1057+
stream->prev_timestamp = timestamp;
10571058
block->sample_count++;
10581059
writer->total_samples++;
10591060
return 0;
@@ -1072,6 +1073,7 @@ block_destroy(SampleBlock *block)
10721073
buffer_clear(&block->transformed);
10731074
buffer_clear(&block->compressed);
10741075
buffer_clear(&block->output);
1076+
PyMem_Free(block->streams);
10751077
PyMem_Free(block);
10761078
}
10771079

@@ -1102,6 +1104,13 @@ binary_writer_create(PyObject *path, uint64_t sample_interval_us, int compressio
11021104
PyErr_NoMemory();
11031105
goto error;
11041106
}
1107+
writer->block->streams = PyMem_Malloc(
1108+
INITIAL_BLOCK_STREAM_CAPACITY * sizeof(BlockStream));
1109+
if (writer->block->streams == NULL) {
1110+
PyErr_NoMemory();
1111+
goto error;
1112+
}
1113+
writer->block->stream_capacity = INITIAL_BLOCK_STREAM_CAPACITY;
11051114

11061115
writer->string_hash = _Py_hashtable_new_full(
11071116
string_hash_func,
@@ -1591,12 +1600,7 @@ binary_writer_destroy(BinaryWriter *writer)
15911600
}
15921601
PyMem_Free(writer->frame_entries);
15931602

1594-
if (writer->thread_entries) {
1595-
for (size_t i = 0; i < writer->thread_count; i++) {
1596-
PyMem_Free(writer->thread_entries[i].prev_stack);
1597-
}
1598-
PyMem_Free(writer->thread_entries);
1599-
}
1603+
PyMem_Free(writer->thread_entries);
16001604

16011605
PyMem_Free(writer);
16021606
}

0 commit comments

Comments
 (0)