Skip to content

Commit b9fc778

Browse files
committed
Fixes and more tests
1 parent dfc0d31 commit b9fc778

File tree

5 files changed

+91
-13
lines changed

5 files changed

+91
-13
lines changed

Lib/profiling/sampling/collector.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,16 @@ def extract_lineno(location):
4444

4545
class Collector(ABC):
4646
@abstractmethod
47-
def collect(self, stack_frames, timestamp_us=None):
47+
def collect(self, stack_frames, timestamps_us=None):
4848
"""Collect profiling data from stack frames.
4949
5050
Args:
5151
stack_frames: List of InterpreterInfo objects
52-
timestamp_us: Optional timestamp in microseconds. If provided (from
53-
binary replay), use this instead of current time. If None,
54-
collectors should use time.monotonic() or similar.
52+
timestamps_us: Optional list of timestamps in microseconds. If provided
53+
(from binary replay with RLE batching), use these instead of current
54+
time. If None, collectors should use time.monotonic() or similar.
55+
The list may contain multiple timestamps when samples are batched
56+
together (same stack, different times).
5557
"""
5658

5759
def collect_failed_sample(self):

Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,5 +1012,70 @@ def test_same_thread_id_multiple_interpreters_stress(self):
10121012
)
10131013

10141014

1015+
class TimestampCollector:
1016+
"""Collector that captures timestamps for verification."""
1017+
1018+
def __init__(self):
1019+
self.all_timestamps = []
1020+
1021+
def collect(self, stack_frames, timestamps_us=None):
1022+
if timestamps_us is not None:
1023+
self.all_timestamps.extend(timestamps_us)
1024+
1025+
def export(self, filename):
1026+
pass
1027+
1028+
1029+
class TestTimestampPreservation(BinaryFormatTestBase):
1030+
"""Tests for timestamp preservation during binary round-trip."""
1031+
1032+
def test_timestamp_preservation(self):
1033+
"""Timestamps are preserved during round-trip."""
1034+
frame = make_frame("test.py", 10, "func")
1035+
timestamps = [1000000, 2000000, 3000000]
1036+
1037+
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
1038+
filename = f.name
1039+
self.temp_files.append(filename)
1040+
1041+
collector = BinaryCollector(filename, 1000, compression="none")
1042+
for ts in timestamps:
1043+
sample = [make_interpreter(0, [make_thread(1, [frame])])]
1044+
collector.collect(sample, timestamp_us=ts)
1045+
collector.export(None)
1046+
1047+
ts_collector = TimestampCollector()
1048+
with BinaryReader(filename) as reader:
1049+
count = reader.replay_samples(ts_collector)
1050+
1051+
self.assertEqual(count, 3)
1052+
self.assertEqual(ts_collector.all_timestamps, timestamps)
1053+
1054+
def test_timestamp_preservation_with_rle(self):
1055+
"""RLE-batched samples preserve individual timestamps."""
1056+
frame = make_frame("rle.py", 42, "rle_func")
1057+
1058+
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
1059+
filename = f.name
1060+
self.temp_files.append(filename)
1061+
1062+
# Identical samples (triggers RLE) with different timestamps
1063+
collector = BinaryCollector(filename, 1000, compression="none")
1064+
expected_timestamps = []
1065+
for i in range(50):
1066+
ts = 1000000 + i * 100
1067+
expected_timestamps.append(ts)
1068+
sample = [make_interpreter(0, [make_thread(1, [frame])])]
1069+
collector.collect(sample, timestamp_us=ts)
1070+
collector.export(None)
1071+
1072+
ts_collector = TimestampCollector()
1073+
with BinaryReader(filename) as reader:
1074+
count = reader.replay_samples(ts_collector)
1075+
1076+
self.assertEqual(count, 50)
1077+
self.assertEqual(ts_collector.all_timestamps, expected_timestamps)
1078+
1079+
10151080
if __name__ == "__main__":
10161081
unittest.main()

Modules/_remote_debugging/binary_io_reader.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,14 @@ decode_stack_suffix(ReaderThreadState *ts, const uint8_t *data,
586586

587587
/* Move shared frames (from bottom of stack) to make room for new frames at the top */
588588
if (new_count > 0 && shared > 0) {
589+
/* Defensive check: ensure subtraction won't underflow.
590+
* This should already be guaranteed by the check above, but we add
591+
* this assertion as defense-in-depth against stack corruption. */
592+
if (ts->current_stack_depth < shared) {
593+
PyErr_SetString(PyExc_ValueError,
594+
"Internal error: stack corruption detected in suffix decoding");
595+
return -1;
596+
}
589597
size_t prev_shared_start = ts->current_stack_depth - shared;
590598
memmove(&ts->current_stack[new_count],
591599
&ts->current_stack[prev_shared_start],

Modules/_remote_debugging/binary_io_writer.c

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,12 +447,13 @@ writer_get_or_create_thread_entry(BinaryWriter *writer, uint64_t thread_id,
447447
}
448448

449449
if (writer->thread_count >= writer->thread_capacity) {
450-
writer->thread_entries = grow_array(writer->thread_entries,
451-
&writer->thread_capacity,
452-
sizeof(ThreadEntry));
453-
if (!writer->thread_entries) {
450+
ThreadEntry *new_entries = grow_array(writer->thread_entries,
451+
&writer->thread_capacity,
452+
sizeof(ThreadEntry));
453+
if (!new_entries) {
454454
return NULL;
455455
}
456+
writer->thread_entries = new_entries;
456457
}
457458

458459
ThreadEntry *entry = &writer->thread_entries[writer->thread_count];

Modules/_remote_debugging/module.c

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,11 +1307,13 @@ _remote_debugging_BinaryWriter___exit___impl(BinaryWriterObject *self,
13071307
/*[clinic end generated code: output=61831f47c72a53c6 input=12334ce1009af37f]*/
13081308
{
13091309
if (self->writer) {
1310-
/* Finalize on normal exit */
1311-
if (binary_writer_finalize(self->writer) < 0) {
1312-
binary_writer_destroy(self->writer);
1313-
self->writer = NULL;
1314-
return NULL;
1310+
/* Only finalize on normal exit (no exception) */
1311+
if (exc_type == Py_None) {
1312+
if (binary_writer_finalize(self->writer) < 0) {
1313+
binary_writer_destroy(self->writer);
1314+
self->writer = NULL;
1315+
return NULL;
1316+
}
13151317
}
13161318
binary_writer_destroy(self->writer);
13171319
self->writer = NULL;

0 commit comments

Comments
 (0)