Skip to content

Commit 8782031

Browse files
committed
Expose the record writer for validating arbitrary files
Adds TestRecordWriter class to enable testing and validation of memray record files by allowing direct writing of test records. This provides a way to create controlled test files with known contents for validating the file format and reader functionality. The implementation supports writing all record types including memory snapshots, allocations, frames, and thread information, making it possible to test edge cases and complex scenarios that would be difficult to trigger in normal operation. Signed-off-by: Pablo Galindo <[email protected]>
1 parent b6fd496 commit 8782031

File tree

7 files changed

+502
-2
lines changed

7 files changed

+502
-2
lines changed

setup.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,12 +267,19 @@ def build_js_files(self):
267267
name="memray._test_utils",
268268
sources=[
269269
"src/memray/_memray_test_utils.pyx",
270+
"src/memray/_memray/sink.cpp",
271+
"src/memray/_memray/records.cpp",
272+
"src/memray/_memray/snapshot.cpp",
273+
"src/memray/_memray/record_writer.cpp",
274+
"src/memray/_memray/hooks.cpp",
275+
"src/memray/_memray/logging.cpp",
270276
],
271277
language="c++",
272278
extra_compile_args=["-std=c++17", "-Wall", *EXTRA_COMPILE_ARGS],
273279
extra_link_args=["-std=c++17", *EXTRA_LINK_ARGS],
274280
define_macros=DEFINE_MACROS,
275281
undef_macros=UNDEF_MACROS,
282+
**library_flags,
276283
)
277284

278285
MEMRAY_INJECT_EXTENSION = Extension(

src/memray/_memray/record_writer.pxd

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
1+
from _memray.records cimport AllocationRecord
12
from _memray.records cimport FileFormat
3+
from _memray.records cimport FramePop
4+
from _memray.records cimport FramePush
5+
from _memray.records cimport HeaderRecord
6+
from _memray.records cimport ImageSegments
7+
from _memray.records cimport MemoryRecord
8+
from _memray.records cimport NativeAllocationRecord
9+
from _memray.records cimport ThreadRecord
10+
from _memray.records cimport UnresolvedNativeFrame
11+
from _memray.records cimport thread_id_t
212
from _memray.sink cimport Sink
313
from libcpp cimport bool
414
from libcpp.memory cimport unique_ptr
515
from libcpp.string cimport string
16+
from libcpp.vector cimport vector
617

718

8-
cdef extern from "record_writer.h" namespace "memray::api":
19+
cdef extern from "record_writer.h" namespace "memray::tracking_api":
920
cdef cppclass RecordWriter:
10-
pass
21+
bool writeRecord(const MemoryRecord& record) except+
22+
bool writeRecord(const UnresolvedNativeFrame& record) except+
23+
bool writeMappings(const vector[ImageSegments]& mappings) except+
24+
bool writeThreadSpecificRecord(thread_id_t tid, const FramePop& record) except+
25+
bool writeThreadSpecificRecord(thread_id_t tid, const FramePush& record) except+
26+
bool writeThreadSpecificRecord(thread_id_t tid, const AllocationRecord& record) except+
27+
bool writeThreadSpecificRecord(thread_id_t tid, const NativeAllocationRecord& record) except+
28+
bool writeThreadSpecificRecord(thread_id_t tid, const ThreadRecord& record) except+
29+
bool writeHeader(bool seek_to_start) except+
30+
bool writeTrailer() except+
31+
void setMainTidAndSkippedFrames(thread_id_t main_tid, size_t skipped_frames_on_main_tid) except+
32+
unique_ptr[RecordWriter] cloneInChildProcess() except+
33+
1134
cdef unique_ptr[RecordWriter] createRecordWriter(
1235
unique_ptr[Sink],
1336
string command_line,

src/memray/_memray/records.pxd

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,69 @@ from libcpp.string cimport string
55
from libcpp.vector cimport vector
66

77

8+
cdef extern from "hooks.h" namespace "memray::hooks":
9+
cdef enum Allocator:
10+
MALLOC "memray::hooks::Allocator::MALLOC"
11+
CALLOC "memray::hooks::Allocator::CALLOC"
12+
REALLOC "memray::hooks::Allocator::REALLOC"
13+
VALLOC "memray::hooks::Allocator::VALLOC"
14+
ALIGNED_ALLOC "memray::hooks::Allocator::ALIGNED_ALLOC"
15+
POSIX_MEMALIGN "memray::hooks::Allocator::POSIX_MEMALIGN"
16+
MEMALIGN "memray::hooks::Allocator::MEMALIGN"
17+
PVALLOC "memray::hooks::Allocator::PVALLOC"
18+
FREE "memray::hooks::Allocator::FREE"
19+
PYMALLOC_MALLOC "memray::hooks::Allocator::PYMALLOC_MALLOC"
20+
PYMALLOC_CALLOC "memray::hooks::Allocator::PYMALLOC_CALLOC"
21+
PYMALLOC_REALLOC "memray::hooks::Allocator::PYMALLOC_REALLOC"
22+
PYMALLOC_FREE "memray::hooks::Allocator::PYMALLOC_FREE"
23+
824
cdef extern from "records.h" namespace "memray::tracking_api":
925
ctypedef unsigned long thread_id_t
1026
ctypedef size_t frame_id_t
27+
ctypedef long long millis_t
28+
29+
struct MemoryRecord:
30+
unsigned long int ms_since_epoch
31+
size_t rss
32+
33+
struct AllocationRecord:
34+
uintptr_t address
35+
size_t size
36+
Allocator allocator
37+
38+
struct NativeAllocationRecord:
39+
uintptr_t address
40+
size_t size
41+
Allocator allocator
42+
frame_id_t native_frame_id
1143

1244
struct Frame:
1345
string function_name
1446
string filename
1547
int lineno
1648

49+
struct FramePush:
50+
frame_id_t frame_id
51+
52+
struct FramePop:
53+
size_t count
54+
55+
struct ThreadRecord:
56+
const char* name
57+
58+
struct Segment:
59+
uintptr_t vaddr
60+
size_t memsz
61+
62+
struct ImageSegments:
63+
string filename
64+
uintptr_t addr
65+
vector[Segment] segments
66+
67+
struct UnresolvedNativeFrame:
68+
uintptr_t ip
69+
frame_id_t index
70+
1771
struct TrackerStats:
1872
size_t n_allocations
1973
size_t n_frames

src/memray/_memray_test_utils.pyx

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,38 @@ from cpython.pylifecycle cimport Py_FinalizeEx
4242
from libc.errno cimport errno
4343
from libc.stdint cimport uintptr_t
4444
from libc.stdlib cimport exit as _exit
45+
from libcpp.utility cimport move
4546
from libcpp.vector cimport vector
4647

4748
from ._destination import Destination
4849

50+
from _memray.record_writer cimport RecordWriter
51+
from _memray.record_writer cimport createRecordWriter
52+
from _memray.records cimport AllocationRecord
53+
from _memray.records cimport Allocator
54+
from _memray.records cimport FileFormat
55+
from _memray.records cimport FramePop
56+
from _memray.records cimport FramePush
57+
from _memray.records cimport HeaderRecord
58+
from _memray.records cimport ImageSegments
59+
from _memray.records cimport MemoryRecord
60+
from _memray.records cimport NativeAllocationRecord
61+
from _memray.records cimport Segment
62+
from _memray.records cimport ThreadRecord
63+
from _memray.records cimport UnresolvedNativeFrame
64+
from _memray.records cimport thread_id_t
65+
from _memray.sink cimport FileSink
66+
from _memray.sink cimport Sink
67+
from cpython.ref cimport PyObject
68+
from libcpp cimport bool
69+
from libcpp.memory cimport unique_ptr
70+
from libcpp.string cimport string
71+
72+
import os
73+
from typing import List
74+
from typing import Optional
75+
from typing import Union
76+
4977

5078
cdef extern from *:
5179
"""
@@ -285,3 +313,123 @@ cdef class PrimeCaches:
285313
return self
286314
def __exit__(self, *args):
287315
sys.setprofile(self.old_profile)
316+
317+
318+
cdef class TestRecordWriter:
319+
"""A Python wrapper around the C++ RecordWriter class for testing purposes."""
320+
321+
cdef unique_ptr[RecordWriter] _writer
322+
cdef unique_ptr[Sink] _sink
323+
cdef bool _native_traces
324+
cdef bool _trace_python_allocators
325+
cdef FileFormat _file_format
326+
327+
def __cinit__(self, str file_path, bool native_traces=False,
328+
bool trace_python_allocators=False,
329+
FileFormat file_format=FileFormat.ALL_ALLOCATIONS):
330+
"""Initialize a new TestRecordWriter.
331+
332+
Args:
333+
file_path: Path to the output file
334+
native_traces: Whether to include native traces
335+
trace_python_allocators: Whether to trace Python allocators
336+
file_format: The format of the output file
337+
"""
338+
self._native_traces = native_traces
339+
self._trace_python_allocators = trace_python_allocators
340+
self._file_format = file_format
341+
342+
# Create the sink
343+
cdef string cpp_path = file_path.encode('utf-8')
344+
try:
345+
self._sink = unique_ptr[Sink](new FileSink(cpp_path, True, False))
346+
except:
347+
raise IOError("Failed to create file sink")
348+
349+
# Create the writer
350+
cdef string command_line = b" ".join(arg.encode('utf-8') for arg in sys.argv)
351+
try:
352+
self._writer = createRecordWriter(
353+
move(self._sink),
354+
command_line,
355+
native_traces,
356+
file_format,
357+
trace_python_allocators
358+
)
359+
except:
360+
raise IOError("Failed to create record writer")
361+
362+
# Write the header
363+
if not self._writer.get().writeHeader(True):
364+
raise RuntimeError("Failed to write header")
365+
366+
def write_memory_record(self, unsigned long ms_since_epoch, size_t rss) -> bool:
367+
"""Write a memory record to the file."""
368+
cdef MemoryRecord record
369+
record.ms_since_epoch = ms_since_epoch
370+
record.rss = rss
371+
return self._writer.get().writeRecord(record)
372+
373+
def write_allocation_record(self, thread_id_t tid, uintptr_t address,
374+
size_t size, unsigned char allocator) -> bool:
375+
"""Write an allocation record to the file."""
376+
cdef AllocationRecord record
377+
record.address = address
378+
record.size = size
379+
record.allocator = <Allocator>allocator
380+
return self._writer.get().writeThreadSpecificRecord(tid, record)
381+
382+
def write_native_allocation_record(self, thread_id_t tid, uintptr_t address,
383+
size_t size, unsigned char allocator,
384+
size_t native_frame_id) -> bool:
385+
"""Write a native allocation record to the file."""
386+
cdef NativeAllocationRecord record
387+
record.address = address
388+
record.size = size
389+
record.allocator = <Allocator>allocator
390+
record.native_frame_id = native_frame_id
391+
return self._writer.get().writeThreadSpecificRecord(tid, record)
392+
393+
def write_frame_push(self, thread_id_t tid, size_t frame_id) -> bool:
394+
"""Write a frame push record to the file."""
395+
cdef FramePush record
396+
record.frame_id = frame_id
397+
return self._writer.get().writeThreadSpecificRecord(tid, record)
398+
399+
def write_frame_pop(self, thread_id_t tid, size_t count) -> bool:
400+
"""Write a frame pop record to the file."""
401+
cdef FramePop record
402+
record.count = count
403+
return self._writer.get().writeThreadSpecificRecord(tid, record)
404+
405+
def write_thread_record(self, thread_id_t tid, str name) -> bool:
406+
"""Write a thread record to the file."""
407+
cdef ThreadRecord record
408+
cdef bytes name_bytes = name.encode('utf-8')
409+
record.name = name_bytes
410+
return self._writer.get().writeThreadSpecificRecord(tid, record)
411+
412+
def write_mappings(self, list mappings) -> bool:
413+
"""Write memory mappings to the file."""
414+
cdef vector[ImageSegments] cpp_mappings
415+
cdef ImageSegments segments
416+
cdef Segment segment
417+
for mapping in mappings:
418+
segments = ImageSegments()
419+
segments.filename = mapping['filename'].encode('utf-8')
420+
segments.addr = mapping['addr']
421+
for seg in mapping['segments']:
422+
segment.vaddr = seg['vaddr']
423+
segment.memsz = seg['memsz']
424+
segments.segments.push_back(segment)
425+
cpp_mappings.push_back(segments)
426+
return self._writer.get().writeMappings(cpp_mappings)
427+
428+
def write_trailer(self) -> bool:
429+
"""Write the trailer to the file."""
430+
return self._writer.get().writeTrailer()
431+
432+
def set_main_tid_and_skipped_frames(self, thread_id_t main_tid,
433+
size_t skipped_frames) -> None:
434+
"""Set the main thread ID and number of skipped frames."""
435+
self._writer.get().setMainTidAndSkippedFrames(main_tid, skipped_frames)

src/memray/_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from ._test_utils import PrimeCaches
66
from ._test_utils import PymallocDomain
77
from ._test_utils import PymallocMemoryAllocator
8+
from ._test_utils import TestRecordWriter
89
from ._test_utils import _cython_allocate_in_two_places
910
from ._test_utils import _cython_nested_allocation
1011
from ._test_utils import allocate_cpp_vector
@@ -64,4 +65,5 @@ def run_in_pthread(self, callback: Callable[[], None]) -> None:
6465
"fill_cpp_vector",
6566
"exit",
6667
"PrimeCaches",
68+
"TestRecordWriter",
6769
]

src/memray/_test_utils.pyi

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Callable
22

3+
from ._memray import FileFormat
34
from ._memray import PymallocDomain as PymallocDomain
45

56
class MemoryAllocator:
@@ -43,3 +44,27 @@ class PrimeCaches:
4344
def __init__(self, size: int) -> None: ...
4445
def __enter__(self) -> None: ...
4546
def __exit__(self, *args: object) -> None: ...
47+
48+
class TestRecordWriter:
49+
def __init__(
50+
self,
51+
file_path: str,
52+
native_traces: bool = False,
53+
trace_python_allocators: bool = False,
54+
file_format: FileFormat = FileFormat.ALL_ALLOCATIONS,
55+
) -> None: ...
56+
def write_memory_record(self, ms_since_epoch: int, rss: int) -> bool: ...
57+
def write_allocation_record(
58+
self, tid: int, address: int, size: int, allocator: int
59+
) -> bool: ...
60+
def write_native_allocation_record(
61+
self, tid: int, address: int, size: int, allocator: int, native_frame_id: int
62+
) -> bool: ...
63+
def write_frame_push(self, tid: int, frame_id: int) -> bool: ...
64+
def write_frame_pop(self, tid: int, count: int) -> bool: ...
65+
def write_thread_record(self, tid: int, name: str) -> bool: ...
66+
def write_mappings(self, mappings: list) -> bool: ...
67+
def write_trailer(self) -> bool: ...
68+
def set_main_tid_and_skipped_frames(
69+
self, main_tid: int, skipped_frames: int
70+
) -> None: ...

0 commit comments

Comments
 (0)