Skip to content

Commit 37f2b5b

Browse files
fixup! Store Tachyon samples in bounded columnar blocks
The stack reuse cache currently keeps one stack at each hash position, so a collision immediately evicts the previous stack. Alternating between two colliding stacks therefore prevents either from being reused. Let each cache bucket remember two stacks while keeping the cache the same overall size. Lookups check both stacks, and inserting a third stack replaces the older one.
1 parent 470c558 commit 37f2b5b

3 files changed

Lines changed: 69 additions & 11 deletions

File tree

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,38 @@ def test_writer_reports_stack_reuse(self):
10191019
self.assertEqual(stats["total_samples"], 2)
10201020
writer.close()
10211021

1022+
def test_writer_reuses_colliding_stacks(self):
1023+
"""Stacks sharing a cache bucket are both stored as STACK_REUSE."""
1024+
buckets = {}
1025+
for frame_id in range(2, 4099):
1026+
bucket = hash(struct.pack("=3I", 0, 1, frame_id)) % 4096
1027+
if bucket in buckets:
1028+
first_id = buckets[bucket]
1029+
second_id = frame_id
1030+
break
1031+
buckets[bucket] = frame_id
1032+
1033+
frames = [make_frame("a.py", line, "f")
1034+
for line in range(second_id + 1)]
1035+
first = [frames[0], frames[1], frames[first_id]]
1036+
second = [frames[0], frames[1], frames[second_id]]
1037+
1038+
filename = self.temp_filename()
1039+
writer = _remote_debugging.BinaryWriter(filename, 1000, 0)
1040+
timestamp = 1000
1041+
# Assign frame IDs without exceeding MAX_STACK_DEPTH.
1042+
for start in range(0, len(frames), 256):
1043+
stack = frames[start:start + 256]
1044+
writer.write_sample([make_interpreter(0, [make_thread(2, stack)])],
1045+
timestamp)
1046+
timestamp += 1000
1047+
for stack in (first, second, first, second):
1048+
writer.write_sample([make_interpreter(0, [make_thread(1, stack)])],
1049+
timestamp)
1050+
timestamp += 1000
1051+
self.assertEqual(writer.get_stats()["reuse_records"], 2)
1052+
writer.close()
1053+
10221054
def test_writer_buffered_bytes_match_flushed_bytes(self):
10231055
"""bytes_written before finalize() matches the flushed size."""
10241056
filename = self.temp_filename()

‎Modules/_remote_debugging/binary_io.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ static_assert(PROFILE_STATS_SIZE == 56,
171171

172172
/* Writer's stack cache size per block */
173173
#define STACK_CACHE_SIZE 4096
174+
#define STACK_CACHE_BUCKET_SIZE 2
175+
#define STACK_CACHE_BUCKET_COUNT \
176+
(STACK_CACHE_SIZE / STACK_CACHE_BUCKET_SIZE)
174177

175178
/* Initial capacities for dynamic arrays - sized to reduce reallocations */
176179
#define INITIAL_STRING_CAPACITY 4096
@@ -352,7 +355,7 @@ typedef struct {
352355
ByteBuffer transformed;
353356
ByteBuffer compressed;
354357
ByteBuffer output;
355-
StackCacheEntry cache[STACK_CACHE_SIZE];
358+
StackCacheEntry cache[STACK_CACHE_BUCKET_COUNT][STACK_CACHE_BUCKET_SIZE];
356359
uint64_t base_timestamp;
357360
uint32_t sample_count;
358361
uint32_t stream_count;

‎Modules/_remote_debugging/binary_io_writer.c‎

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,26 @@ cached_stack_matches(const StackCacheEntry *cached,
810810
return 1;
811811
}
812812

813+
static StackCacheEntry *
814+
stack_cache_lookup(StackCacheEntry *bucket, const ByteBuffer *frames,
815+
Py_uhash_t hash, const uint32_t *stack, size_t depth)
816+
{
817+
for (size_t i = 0; i < STACK_CACHE_BUCKET_SIZE; i++) {
818+
if (cached_stack_matches(&bucket[i], frames, hash, stack, depth)) {
819+
return &bucket[i];
820+
}
821+
}
822+
return NULL;
823+
}
824+
825+
static void
826+
stack_cache_insert(StackCacheEntry *bucket, StackCacheEntry entry)
827+
{
828+
memmove(&bucket[1], &bucket[0],
829+
(STACK_CACHE_BUCKET_SIZE - 1) * sizeof(*bucket));
830+
bucket[0] = entry;
831+
}
832+
813833
static int
814834
write_stack_repeat(BinaryWriter *writer, ThreadEntry *entry, size_t depth)
815835
{
@@ -853,7 +873,7 @@ write_stack_reuse(BinaryWriter *writer,
853873
}
854874

855875
static int
856-
write_stack_replace_suffix(BinaryWriter *writer, StackCacheEntry *cached,
876+
write_stack_replace_suffix(BinaryWriter *writer, StackCacheEntry *bucket,
857877
Py_uhash_t hash, const uint32_t *stack,
858878
size_t depth, size_t shared)
859879
{
@@ -888,9 +908,9 @@ write_stack_replace_suffix(BinaryWriter *writer, StackCacheEntry *cached,
888908
}
889909
/* Complete stacks are cached so later samples can REUSE them */
890910
if (shared == 0) {
891-
if (cached != NULL) {
892-
*cached = (StackCacheEntry){
893-
hash, (uint32_t)stack_offset, (uint32_t)depth};
911+
if (bucket != NULL) {
912+
stack_cache_insert(bucket, (StackCacheEntry){
913+
hash, (uint32_t)stack_offset, (uint32_t)depth});
894914
}
895915
writer->stats.full_records++;
896916
}
@@ -906,10 +926,11 @@ static int
906926
choose_stack_encoding(SampleBlock *block, const ThreadEntry *entry,
907927
int new_stream, const uint32_t *stack, size_t depth,
908928
size_t *shared, Py_uhash_t *hash,
909-
StackCacheEntry **cached)
929+
StackCacheEntry **bucket, StackCacheEntry **cached)
910930
{
911931
*shared = common_outer_frames(entry, stack, depth);
912932
*hash = 0;
933+
*bucket = NULL;
913934
*cached = NULL;
914935
if (!new_stream && *shared == depth &&
915936
*shared == entry->prev_stack_depth) {
@@ -919,9 +940,10 @@ choose_stack_encoding(SampleBlock *block, const ThreadEntry *entry,
919940
return STACK_REPLACE_SUFFIX;
920941
}
921942
*hash = stack_hash(stack, depth);
922-
*cached = &block->cache[*hash % STACK_CACHE_SIZE];
923-
if (cached_stack_matches(*cached, &block->columns[COLUMN_FRAME_IDS],
924-
*hash, stack, depth)) {
943+
*bucket = block->cache[*hash % STACK_CACHE_BUCKET_COUNT];
944+
*cached = stack_cache_lookup(*bucket, &block->columns[COLUMN_FRAME_IDS],
945+
*hash, stack, depth);
946+
if (*cached != NULL) {
925947
return STACK_REUSE;
926948
}
927949
return STACK_REPLACE_SUFFIX;
@@ -933,10 +955,11 @@ write_stack(BinaryWriter *writer, ThreadEntry *entry, int new_stream,
933955
{
934956
size_t shared;
935957
Py_uhash_t hash;
958+
StackCacheEntry *bucket;
936959
StackCacheEntry *cached;
937960
int encoding = choose_stack_encoding(writer->block, entry, new_stream,
938961
stack, depth, &shared, &hash,
939-
&cached);
962+
&bucket, &cached);
940963

941964
int result;
942965
switch (encoding) {
@@ -946,7 +969,7 @@ write_stack(BinaryWriter *writer, ThreadEntry *entry, int new_stream,
946969
result = write_stack_reuse(writer, cached, depth);
947970
break;
948971
case STACK_REPLACE_SUFFIX:
949-
result = write_stack_replace_suffix(writer, cached, hash, stack,
972+
result = write_stack_replace_suffix(writer, bucket, hash, stack,
950973
depth, shared);
951974
break;
952975
default:

0 commit comments

Comments
 (0)