diff --git a/Makefile b/Makefile index 936a9900ba..58f4fbe2b1 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ js_files := $(wildcard $(reporters_path)/assets/*.js) generated_js_files := \ $(reporters_path)/templates/assets/flamegraph_common.js \ $(reporters_path)/templates/assets/flamegraph.js \ + $(reporters_path)/templates/assets/temporal_flamegraph.js \ $(reporters_path)/templates/assets/table.js css_files := 'src/**/*.css' markdown_files := $(shell find . -name \*.md -not -path '*/\.*' -not -path './src/vendor/*') diff --git a/src/memray/_memray.pyi b/src/memray/_memray.pyi index 9cbbfa1e90..a3d50072a3 100644 --- a/src/memray/_memray.pyi +++ b/src/memray/_memray.pyi @@ -42,6 +42,10 @@ class AllocationRecord: @property def tid(self) -> int: ... @property + def native_stack_id(self) -> int: ... + @property + def native_segment_generation(self) -> int: ... + @property def thread_name(self) -> str: ... def hybrid_stack_trace( self, @@ -61,6 +65,47 @@ class AllocationRecord: def __lt__(self, other: Any) -> Any: ... def __ne__(self, other: Any) -> Any: ... +class Interval: + def __init__( + self, + allocated_before_snapshot: int, + deallocated_before_snapshot: int | None, + n_allocations: int, + n_bytes: int, + ) -> None: ... + def __eq__(self, other: Any) -> Any: ... + allocated_before_snapshot: int + deallocated_before_snapshot: int | None + n_allocations: int + n_bytes: int + +class TemporalAllocationRecord: + @property + def allocator(self) -> int: ... + @property + def stack_id(self) -> int: ... + @property + def tid(self) -> int: ... + @property + def native_stack_id(self) -> int: ... + @property + def native_segment_generation(self) -> int: ... + @property + def thread_name(self) -> str: ... + def hybrid_stack_trace( + self, + max_stacks: Optional[int] = None, + ) -> List[Union[PythonStackElement, NativeStackElement]]: ... + def native_stack_trace( + self, max_stacks: Optional[int] = None + ) -> List[NativeStackElement]: ... + def stack_trace( + self, max_stacks: Optional[int] = None + ) -> List[PythonStackElement]: ... + def __eq__(self, other: Any) -> Any: ... + def __hash__(self) -> Any: ... + intervals: List[Interval] + class AllocatorType(enum.IntEnum): MALLOC: int FREE: int @@ -91,6 +136,10 @@ class FileReader: self, file_name: Union[str, Path], *, report_progress: bool = False ) -> None: ... def get_allocation_records(self) -> Iterable[AllocationRecord]: ... + def get_temporal_allocation_records( + self, + merge_threads: bool, + ) -> Iterable[TemporalAllocationRecord]: ... def get_high_watermark_allocation_records( self, merge_threads: bool = ..., @@ -207,3 +256,17 @@ class HighWaterMarkAggregatorTestHarness: ) -> None: ... def get_current_heap_size(self) -> int: ... def get_allocations(self) -> list[dict[str, int]]: ... + +class AllocationLifetimeAggregatorTestHarness: + def add_allocation( + self, + tid: int, + address: int, + size: int, + allocator: int, + native_frame_id: int, + frame_index: int, + native_segment_generation: int, + ) -> None: ... + def capture_snapshot(self) -> None: ... + def get_allocations(self) -> list[TemporalAllocationRecord]: ... diff --git a/src/memray/_memray.pyx b/src/memray/_memray.pyx index 4b72c23967..47b9ff450f 100644 --- a/src/memray/_memray.pyx +++ b/src/memray/_memray.pyx @@ -35,12 +35,16 @@ from _memray.sink cimport FileSink from _memray.sink cimport NullSink from _memray.sink cimport Sink from _memray.sink cimport SocketSink +from _memray.snapshot cimport NO_THREAD_INFO from _memray.snapshot cimport AbstractAggregator from _memray.snapshot cimport AggregatedCaptureReaggregator +from _memray.snapshot cimport AllocationLifetime +from _memray.snapshot cimport AllocationLifetimeAggregator from _memray.snapshot cimport AllocationStatsAggregator from _memray.snapshot cimport HighWatermark from _memray.snapshot cimport HighWaterMarkAggregator from _memray.snapshot cimport HighWatermarkFinder +from _memray.snapshot cimport HighWaterMarkLocationKey from _memray.snapshot cimport Py_GetSnapshotAllocationRecords from _memray.snapshot cimport Py_ListFromSnapshotAllocationRecords from _memray.snapshot cimport SnapshotAllocationAggregator @@ -120,18 +124,138 @@ def size_fmt(num, suffix='B'): # Memray core -PYTHON_VERSION = (sys.version_info.major, sys.version_info.minor) +cdef stack_trace( + RecordReader* reader, + tid, + allocator, + python_stack_id, + max_stacks=None, +): + if allocator in (AllocatorType.FREE, AllocatorType.MUNMAP): + raise NotImplementedError("Stack traces for deallocations aren't captured.") + + assert reader != NULL, "Cannot get stack trace without reader." + cdef ssize_t to_skip + cdef ssize_t to_keep + + if max_stacks is not None: + return reader.Py_GetStackFrame(python_stack_id, max_stacks) + + stack_trace = reader.Py_GetStackFrame(python_stack_id) + if tid == reader.getMainThreadTid(): + to_skip = reader.getSkippedFramesOnMainThread() + to_keep = max(len(stack_trace) - to_skip, 0) + del stack_trace[to_keep:] + return stack_trace + + +cdef native_stack_trace( + RecordReader* reader, + allocator, + native_stack_id, + generation, + max_stacks=None, +): + if allocator in (AllocatorType.FREE, AllocatorType.MUNMAP): + raise NotImplementedError("Stack traces for deallocations aren't captured.") + + assert reader != NULL, "Cannot get stack trace without reader." + if max_stacks is None: + return reader.Py_GetNativeStackFrame(native_stack_id, generation) + return reader.Py_GetNativeStackFrame(native_stack_id, generation, max_stacks) + + +cdef hybrid_stack_trace( + RecordReader* reader, + tid, + allocator, + python_stack_id, + native_stack_id, + generation, + max_stacks=None, +): + # This function merges a Python stack and a native stack into + # a "hybrid" stack. It substitutes _PyFrame_EvalFrameDefault calls in + # the native stack with the corresponding frame in the Python stack. + # This sounds easy, but there are several tricky aspects: + # 1. For the thread that called Tracker.__enter__, we want to hide + # frames (both Python and C) above the one that made that call. + # For other threads we want to keep all frames. + # 2. If _PyFrame_EvalFrameDefault allocates memory before calling our + # profile function, we'll have too few Python frames to pair up + # every _PyFrame_EvalFrameDefault call. This happens in 3.11. + # 3. Since Python 3.11, one _PyFrame_EvalFrameDefault call can evaluate + # many Python frames. If a frame's is_entry_frame flag is unset, it + # uses the same _PyFrame_EvalFrameDefault call as its caller. + # 4. If the interpreter was stripped, we may not be able to recognize + # every (or even any) _PyFrame_EvalFrameDefault call, so we may + # have extra Python frames left after pairing. + cdef vector[unsigned char] is_entry_frame + native_stack = native_stack_trace(reader, allocator, native_stack_id, generation) + python_stack = reader.Py_GetStackFrameAndEntryInfo(python_stack_id, &is_entry_frame) + + cdef ssize_t num_non_entry_frames = count( + is_entry_frame.begin(), is_entry_frame.end(), 0 + ) + + # Entry frames replace native frames; non-entry frames are inserted. + hybrid_stack = [None] * (len(native_stack) + num_non_entry_frames) + + # Both stacks are from most recent to least, but we must pair things up + # least recent to most to handle cases where _PyFrame_EvalFrameDefault + # allocated memory before calling the profile function. + native_stack.reverse() + cdef ssize_t pidx = len(python_stack) - 1 + cdef ssize_t hidx = len(hybrid_stack) - 1 + + cdef ssize_t to_skip = 0 + if tid == reader.getMainThreadTid(): + to_skip = reader.getSkippedFramesOnMainThread() + cdef ssize_t first_kept_frame = pidx - to_skip + + for native_frame in native_stack: + symbol = native_frame[0] + if pidx >= 0 and "_PyEval_EvalFrameDefault" in symbol: + while True: + # If we're not keeping all frames and we've reached the + # first one we want to keep, remove frames above it. + if to_skip != 0 and pidx == first_kept_frame: + del hybrid_stack[hidx + 1:] + + assert hidx >= 0 + hybrid_stack[hidx] = python_stack[pidx] + hidx -= 1 + pidx -= 1 + + # Stop when we either run out of Python frames or reach the + # entry frame being evaluated by the next eval loop. + if pidx < 0 or is_entry_frame[pidx]: + break + else: + assert hidx >= 0 + hybrid_stack[hidx] = native_frame + hidx -= 1 + + assert hidx == -1 + if pidx >= 0: + # We ran out of native frames without using up all of our Python + # frames. We've seen this happen on stripped interpreters on Alpine + # Linux in CI. Presumably this indicates that unwinding failed to + # symbolify some of the calls to _PyEval_EvalFrameDefault. + return [("", "", 0)] + + return hybrid_stack[:max_stacks] + @cython.freelist(1024) cdef class AllocationRecord: cdef object _tuple - cdef object _stack_trace - cdef object _native_stack_trace + cdef dict _stack_trace_cache cdef shared_ptr[RecordReader] _reader def __init__(self, record): self._tuple = record - self._stack_trace = None + self._stack_trace_cache = {} def __eq__(self, other): cdef AllocationRecord _other @@ -167,6 +291,14 @@ cdef class AllocationRecord: def n_allocations(self): return self._tuple[5] + @property + def native_stack_id(self): + return self._tuple[6] + + @property + def native_segment_generation(self): + return self._tuple[7] + @property def thread_name(self): if self.tid == -1: @@ -177,119 +309,251 @@ cdef class AllocationRecord: return f"{thread_id} ({name})" if name else f"{thread_id}" def stack_trace(self, max_stacks=None): - assert self._reader.get() != NULL, "Cannot get stack trace without reader." - cdef ssize_t to_skip - cdef ssize_t to_keep - if self._stack_trace is None: - if self.allocator in (AllocatorType.FREE, AllocatorType.MUNMAP): - raise NotImplementedError("Stack traces for deallocations aren't captured.") - - if max_stacks is None: - self._stack_trace = self._reader.get().Py_GetStackFrame(self._tuple[4]) - if self._tuple[0] == self._reader.get().getMainThreadTid(): - to_skip = self._reader.get().getSkippedFramesOnMainThread() - to_keep = max(len(self._stack_trace) - to_skip, 0) - del self._stack_trace[to_keep:] - else: - self._stack_trace = self._reader.get().Py_GetStackFrame(self._tuple[4], max_stacks) - return self._stack_trace + cache_key = ("python", max_stacks) + if cache_key not in self._stack_trace_cache: + self._stack_trace_cache[cache_key] = stack_trace( + self._reader.get(), + self.tid, + self.allocator, + self.stack_id, + max_stacks, + ) + return self._stack_trace_cache[cache_key] def native_stack_trace(self, max_stacks=None): - assert self._reader.get() != NULL, "Cannot get stack trace without reader." - if self._native_stack_trace is None: - if self.allocator in (AllocatorType.FREE, AllocatorType.MUNMAP): - raise NotImplementedError("Stack traces for deallocations aren't captured.") - - if max_stacks is None: - self._native_stack_trace = self._reader.get().Py_GetNativeStackFrame( - self._tuple[6], self._tuple[7]) - else: - self._native_stack_trace = self._reader.get().Py_GetNativeStackFrame( - self._tuple[6], self._tuple[7], max_stacks) - return self._native_stack_trace - - cdef _is_eval_frame(self, object symbol): - return "_PyEval_EvalFrameDefault" in symbol + cache_key = ("native", max_stacks) + if cache_key not in self._stack_trace_cache: + self._stack_trace_cache[cache_key] = native_stack_trace( + self._reader.get(), + self.allocator, + self.native_stack_id, + self.native_segment_generation, + max_stacks, + ) + return self._stack_trace_cache[cache_key] def hybrid_stack_trace(self, max_stacks=None): - # This function merges a Python stack and a native stack into - # a "hybrid" stack. It substitutes _PyFrame_EvalFrameDefault calls in - # the native stack with the corresponding frame in the Python stack. - # This sounds easy, but there are several tricky aspects: - # 1. For the thread that called Tracker.__enter__, we want to hide - # frames (both Python and C) above the one that made that call. - # For other threads we want to keep all frames. - # 2. If _PyFrame_EvalFrameDefault allocates memory before calling our - # profile function, we'll have too few Python frames to pair up - # every _PyFrame_EvalFrameDefault call. This happens in 3.11. - # 3. Since Python 3.11, one _PyFrame_EvalFrameDefault call can evaluate - # many Python frames. If a frame's is_entry_frame flag is unset, it - # uses the same _PyFrame_EvalFrameDefault call as its caller. - # 4. If the interpreter was stripped, we may not be able to recognize - # every (or even any) _PyFrame_EvalFrameDefault call, so we may - # have extra Python frames left after pairing. - cdef vector[unsigned char] is_entry_frame - python_stack = self._reader.get().Py_GetStackFrameAndEntryInfo( - self._tuple[4], &is_entry_frame + cache_key = ("hybrid", max_stacks) + if cache_key not in self._stack_trace_cache: + self._stack_trace_cache[cache_key] = hybrid_stack_trace( + self._reader.get(), + self.tid, + self.allocator, + self.stack_id, + self.native_stack_id, + self.native_segment_generation, + max_stacks, + ) + return self._stack_trace_cache[cache_key] + + def __repr__(self): + return (f"AllocationRecord") + + +@cython.freelist(1024) +cdef class Interval: + cdef public size_t allocated_before_snapshot + cdef public object deallocated_before_snapshot + cdef public size_t n_allocations + cdef public size_t n_bytes + + def __cinit__( + self, + size_t allocated_before_snapshot, + object deallocated_before_snapshot, + size_t n_allocations, + size_t n_bytes, + ): + self.allocated_before_snapshot = allocated_before_snapshot + self.deallocated_before_snapshot = deallocated_before_snapshot + self.n_allocations = n_allocations + self.n_bytes = n_bytes + + def __eq__(self, other): + if type(other) is not Interval: + return NotImplemented + return ( + self.allocated_before_snapshot, + self.deallocated_before_snapshot, + self.n_allocations, + self.n_bytes, + ) == ( + other.allocated_before_snapshot, + other.deallocated_before_snapshot, + other.n_allocations, + other.n_bytes, ) - native_stack = self.native_stack_trace() - cdef ssize_t num_non_entry_frames = count( - is_entry_frame.begin(), is_entry_frame.end(), 0 + def __repr__(self): + return ( + f"Interval(allocated_before_snapshot={self.allocated_before_snapshot}," + f" deallocated_before_snapshot={self.deallocated_before_snapshot}," + f" n_allocations={self.n_allocations}," + f" n_bytes={self.n_bytes})" ) - # Entry frames replace native frames; non-entry frames are inserted. - hybrid_stack = [None] * (len(native_stack) + num_non_entry_frames) - - # Both stacks are from most recent to least, but we must pair things up - # least recent to most to handle cases where _PyFrame_EvalFrameDefault - # allocated memory before calling the profile function. - native_stack.reverse() - cdef ssize_t pidx = len(python_stack) - 1 - cdef ssize_t hidx = len(hybrid_stack) - 1 - - cdef ssize_t to_skip = 0 - if self._tuple[0] == self._reader.get().getMainThreadTid(): - to_skip = self._reader.get().getSkippedFramesOnMainThread() - cdef ssize_t first_kept_frame = pidx - to_skip - - for native_frame in native_stack: - symbol = native_frame[0] - if pidx >= 0 and self._is_eval_frame(symbol): - while True: - # If we're not keeping all frames and we've reached the - # first one we want to keep, remove frames above it. - if to_skip != 0 and pidx == first_kept_frame: - del hybrid_stack[hidx + 1:] - - assert hidx >= 0 - hybrid_stack[hidx] = python_stack[pidx] - hidx -= 1 - pidx -= 1 - - # Stop when we either run out of Python frames or reach the - # entry frame being evaluated by the next eval loop. - if pidx < 0 or is_entry_frame[pidx]: - break - else: - assert hidx >= 0 - hybrid_stack[hidx] = native_frame - hidx -= 1 - assert hidx == -1 - if pidx >= 0: - # We ran out of native frames without using up all of our Python - # frames. We've seen this happen on stripped interpreters on Alpine - # Linux in CI. Presumably this indicates that unwinding failed to - # symbolify some of the calls to _PyEval_EvalFrameDefault. - return [("", "", 0)] +@cython.freelist(1024) +cdef class TemporalAllocationRecord: + cdef object _tuple + cdef dict _stack_trace_cache + cdef shared_ptr[RecordReader] _reader + cdef public object intervals - return hybrid_stack[:max_stacks] + def __cinit__(self, record): + self._tuple = record + self._stack_trace_cache = {} + self.intervals = [] - def __repr__(self): - return (f"AllocationRecord") + def __eq__(self, other): + if type(other) != TemporalAllocationRecord: + return NotImplemented + cdef TemporalAllocationRecord o = other + return self._tuple == o._tuple and self._intervals == o._intervals + + def __hash__(self): + return hash(self._tuple) + + @property + def tid(self): + return self._tuple[0] + + @property + def allocator(self): + return self._tuple[1] + + @property + def stack_id(self): + return self._tuple[2] + + @property + def native_stack_id(self): + return self._tuple[3] + + @property + def native_segment_generation(self): + return self._tuple[4] + + @property + def thread_name(self): + assert self._reader.get() != NULL, "Cannot get thread name without reader." + cdef object name = self._reader.get().getThreadName(self.tid) + thread_id = hex(self.tid) + return f"{thread_id} ({name})" if name else f"{thread_id}" + + def stack_trace(self, max_stacks=None): + cache_key = ("python", max_stacks) + if cache_key not in self._stack_trace_cache: + self._stack_trace_cache[cache_key] = stack_trace( + self._reader.get(), + self.tid, + self.allocator, + self.stack_id, + max_stacks, + ) + return self._stack_trace_cache[cache_key] + + def native_stack_trace(self, max_stacks=None): + cache_key = ("native", max_stacks) + if cache_key not in self._stack_trace_cache: + self._stack_trace_cache[cache_key] = native_stack_trace( + self._reader.get(), + self.allocator, + self.native_stack_id, + self.native_segment_generation, + max_stacks, + ) + return self._stack_trace_cache[cache_key] + + def hybrid_stack_trace(self, max_stacks=None): + cache_key = ("hybrid", max_stacks) + if cache_key not in self._stack_trace_cache: + self._stack_trace_cache[cache_key] = hybrid_stack_trace( + self._reader.get(), + self.tid, + self.allocator, + self.stack_id, + self.native_stack_id, + self.native_segment_generation, + max_stacks, + ) + return self._stack_trace_cache[cache_key] + + +cdef create_temporal_allocation_record( + const HighWaterMarkLocationKey& key, + shared_ptr[RecordReader] reader, +): + cdef object elem = ( + key.thread_id, + key.allocator, + key.python_frame_id, + key.native_frame_id, + key.native_segment_generation, + ) + cdef TemporalAllocationRecord alloc = TemporalAllocationRecord(elem) + alloc._reader = reader + return alloc + + +cdef class TemporalAllocationGenerator: + cdef vector[AllocationLifetime] lifetimes + cdef shared_ptr[RecordReader] reader + + cdef object curr_record + cdef HighWaterMarkLocationKey last_key + cdef size_t idx + + cdef setup( + self, + vector[AllocationLifetime]&& lifetimes, + shared_ptr[RecordReader] reader, + ): + self.lifetimes = move(lifetimes) + self.reader = move(reader) + + def __iter__(self): + return self + + def __next__(self): + to_return = None + cdef AllocationLifetime lifetime + while self.idx < self.lifetimes.size(): + lifetime = self.lifetimes[self.idx] + self.idx += 1 + + if lifetime.key != self.last_key: + if self.curr_record is not None: + to_return = self.curr_record + self.last_key = lifetime.key + self.curr_record = create_temporal_allocation_record( + lifetime.key, self.reader + ) + + if lifetime.deallocatedBeforeSnapshot == (-1): + deallocated_before_snapshot = None + else: + deallocated_before_snapshot = lifetime.deallocatedBeforeSnapshot + + self.curr_record.intervals.append( + Interval( + lifetime.allocatedBeforeSnapshot, + deallocated_before_snapshot, + lifetime.n_allocations, + lifetime.n_bytes, + ) + ) + + if to_return is not None: + return to_return + + if self.curr_record is not None: + to_return = self.curr_record + self.curr_record = None + return to_return + raise StopIteration MemorySnapshot = collections.namedtuple("MemorySnapshot", "time rss heap") @@ -813,6 +1077,50 @@ cdef class FileReader: temporary_buffer_size=threshold + 1, ) + def get_temporal_allocation_records(self, merge_threads=True): + self._ensure_not_closed() + if self._header["file_format"] == FileFormat.AGGREGATED_ALLOCATIONS: + raise NotImplementedError( + "Can't get allocation history using a pre-aggregated capture file." + ) + + cdef shared_ptr[RecordReader] reader_sp = make_shared[RecordReader]( + unique_ptr[FileSource](new FileSource(self._path)) + ) + cdef RecordReader* reader = reader_sp.get() + + cdef size_t records_to_process = self._header["stats"]["n_allocations"] + cdef ProgressIndicator progress_indicator = ProgressIndicator( + "Processing allocation records", + total=records_to_process, + report_progress=self._report_progress + ) + + cdef AllocationLifetimeAggregator aggregator + cdef _Allocation allocation + + with progress_indicator: + while records_to_process > 0: + PyErr_CheckSignals() + ret = reader.nextRecord() + if ret == RecordResult.RecordResultAllocationRecord: + allocation = reader.getLatestAllocation() + if merge_threads: + allocation.tid = NO_THREAD_INFO + aggregator.addAllocation(allocation) + records_to_process -= 1 + progress_indicator.update(1) + elif ret == RecordResult.RecordResultMemoryRecord: + aggregator.captureSnapshot() + else: + assert ret != RecordResult.RecordResultMemorySnapshot + assert ret != RecordResult.RecordResultAggregatedAllocationRecord + break + + cdef TemporalAllocationGenerator gen = TemporalAllocationGenerator() + gen.setup(move(aggregator.generateIndex()), reader_sp) + yield from gen + def get_allocation_records(self): self._ensure_not_closed() if self._header["file_format"] == FileFormat.AGGREGATED_ALLOCATIONS: @@ -1111,3 +1419,37 @@ cdef class HighWaterMarkAggregatorTestHarness: ) ) return ret + + +cdef class AllocationLifetimeAggregatorTestHarness: + cdef AllocationLifetimeAggregator aggregator + + def add_allocation( + self, + tid, + address, + size, + allocator, + native_frame_id, + frame_index, + native_segment_generation, + ): + cdef _Allocation allocation + allocation.tid = tid + allocation.address = address + allocation.size = size + allocation.allocator = allocator + allocation.native_frame_id = native_frame_id + allocation.frame_index = frame_index + allocation.native_segment_generation = native_segment_generation + allocation.n_allocations = 1 + self.aggregator.addAllocation(allocation) + + def capture_snapshot(self): + return self.aggregator.captureSnapshot() + + def get_allocations(self): + cdef shared_ptr[RecordReader] reader + cdef TemporalAllocationGenerator gen = TemporalAllocationGenerator() + gen.setup(move(self.aggregator.generateIndex()), reader) + yield from gen diff --git a/src/memray/_memray/snapshot.cpp b/src/memray/_memray/snapshot.cpp index f86796c505..46a95272ef 100644 --- a/src/memray/_memray/snapshot.cpp +++ b/src/memray/_memray/snapshot.cpp @@ -1,7 +1,8 @@ -#include - #include "snapshot.h" +#include +#include + namespace memray::api { bool @@ -19,6 +20,46 @@ HighWaterMarkLocationKey::operator==(const HighWaterMarkLocationKey& rhs) const && native_segment_generation == rhs.native_segment_generation && allocator == rhs.allocator; } +bool +HighWaterMarkLocationKey::operator!=(const HighWaterMarkLocationKey& rhs) const +{ + return !(*this == rhs); +} + +bool +HighWaterMarkLocationKey::operator<(const HighWaterMarkLocationKey& rhs) const +{ + if (thread_id != rhs.thread_id) { + return thread_id < rhs.thread_id; + } else if (python_frame_id != rhs.python_frame_id) { + return python_frame_id < rhs.python_frame_id; + } else if (native_frame_id != rhs.native_frame_id) { + return native_frame_id < rhs.native_frame_id; + } else if (native_segment_generation != rhs.native_segment_generation) { + return native_segment_generation < rhs.native_segment_generation; + } else if (allocator != rhs.allocator) { + return allocator < rhs.allocator; + } + return false; +} + +bool +operator<(const AllocationLifetime& lhs, const AllocationLifetime& rhs) +{ + // Sort first by location, then allocatedBefore, then deallocatedBefore. + // Sort by n_bytes if allocatedBefore/deallocatedBefore are equal, + // so that our test suite gets records in a predictable order. + if (lhs.key != rhs.key) { + return lhs.key < rhs.key; + } else if (lhs.allocatedBeforeSnapshot != rhs.allocatedBeforeSnapshot) { + return lhs.allocatedBeforeSnapshot < rhs.allocatedBeforeSnapshot; + } else if (lhs.deallocatedBeforeSnapshot != rhs.deallocatedBeforeSnapshot) { + return lhs.deallocatedBeforeSnapshot < rhs.deallocatedBeforeSnapshot; + } else { + return lhs.n_bytes < rhs.n_bytes; + } +} + Interval::Interval(uintptr_t begin, uintptr_t end) : begin(begin) , end(end) @@ -407,6 +448,167 @@ HighWaterMarkAggregator::visitAllocations(const allocation_callback_t& callback) return true; } +void +AllocationLifetimeAggregator::addAllocation(const Allocation& allocation_or_deallocation) +{ + // Note: Deallocation records don't tell us where the memory was allocated, + // so we need to save the records for allocations and cross-reference + // deallocations against them. + switch (hooks::allocatorKind(allocation_or_deallocation.allocator)) { + case hooks::AllocatorKind::SIMPLE_ALLOCATOR: { + const Allocation& allocation = allocation_or_deallocation; + size_t generation = d_num_snapshots; + d_ptr_to_allocation[allocation.address] = {allocation, generation}; + break; + } + case hooks::AllocatorKind::SIMPLE_DEALLOCATOR: { + const Allocation& deallocation = allocation_or_deallocation; + const auto it = d_ptr_to_allocation.find(deallocation.address); + if (it != d_ptr_to_allocation.end()) { + const auto& [allocation, generation] = it->second; + recordDeallocation(extractKey(allocation), 1, allocation.size, generation); + d_ptr_to_allocation.erase(it); + } + break; + } + case hooks::AllocatorKind::RANGED_ALLOCATOR: { + const Allocation& allocation = allocation_or_deallocation; + size_t generation = d_num_snapshots; + d_mmap_intervals.addInterval( + allocation.address, + allocation.size, + {std::make_shared(allocation), generation}); + break; + } + case hooks::AllocatorKind::RANGED_DEALLOCATOR: { + const Allocation& deallocation = allocation_or_deallocation; + auto removal_stats = + d_mmap_intervals.removeInterval(deallocation.address, deallocation.size); + for (const auto& [interval, pair] : removal_stats.freed_allocations) { + recordRangedDeallocation(pair.first, interval.size(), pair.second); + } + for (const auto& [interval, pair] : removal_stats.shrunk_allocations) { + recordRangedDeallocation(pair.first, interval.size(), pair.second); + } + for (const auto& [interval, pair] : removal_stats.split_allocations) { + recordRangedDeallocation(pair.first, interval.size(), pair.second); + } + break; + } + } +} + +HighWaterMarkLocationKey +AllocationLifetimeAggregator::extractKey(const Allocation& allocation) const +{ + return {allocation.tid, + allocation.frame_index, + allocation.native_frame_id, + allocation.native_segment_generation, + allocation.allocator}; +} + +void +AllocationLifetimeAggregator::recordRangedDeallocation( + const std::shared_ptr& allocation_ptr, + size_t bytes_deallocated, + size_t generation_allocated) +{ + // We hold one reference, and the IntervalTree may or may not hold others. + // We use a count of 0 for all but the last deallocation of a range so that + // partial deallocations won't affect the count of allocations by location. + bool fully_deallocated = allocation_ptr.use_count() == 1; + recordDeallocation( + extractKey(*allocation_ptr), + (fully_deallocated ? 1 : 0), + bytes_deallocated, + generation_allocated); +} + +void +AllocationLifetimeAggregator::recordDeallocation( + const HighWaterMarkLocationKey& key, + size_t count_delta, + size_t bytes_delta, + size_t generation) +{ + if (d_num_snapshots == generation) { + // Allocated and deallocated within the same snapshot. We can ignore this. + return; + } + + auto& counts = d_allocation_history[std::make_tuple(generation, d_num_snapshots, key)]; + counts.first += count_delta; + counts.second += bytes_delta; +} + +void +AllocationLifetimeAggregator::captureSnapshot() +{ + ++d_num_snapshots; +} + +std::vector +AllocationLifetimeAggregator::generateIndex() const +{ + struct KeyHash + { + size_t operator()(const std::pair& key) const + { + size_t ret = HighWaterMarkLocationKeyHash{}(std::get<1>(key)); + ret = (ret << 1) ^ std::get<0>(key); + return ret; + } + }; + + // First, gather information about allocations that were never deallocated. + // These are still sitting in `d_ptr_to_allocation` and `d_mmap_intervals`, + // since `d_allocation_history` only gets updated when things are freed. + // We can't update `d_allocation_history` here since this method is const. + std::unordered_map, std::pair, KeyHash> + leaks; + + for (const auto& [ptr, allocation_and_generation] : d_ptr_to_allocation) { + (void)ptr; + const auto& [allocation, generation] = allocation_and_generation; + auto& counts = leaks[std::make_pair(generation, extractKey(allocation))]; + counts.first += 1; + counts.second += allocation.size; + } + + std::unordered_set leaked_mappings; + for (const auto& [interval, allocation_ptr_and_generation] : d_mmap_intervals) { + const auto& [allocation_ptr, generation] = allocation_ptr_and_generation; + auto& counts = leaks[std::make_pair(generation, extractKey(*allocation_ptr))]; + + // Ensure we only count each allocation once, even if it's been split. + auto inserted = leaked_mappings.insert(allocation_ptr.get()).second; + counts.first += (inserted ? 1 : 0); + counts.second += interval.size(); + } + + // Then, combine information about both leaked allocations and freed + // allocations into the vector we'll be returning. + std::vector ret; + + for (const auto& [when_where, how_much] : leaks) { + const auto& [allocated_before, key] = when_where; + const auto& [n_allocations, n_bytes] = how_much; + ret.push_back({allocated_before, static_cast(-1), key, n_allocations, n_bytes}); + } + + for (const auto& [when_where, how_much] : d_allocation_history) { + const auto& [allocated_before, deallocated_before, key] = when_where; + const auto& [n_allocations, n_bytes] = how_much; + ret.push_back({allocated_before, deallocated_before, key, n_allocations, n_bytes}); + } + + // Finally, sort the vector we're returning, so that our callers can count + // on all intervals for a given location being contiguous. + std::sort(ret.begin(), ret.end()); + return ret; +} + /** * Produce an aggregated snapshot from a vector of allocations and a index in that vector * diff --git a/src/memray/_memray/snapshot.h b/src/memray/_memray/snapshot.h index ad5f38a9ae..46d5dc47d4 100644 --- a/src/memray/_memray/snapshot.h +++ b/src/memray/_memray/snapshot.h @@ -150,6 +150,16 @@ class IntervalTree return d_intervals.end(); } + const_iterator begin() const + { + return d_intervals.begin(); + } + + const_iterator end() const + { + return d_intervals.end(); + } + const_iterator cbegin() { return d_intervals.cbegin(); @@ -248,6 +258,8 @@ struct HighWaterMarkLocationKey hooks::Allocator allocator; bool operator==(const HighWaterMarkLocationKey& rhs) const; + bool operator!=(const HighWaterMarkLocationKey& rhs) const; + bool operator<(const HighWaterMarkLocationKey& rhs) const; }; struct HighWaterMarkLocationKeyHash @@ -320,6 +332,64 @@ class HighWaterMarkAggregator reduced_snapshot_map_t getAllocations(bool merge_threads, bool stop_at_high_water_mark) const; }; +struct AllocationLifetime +{ + size_t allocatedBeforeSnapshot; + size_t deallocatedBeforeSnapshot; // SIZE_MAX if never deallocated + HighWaterMarkLocationKey key; + size_t n_allocations; + size_t n_bytes; +}; + +class AllocationLifetimeAggregator +{ + public: + void addAllocation(const Allocation& allocation); + void captureSnapshot(); + + std::vector generateIndex() const; + + private: + size_t d_num_snapshots{}; + + struct allocation_history_key_hash + { + size_t operator()(const std::tuple& key) const + { + size_t ret = HighWaterMarkLocationKeyHash{}(std::get<2>(key)); + ret = (ret << 1) ^ std::get<1>(key); + ret = (ret << 1) ^ std::get<0>(key); + return ret; + } + }; + + // Record of freed allocations that spanned multiple snapshots. + std::unordered_map< + std::tuple, + std::pair, + allocation_history_key_hash> + d_allocation_history; + + // Simple allocations contributing to the current heap size. + std::unordered_map> d_ptr_to_allocation; + + // Ranged allocations contributing to the current heap size. + IntervalTree, size_t>> d_mmap_intervals; + + HighWaterMarkLocationKey extractKey(const Allocation& allocation) const; + + void recordRangedDeallocation( + const std::shared_ptr& allocation, + size_t bytes_deallocated, + size_t generation_allocated); + + void recordDeallocation( + const HighWaterMarkLocationKey& key, + size_t count_delta, + size_t bytes_delta, + size_t generation); +}; + class AllocationStatsAggregator { public: diff --git a/src/memray/_memray/snapshot.pxd b/src/memray/_memray/snapshot.pxd index 964061bf55..d06a229f30 100644 --- a/src/memray/_memray/snapshot.pxd +++ b/src/memray/_memray/snapshot.pxd @@ -1,3 +1,4 @@ +from _memray.hooks cimport Allocator from _memray.records cimport AggregatedAllocation from _memray.records cimport Allocation from _memray.records cimport optional_frame_id_t @@ -10,6 +11,8 @@ from libcpp.vector cimport vector cdef extern from "snapshot.h" namespace "memray::api": + unsigned long NO_THREAD_INFO + cdef struct HighWatermark: size_t index size_t peak_memory @@ -48,6 +51,28 @@ cdef extern from "snapshot.h" namespace "memray::api": size_t getCurrentHeapSize() bool visitAllocations[T](const T& callback) except+ + cdef cppclass HighWaterMarkLocationKey: + unsigned long thread_id + size_t python_frame_id + size_t native_frame_id + size_t native_segment_generation + Allocator allocator + + bool operator==(const HighWaterMarkLocationKey& other) + bool operator!=(const HighWaterMarkLocationKey& other) + + cdef cppclass AllocationLifetime: + size_t allocatedBeforeSnapshot + size_t deallocatedBeforeSnapshot + HighWaterMarkLocationKey key + size_t n_allocations + size_t n_bytes + + cdef cppclass AllocationLifetimeAggregator: + void addAllocation(const Allocation& allocation) except+ + void captureSnapshot() + vector[AllocationLifetime] generateIndex() except+ + cdef cppclass AllocationStatsAggregator: void addAllocation(const Allocation&, optional_frame_id_t python_frame_id) except+ uint64_t totalAllocations() diff --git a/src/memray/commands/common.py b/src/memray/commands/common.py index a03def774a..9b76d0a2fa 100644 --- a/src/memray/commands/common.py +++ b/src/memray/commands/common.py @@ -106,25 +106,33 @@ def write_report( show_memory_leaks: bool, temporary_allocation_threshold: int, merge_threads: Optional[bool] = None, + temporal_leaks: bool = False, **kwargs: Any, ) -> None: try: reader = FileReader(os.fspath(result_path), report_progress=True) + default_merge_threads = True if merge_threads is None else merge_threads + if reader.metadata.has_native_traces: warn_if_not_enough_symbols() if show_memory_leaks: snapshot = reader.get_leaked_allocation_records( - merge_threads=merge_threads if merge_threads is not None else True + merge_threads=default_merge_threads ) elif temporary_allocation_threshold >= 0: snapshot = reader.get_temporary_allocation_records( threshold=temporary_allocation_threshold, - merge_threads=merge_threads if merge_threads is not None else True, + merge_threads=default_merge_threads, ) + elif temporal_leaks: + snapshot = reader.get_temporal_allocation_records( + merge_threads=default_merge_threads + ) # type: ignore + show_memory_leaks = True else: snapshot = reader.get_high_watermark_allocation_records( - merge_threads=merge_threads if merge_threads is not None else True + merge_threads=default_merge_threads ) memory_records = tuple(reader.get_memory_snapshots()) reporter = self.reporter_factory( @@ -167,6 +175,7 @@ def run( output_file, args.show_memory_leaks, args.temporary_allocation_threshold, + temporal_leaks=getattr(args, "temporal_leaks", False), **kwargs, ) diff --git a/src/memray/commands/flamegraph.py b/src/memray/commands/flamegraph.py index f49b51f9ca..859ab33a88 100644 --- a/src/memray/commands/flamegraph.py +++ b/src/memray/commands/flamegraph.py @@ -38,6 +38,16 @@ def prepare_parser(self, parser: argparse.ArgumentParser) -> None: dest="show_memory_leaks", default=False, ) + alloc_type_group.add_argument( + "--temporal-leaks", + help=( + "Generate a dynamic flame graph showing allocations performed" + " in a user-selected time range and not freed before the end" + " of that time range." + ), + action="store_true", + default=False, + ) alloc_type_group.add_argument( "--temporary-allocation-threshold", metavar="N", diff --git a/src/memray/reporters/assets/temporal_flamegraph.js b/src/memray/reporters/assets/temporal_flamegraph.js new file mode 100644 index 0000000000..5e8436463d --- /dev/null +++ b/src/memray/reporters/assets/temporal_flamegraph.js @@ -0,0 +1,290 @@ +import { debounced } from "./common"; + +import { + initThreadsDropdown, + drawChart, + handleFragments, + onFilterUninteresting, + onFilterImportSystem, + onFilterThread, + onResetZoom, + onResize, + onInvert, + getFilteredChart, + getFlamegraph, +} from "./flamegraph_common"; + +var active_plot = null; +var current_dimensions = null; + +var parent_index_by_child_index = (function () { + let ret = new Array(packed_data.nodes.children.length); + console.log("finding parent index for each node"); + for (const [parentIndex, children] of packed_data.nodes.children.entries()) { + children.forEach((idx) => (ret[idx] = parentIndex)); + } + console.assert(ret[0] === undefined, "root node has a parent"); + return ret; +})(); + +function packedDataToTree(packedData, rangeStart, rangeEnd) { + const { strings, nodes, unique_threads } = packedData; + + console.log("constructing nodes"); + const node_objects = nodes.name.map((_, i) => ({ + name: strings[nodes["name"][i]], + location: [ + strings[nodes["function"][i]], + strings[nodes["filename"][i]], + nodes["lineno"][i], + ], + value: 0, + children: nodes["children"][i], + n_allocations: 0, + thread_id: strings[nodes["thread_id"][i]], + interesting: nodes["interesting"][i] !== 0, + import_system: nodes["import_system"][i] !== 0, + })); + + console.log("mapping child indices to child nodes"); + for (const [parentIndex, node] of node_objects.entries()) { + node["children"] = node["children"].map((idx) => node_objects[idx]); + } + + // We could binary search rather than using a linear scan... + console.log("finding leaked allocations"); + packedData.intervals.forEach((interval) => { + let [allocBefore, deallocBefore, nodeIndex, count, bytes] = interval; + + if ( + allocBefore >= rangeStart && + allocBefore <= rangeEnd && + (deallocBefore === null || deallocBefore > rangeEnd) + ) { + while (nodeIndex !== undefined) { + node_objects[nodeIndex].n_allocations += count; + node_objects[nodeIndex].value += bytes; + nodeIndex = parent_index_by_child_index[nodeIndex]; + } + } + }); + + console.log( + "total leaked allocations in range: " + node_objects[0].n_allocations + ); + console.log("total leaked bytes in range: " + node_objects[0].value); + + node_objects.forEach((node) => { + node.children = node.children.filter((node) => node.n_allocations > 0); + }); + + return node_objects[0]; +} + +function initMemoryGraph(memory_records) { + console.log("init memory graph"); + const time = memory_records.map((a) => new Date(a[0])); + const resident_size = memory_records.map((a) => a[1]); + const heap_size = memory_records.map((a) => a[2]); + + var resident_size_plot = { + x: time, + y: resident_size, + mode: "lines", + name: "Resident size", + }; + + var heap_size_plot = { + x: time, + y: heap_size, + mode: "lines", + name: "Heap size", + }; + + var plot_data = [resident_size_plot, heap_size_plot]; + var config = { + responsive: true, + displayModeBar: false, + }; + var layout = { + xaxis: { + title: { + text: "Time", + }, + rangeslider: { + visible: true, + }, + }, + yaxis: { + title: { + text: "Memory Size", + }, + tickformat: ".4~s", + exponentformat: "B", + ticksuffix: "B", + }, + }; + + Plotly.newPlot("plot", plot_data, layout, config).then((plot) => { + console.assert(active_plot === null); + active_plot = plot; + }); +} + +function showLoadingAnimation() { + console.log("showLoadingAnimation"); + document.getElementById("loading").style.display = "block"; + document.getElementById("overlay").style.display = "block"; +} + +function hideLoadingAnimation() { + console.log("hideLoadingAnimation"); + document.getElementById("loading").style.display = "none"; + document.getElementById("overlay").style.display = "none"; +} + +function refreshFlamegraph(event) { + console.log("refreshing flame graph!"); + + let request_data = getRangeData(event); + console.log("range data: " + JSON.stringify(request_data)); + + if ( + current_dimensions != null && + JSON.stringify(request_data) === JSON.stringify(current_dimensions) + ) { + return; + } + + console.log("showing loading animation"); + showLoadingAnimation(); + + current_dimensions = request_data; + + console.log("finding range of relevant snapshot"); + + let idx0 = 0; + let idx1 = memory_records.length; + + if (request_data) { + const t0 = new Date(request_data.string1).getTime(); + const t0_idx = memory_records.findIndex((rec) => rec[0] >= t0); + if (t0_idx != -1) idx0 = t0_idx; + + const t1 = new Date(request_data.string2).getTime(); + const t1_idx = memory_records.findIndex((rec) => rec[0] > t1); + if (t1_idx != -1) idx1 = t1_idx; + } + + console.log("start index is " + idx0); + console.log("end index is " + idx1); + console.log("first possible index is 0"); + console.log("last possible index is " + memory_records.length); + + console.log("constructing tree"); + data = packedDataToTree(packed_data, idx0, idx1); + + console.log("drawing chart"); + getFilteredChart().drawChart(data); + console.log("hiding loading animation"); + hideLoadingAnimation(); +} + +function getRangeData(event) { + console.log("getRangeData"); + let request_data = {}; + if (event.hasOwnProperty("xaxis.range[0]")) { + request_data = { + string1: event["xaxis.range[0]"], + string2: event["xaxis.range[1]"], + }; + } else if (event.hasOwnProperty("xaxis.range")) { + request_data = { + string1: event["xaxis.range"][0], + string2: event["xaxis.range"][1], + }; + } else if (active_plot !== null) { + let the_range = active_plot.layout.xaxis.range; + request_data = { + string1: the_range[0], + string2: the_range[1], + }; + } else { + return; + } + return request_data; +} + +var debounce = null; +function refreshFlamegraphDebounced(event) { + console.log("refreshFlamegraphDebounced"); + if (debounce) { + clearTimeout(debounce); + } + debounce = setTimeout(function () { + refreshFlamegraph(event); + }, 500); +} + +// Main entrypoint +function main() { + console.log("main"); + + const unique_threads = packed_data.unique_threads.map( + (tid) => packed_data.strings[tid] + ); + initThreadsDropdown({ unique_threads: unique_threads }, merge_threads); + + initMemoryGraph(memory_records); + + // Draw the initial flame graph + refreshFlamegraph({}); + + // Set zoom to correct element + if (location.hash) { + handleFragments(); + } + + // Setup event handlers + document.getElementById("invertButton").onclick = onInvert; + document.getElementById("resetZoomButton").onclick = onResetZoom; + document.getElementById("resetThreadFilterItem").onclick = onFilterThread; + let hideUninterestingCheckBox = document.getElementById("hideUninteresting"); + hideUninterestingCheckBox.onclick = onFilterUninteresting.bind(this); + let hideImportSystemCheckBox = document.getElementById("hideImportSystem"); + hideImportSystemCheckBox.onclick = onFilterImportSystem.bind(this); + // Enable filtering by default + onFilterUninteresting.bind(this)(); + + document.onkeyup = (event) => { + if (event.code == "Escape") { + onResetZoom(); + } + }; + document.getElementById("searchTerm").addEventListener("input", () => { + const termElement = document.getElementById("searchTerm"); + getFlamegraph().search(termElement.value); + }); + + window.addEventListener("popstate", handleFragments); + window.addEventListener("resize", debounced(onResize)); + + // Enable tooltips + $('[data-toggle-second="tooltip"]').tooltip(); + $('[data-toggle="tooltip"]').tooltip(); + + // Set up the reload handler + console.log("setup reload handler"); + document + .getElementById("plot") + .on("plotly_relayout", refreshFlamegraphDebounced); + + // Enable toasts + var toastElList = [].slice.call(document.querySelectorAll(".toast")); + var toastList = toastElList.map(function (toastEl) { + return new bootstrap.Toast(toastEl, { delay: 10000 }); + }); + toastList.forEach((toast) => toast.show()); +} + +document.addEventListener("DOMContentLoaded", main); diff --git a/src/memray/reporters/flamegraph.py b/src/memray/reporters/flamegraph.py index 0a7144334e..f940e1ec30 100644 --- a/src/memray/reporters/flamegraph.py +++ b/src/memray/reporters/flamegraph.py @@ -9,13 +9,16 @@ from typing import Iterable from typing import Iterator from typing import List +from typing import Optional from typing import TextIO from typing import Tuple from typing import TypeVar +from typing import Union from memray import AllocationRecord from memray import MemorySnapshot from memray import Metadata +from memray._memray import TemporalAllocationRecord from memray.reporters.frame_tools import StackFrame from memray.reporters.frame_tools import is_cpython_internal from memray.reporters.frame_tools import is_frame_from_import_system @@ -84,7 +87,7 @@ def __init__( @classmethod def from_snapshot( cls, - allocations: Iterator[AllocationRecord], + allocations: Iterator[Union[AllocationRecord, TemporalAllocationRecord]], *, memory_records: Iterable[MemorySnapshot], native_traces: bool, @@ -101,18 +104,19 @@ def from_snapshot( } frames = [root] + interval_list: List[Tuple[int, Optional[int], int, int, int]] = [] node_index_by_key: Dict[Tuple[int, StackFrame, str], int] = {} unique_threads = set() for record in allocations: - size = record.size thread_id = record.thread_name unique_threads.add(thread_id) - root["value"] += size - root["n_allocations"] += record.n_allocations + if not isinstance(record, TemporalAllocationRecord): + root["value"] += record.size + root["n_allocations"] += record.n_allocations current_frame_id = 0 current_frame = root @@ -155,14 +159,27 @@ def from_snapshot( current_frame_id = node_index_by_key[node_key] current_frame = frames[current_frame_id] - current_frame["value"] += size - current_frame["n_allocations"] += record.n_allocations + if not isinstance(record, TemporalAllocationRecord): + current_frame["value"] += record.size + current_frame["n_allocations"] += record.n_allocations if index - num_skipped_frames > MAX_STACKS: current_frame["name"] = "" current_frame["location"] = ["...", "...", 0] break + if isinstance(record, TemporalAllocationRecord): + interval_list.extend( + ( + interval.allocated_before_snapshot, + interval.deallocated_before_snapshot, + current_frame_id, + interval.n_allocations, + interval.n_bytes, + ) + for interval in record.intervals + ) + all_strings = StringRegistry() nodes = collections.defaultdict(list) for frame in frames: @@ -170,9 +187,10 @@ def from_snapshot( nodes["function"].append(all_strings.register(frame["location"][0])) nodes["filename"].append(all_strings.register(frame["location"][1])) nodes["lineno"].append(frame["location"][2]) - nodes["value"].append(frame["value"]) nodes["children"].append(frame["children"]) - nodes["n_allocations"].append(frame["n_allocations"]) + if not interval_list: + nodes["value"].append(frame["value"]) + nodes["n_allocations"].append(frame["n_allocations"]) nodes["thread_id"].append(all_strings.register(frame["thread_id"])) nodes["interesting"].append(int(frame["interesting"])) nodes["import_system"].append(int(frame["import_system"])) @@ -185,6 +203,9 @@ def from_snapshot( "strings": all_strings.strings, } + if interval_list: + data["intervals"] = interval_list + return cls(data, memory_records=memory_records) def render( @@ -194,8 +215,9 @@ def render( show_memory_leaks: bool, merge_threads: bool, ) -> None: + kind = "temporal_flamegraph" if "intervals" in self.data else "flamegraph" html_code = render_report( - kind="flamegraph", + kind=kind, data=self.data, metadata=metadata, memory_records=self.memory_records, diff --git a/src/memray/reporters/templates/__init__.py b/src/memray/reporters/templates/__init__.py index 22e12eae45..73c650c263 100644 --- a/src/memray/reporters/templates/__init__.py +++ b/src/memray/reporters/templates/__init__.py @@ -38,9 +38,10 @@ def render_report( env = get_render_environment() template = env.get_template(kind + ".html") - title = get_report_title(kind=kind, show_memory_leaks=show_memory_leaks) + pretty_kind = kind.replace("_", " ") + title = get_report_title(kind=pretty_kind, show_memory_leaks=show_memory_leaks) return template.render( - kind=kind, + kind=pretty_kind, title=title, data=data, metadata=metadata, diff --git a/src/memray/reporters/templates/assets/flamegraph.css b/src/memray/reporters/templates/assets/flamegraph.css index 68ea106104..37f6ba590f 100644 --- a/src/memray/reporters/templates/assets/flamegraph.css +++ b/src/memray/reporters/templates/assets/flamegraph.css @@ -50,3 +50,41 @@ .tooltip-inner { max-width: 300px; } + +/* Loading animation */ + +#loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +.loading-spinner { + border: 4px solid #f3f3f3; + border-top: 4px solid #3498db; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +#overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); /* semi-transparent black */ + z-index: 99; /* make sure it's on top of other elements */ +} diff --git a/src/memray/reporters/templates/assets/temporal_flamegraph.js b/src/memray/reporters/templates/assets/temporal_flamegraph.js new file mode 100644 index 0000000000..e805ae94b3 --- /dev/null +++ b/src/memray/reporters/templates/assets/temporal_flamegraph.js @@ -0,0 +1,9 @@ +(()=>{var n={486:function(n,t,r){var e; +/** + * @license + * Lodash + * Copyright OpenJS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */n=r.nmd(n),function(){var u,i="Expected a function",o="__lodash_hash_undefined__",a="__lodash_placeholder__",c=16,f=32,l=64,s=128,h=256,p=1/0,v=9007199254740991,_=NaN,g=4294967295,d=[["ary",s],["bind",1],["bindKey",2],["curry",8],["curryRight",c],["flip",512],["partial",f],["partialRight",l],["rearg",h]],y="[object Arguments]",m="[object Array]",w="[object Boolean]",b="[object Date]",x="[object Error]",j="[object Function]",A="[object GeneratorFunction]",I="[object Map]",E="[object Number]",k="[object Object]",O="[object Promise]",B="[object RegExp]",S="[object Set]",R="[object String]",z="[object Symbol]",C="[object WeakMap]",F="[object ArrayBuffer]",L="[object DataView]",T="[object Float32Array]",W="[object Float64Array]",U="[object Int8Array]",D="[object Int16Array]",$="[object Int32Array]",M="[object Uint8Array]",N="[object Uint8ClampedArray]",P="[object Uint16Array]",q="[object Uint32Array]",Z=/\b__p \+= '';/g,J=/\b(__p \+=) '' \+/g,K=/(__e\(.*?\)|\b__t\)) \+\n'';/g,Y=/&(?:amp|lt|gt|quot|#39);/g,G=/[&<>"']/g,V=RegExp(Y.source),H=RegExp(G.source),X=/<%-([\s\S]+?)%>/g,Q=/<%([\s\S]+?)%>/g,nn=/<%=([\s\S]+?)%>/g,tn=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,rn=/^\w*$/,en=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,un=/[\\^$.*+?()[\]{}|]/g,on=RegExp(un.source),an=/^\s+/,cn=/\s/,fn=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,ln=/\{\n\/\* \[wrapped with (.+)\] \*/,sn=/,? & /,hn=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,pn=/[()=,{}\[\]\/\s]/,vn=/\\(\\)?/g,_n=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,gn=/\w*$/,dn=/^[-+]0x[0-9a-f]+$/i,yn=/^0b[01]+$/i,mn=/^\[object .+?Constructor\]$/,wn=/^0o[0-7]+$/i,bn=/^(?:0|[1-9]\d*)$/,xn=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,jn=/($^)/,An=/['\n\r\u2028\u2029\\]/g,In="\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff",En="\\u2700-\\u27bf",kn="a-z\\xdf-\\xf6\\xf8-\\xff",On="A-Z\\xc0-\\xd6\\xd8-\\xde",Bn="\\ufe0e\\ufe0f",Sn="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",Rn="['’]",zn="[\\ud800-\\udfff]",Cn="["+Sn+"]",Fn="["+In+"]",Ln="\\d+",Tn="[\\u2700-\\u27bf]",Wn="["+kn+"]",Un="[^\\ud800-\\udfff"+Sn+Ln+En+kn+On+"]",Dn="\\ud83c[\\udffb-\\udfff]",$n="[^\\ud800-\\udfff]",Mn="(?:\\ud83c[\\udde6-\\uddff]){2}",Nn="[\\ud800-\\udbff][\\udc00-\\udfff]",Pn="["+On+"]",qn="(?:"+Wn+"|"+Un+")",Zn="(?:"+Pn+"|"+Un+")",Jn="(?:['’](?:d|ll|m|re|s|t|ve))?",Kn="(?:['’](?:D|LL|M|RE|S|T|VE))?",Yn="(?:"+Fn+"|"+Dn+")"+"?",Gn="[\\ufe0e\\ufe0f]?",Vn=Gn+Yn+("(?:\\u200d(?:"+[$n,Mn,Nn].join("|")+")"+Gn+Yn+")*"),Hn="(?:"+[Tn,Mn,Nn].join("|")+")"+Vn,Xn="(?:"+[$n+Fn+"?",Fn,Mn,Nn,zn].join("|")+")",Qn=RegExp(Rn,"g"),nt=RegExp(Fn,"g"),tt=RegExp(Dn+"(?="+Dn+")|"+Xn+Vn,"g"),rt=RegExp([Pn+"?"+Wn+"+"+Jn+"(?="+[Cn,Pn,"$"].join("|")+")",Zn+"+"+Kn+"(?="+[Cn,Pn+qn,"$"].join("|")+")",Pn+"?"+qn+"+"+Jn,Pn+"+"+Kn,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",Ln,Hn].join("|"),"g"),et=RegExp("[\\u200d\\ud800-\\udfff"+In+Bn+"]"),ut=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,it=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],ot=-1,at={};at[T]=at[W]=at[U]=at[D]=at[$]=at[M]=at[N]=at[P]=at[q]=!0,at[y]=at[m]=at[F]=at[w]=at[L]=at[b]=at[x]=at[j]=at[I]=at[E]=at[k]=at[B]=at[S]=at[R]=at[C]=!1;var ct={};ct[y]=ct[m]=ct[F]=ct[L]=ct[w]=ct[b]=ct[T]=ct[W]=ct[U]=ct[D]=ct[$]=ct[I]=ct[E]=ct[k]=ct[B]=ct[S]=ct[R]=ct[z]=ct[M]=ct[N]=ct[P]=ct[q]=!0,ct[x]=ct[j]=ct[C]=!1;var ft={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},lt=parseFloat,st=parseInt,ht="object"==typeof r.g&&r.g&&r.g.Object===Object&&r.g,pt="object"==typeof self&&self&&self.Object===Object&&self,vt=ht||pt||Function("return this")(),_t=t&&!t.nodeType&&t,gt=_t&&n&&!n.nodeType&&n,dt=gt&>.exports===_t,yt=dt&&ht.process,mt=function(){try{var n=gt&>.require&>.require("util").types;return n||yt&&yt.binding&&yt.binding("util")}catch(n){}}(),wt=mt&&mt.isArrayBuffer,bt=mt&&mt.isDate,xt=mt&&mt.isMap,jt=mt&&mt.isRegExp,At=mt&&mt.isSet,It=mt&&mt.isTypedArray;function Et(n,t,r){switch(r.length){case 0:return n.call(t);case 1:return n.call(t,r[0]);case 2:return n.call(t,r[0],r[1]);case 3:return n.call(t,r[0],r[1],r[2])}return n.apply(t,r)}function kt(n,t,r,e){for(var u=-1,i=null==n?0:n.length;++u-1}function Ct(n,t,r){for(var e=-1,u=null==n?0:n.length;++e-1;);return r}function rr(n,t){for(var r=n.length;r--&&Nt(t,n[r],0)>-1;);return r}function er(n,t){for(var r=n.length,e=0;r--;)n[r]===t&&++e;return e}var ur=Kt({À:"A",Á:"A",Â:"A",Ã:"A",Ä:"A",Å:"A",à:"a",á:"a",â:"a",ã:"a",ä:"a",å:"a",Ç:"C",ç:"c",Ð:"D",ð:"d",È:"E",É:"E",Ê:"E",Ë:"E",è:"e",é:"e",ê:"e",ë:"e",Ì:"I",Í:"I",Î:"I",Ï:"I",ì:"i",í:"i",î:"i",ï:"i",Ñ:"N",ñ:"n",Ò:"O",Ó:"O",Ô:"O",Õ:"O",Ö:"O",Ø:"O",ò:"o",ó:"o",ô:"o",õ:"o",ö:"o",ø:"o",Ù:"U",Ú:"U",Û:"U",Ü:"U",ù:"u",ú:"u",û:"u",ü:"u",Ý:"Y",ý:"y",ÿ:"y",Æ:"Ae",æ:"ae",Þ:"Th",þ:"th",ß:"ss",Ā:"A",Ă:"A",Ą:"A",ā:"a",ă:"a",ą:"a",Ć:"C",Ĉ:"C",Ċ:"C",Č:"C",ć:"c",ĉ:"c",ċ:"c",č:"c",Ď:"D",Đ:"D",ď:"d",đ:"d",Ē:"E",Ĕ:"E",Ė:"E",Ę:"E",Ě:"E",ē:"e",ĕ:"e",ė:"e",ę:"e",ě:"e",Ĝ:"G",Ğ:"G",Ġ:"G",Ģ:"G",ĝ:"g",ğ:"g",ġ:"g",ģ:"g",Ĥ:"H",Ħ:"H",ĥ:"h",ħ:"h",Ĩ:"I",Ī:"I",Ĭ:"I",Į:"I",İ:"I",ĩ:"i",ī:"i",ĭ:"i",į:"i",ı:"i",Ĵ:"J",ĵ:"j",Ķ:"K",ķ:"k",ĸ:"k",Ĺ:"L",Ļ:"L",Ľ:"L",Ŀ:"L",Ł:"L",ĺ:"l",ļ:"l",ľ:"l",ŀ:"l",ł:"l",Ń:"N",Ņ:"N",Ň:"N",Ŋ:"N",ń:"n",ņ:"n",ň:"n",ŋ:"n",Ō:"O",Ŏ:"O",Ő:"O",ō:"o",ŏ:"o",ő:"o",Ŕ:"R",Ŗ:"R",Ř:"R",ŕ:"r",ŗ:"r",ř:"r",Ś:"S",Ŝ:"S",Ş:"S",Š:"S",ś:"s",ŝ:"s",ş:"s",š:"s",Ţ:"T",Ť:"T",Ŧ:"T",ţ:"t",ť:"t",ŧ:"t",Ũ:"U",Ū:"U",Ŭ:"U",Ů:"U",Ű:"U",Ų:"U",ũ:"u",ū:"u",ŭ:"u",ů:"u",ű:"u",ų:"u",Ŵ:"W",ŵ:"w",Ŷ:"Y",ŷ:"y",Ÿ:"Y",Ź:"Z",Ż:"Z",Ž:"Z",ź:"z",ż:"z",ž:"z",IJ:"IJ",ij:"ij",Œ:"Oe",œ:"oe",ʼn:"'n",ſ:"s"}),ir=Kt({"&":"&","<":"<",">":">",'"':""","'":"'"});function or(n){return"\\"+ft[n]}function ar(n){return et.test(n)}function cr(n){var t=-1,r=Array(n.size);return n.forEach((function(n,e){r[++t]=[e,n]})),r}function fr(n,t){return function(r){return n(t(r))}}function lr(n,t){for(var r=-1,e=n.length,u=0,i=[];++r",""":'"',"'":"'"});var dr=function n(t){var r,e=(t=null==t?vt:dr.defaults(vt.Object(),t,dr.pick(vt,it))).Array,cn=t.Date,In=t.Error,En=t.Function,kn=t.Math,On=t.Object,Bn=t.RegExp,Sn=t.String,Rn=t.TypeError,zn=e.prototype,Cn=En.prototype,Fn=On.prototype,Ln=t["__core-js_shared__"],Tn=Cn.toString,Wn=Fn.hasOwnProperty,Un=0,Dn=(r=/[^.]+$/.exec(Ln&&Ln.keys&&Ln.keys.IE_PROTO||""))?"Symbol(src)_1."+r:"",$n=Fn.toString,Mn=Tn.call(On),Nn=vt._,Pn=Bn("^"+Tn.call(Wn).replace(un,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),qn=dt?t.Buffer:u,Zn=t.Symbol,Jn=t.Uint8Array,Kn=qn?qn.allocUnsafe:u,Yn=fr(On.getPrototypeOf,On),Gn=On.create,Vn=Fn.propertyIsEnumerable,Hn=zn.splice,Xn=Zn?Zn.isConcatSpreadable:u,tt=Zn?Zn.iterator:u,et=Zn?Zn.toStringTag:u,ft=function(){try{var n=pi(On,"defineProperty");return n({},"",{}),n}catch(n){}}(),ht=t.clearTimeout!==vt.clearTimeout&&t.clearTimeout,pt=cn&&cn.now!==vt.Date.now&&cn.now,_t=t.setTimeout!==vt.setTimeout&&t.setTimeout,gt=kn.ceil,yt=kn.floor,mt=On.getOwnPropertySymbols,Dt=qn?qn.isBuffer:u,Kt=t.isFinite,yr=zn.join,mr=fr(On.keys,On),wr=kn.max,br=kn.min,xr=cn.now,jr=t.parseInt,Ar=kn.random,Ir=zn.reverse,Er=pi(t,"DataView"),kr=pi(t,"Map"),Or=pi(t,"Promise"),Br=pi(t,"Set"),Sr=pi(t,"WeakMap"),Rr=pi(On,"create"),zr=Sr&&new Sr,Cr={},Fr=$i(Er),Lr=$i(kr),Tr=$i(Or),Wr=$i(Br),Ur=$i(Sr),Dr=Zn?Zn.prototype:u,$r=Dr?Dr.valueOf:u,Mr=Dr?Dr.toString:u;function Nr(n){if(ua(n)&&!Ko(n)&&!(n instanceof Jr)){if(n instanceof Zr)return n;if(Wn.call(n,"__wrapped__"))return Mi(n)}return new Zr(n)}var Pr=function(){function n(){}return function(t){if(!ea(t))return{};if(Gn)return Gn(t);n.prototype=t;var r=new n;return n.prototype=u,r}}();function qr(){}function Zr(n,t){this.__wrapped__=n,this.__actions__=[],this.__chain__=!!t,this.__index__=0,this.__values__=u}function Jr(n){this.__wrapped__=n,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=g,this.__views__=[]}function Kr(n){var t=-1,r=null==n?0:n.length;for(this.clear();++t=t?n:t)),n}function le(n,t,r,e,i,o){var a,c=1&t,f=2&t,l=4&t;if(r&&(a=i?r(n,e,i,o):r(n)),a!==u)return a;if(!ea(n))return n;var s=Ko(n);if(s){if(a=function(n){var t=n.length,r=new n.constructor(t);t&&"string"==typeof n[0]&&Wn.call(n,"index")&&(r.index=n.index,r.input=n.input);return r}(n),!c)return Ru(n,a)}else{var h=gi(n),p=h==j||h==A;if(Ho(n))return Iu(n,c);if(h==k||h==y||p&&!i){if(a=f||p?{}:yi(n),!c)return f?function(n,t){return zu(n,_i(n),t)}(n,function(n,t){return n&&zu(t,La(t),n)}(a,n)):function(n,t){return zu(n,vi(n),t)}(n,oe(a,n))}else{if(!ct[h])return i?n:{};a=function(n,t,r){var e=n.constructor;switch(t){case F:return Eu(n);case w:case b:return new e(+n);case L:return function(n,t){var r=t?Eu(n.buffer):n.buffer;return new n.constructor(r,n.byteOffset,n.byteLength)}(n,r);case T:case W:case U:case D:case $:case M:case N:case P:case q:return ku(n,r);case I:return new e;case E:case R:return new e(n);case B:return function(n){var t=new n.constructor(n.source,gn.exec(n));return t.lastIndex=n.lastIndex,t}(n);case S:return new e;case z:return u=n,$r?On($r.call(u)):{}}var u}(n,h,c)}}o||(o=new Hr);var v=o.get(n);if(v)return v;o.set(n,a),fa(n)?n.forEach((function(e){a.add(le(e,t,r,e,n,o))})):ia(n)&&n.forEach((function(e,u){a.set(u,le(e,t,r,u,n,o))}));var _=s?u:(l?f?oi:ii:f?La:Fa)(n);return Ot(_||n,(function(e,u){_&&(e=n[u=e]),ee(a,u,le(e,t,r,u,n,o))})),a}function se(n,t,r){var e=r.length;if(null==n)return!e;for(n=On(n);e--;){var i=r[e],o=t[i],a=n[i];if(a===u&&!(i in n)||!o(a))return!1}return!0}function he(n,t,r){if("function"!=typeof n)throw new Rn(i);return Ci((function(){n.apply(u,r)}),t)}function pe(n,t,r,e){var u=-1,i=zt,o=!0,a=n.length,c=[],f=t.length;if(!a)return c;r&&(t=Ft(t,Xt(r))),e?(i=Ct,o=!1):t.length>=200&&(i=nr,o=!1,t=new Vr(t));n:for(;++u-1},Yr.prototype.set=function(n,t){var r=this.__data__,e=ue(r,n);return e<0?(++this.size,r.push([n,t])):r[e][1]=t,this},Gr.prototype.clear=function(){this.size=0,this.__data__={hash:new Kr,map:new(kr||Yr),string:new Kr}},Gr.prototype.delete=function(n){var t=si(this,n).delete(n);return this.size-=t?1:0,t},Gr.prototype.get=function(n){return si(this,n).get(n)},Gr.prototype.has=function(n){return si(this,n).has(n)},Gr.prototype.set=function(n,t){var r=si(this,n),e=r.size;return r.set(n,t),this.size+=r.size==e?0:1,this},Vr.prototype.add=Vr.prototype.push=function(n){return this.__data__.set(n,o),this},Vr.prototype.has=function(n){return this.__data__.has(n)},Hr.prototype.clear=function(){this.__data__=new Yr,this.size=0},Hr.prototype.delete=function(n){var t=this.__data__,r=t.delete(n);return this.size=t.size,r},Hr.prototype.get=function(n){return this.__data__.get(n)},Hr.prototype.has=function(n){return this.__data__.has(n)},Hr.prototype.set=function(n,t){var r=this.__data__;if(r instanceof Yr){var e=r.__data__;if(!kr||e.length<199)return e.push([n,t]),this.size=++r.size,this;r=this.__data__=new Gr(e)}return r.set(n,t),this.size=r.size,this};var ve=Lu(xe),_e=Lu(je,!0);function ge(n,t){var r=!0;return ve(n,(function(n,e,u){return r=!!t(n,e,u)})),r}function de(n,t,r){for(var e=-1,i=n.length;++e0&&r(a)?t>1?me(a,t-1,r,e,u):Lt(u,a):e||(u[u.length]=a)}return u}var we=Tu(),be=Tu(!0);function xe(n,t){return n&&we(n,t,Fa)}function je(n,t){return n&&be(n,t,Fa)}function Ae(n,t){return Rt(t,(function(t){return na(n[t])}))}function Ie(n,t){for(var r=0,e=(t=bu(t,n)).length;null!=n&&rt}function Be(n,t){return null!=n&&Wn.call(n,t)}function Se(n,t){return null!=n&&t in On(n)}function Re(n,t,r){for(var i=r?Ct:zt,o=n[0].length,a=n.length,c=a,f=e(a),l=1/0,s=[];c--;){var h=n[c];c&&t&&(h=Ft(h,Xt(t))),l=br(h.length,l),f[c]=!r&&(t||o>=120&&h.length>=120)?new Vr(c&&h):u}h=n[0];var p=-1,v=f[0];n:for(;++p=a?c:c*("desc"==r[e]?-1:1)}return n.index-t.index}(n,t,r)}))}function Ke(n,t,r){for(var e=-1,u=t.length,i={};++e-1;)a!==n&&Hn.call(a,c,1),Hn.call(n,c,1);return n}function Ge(n,t){for(var r=n?t.length:0,e=r-1;r--;){var u=t[r];if(r==e||u!==i){var i=u;wi(u)?Hn.call(n,u,1):pu(n,u)}}return n}function Ve(n,t){return n+yt(Ar()*(t-n+1))}function He(n,t){var r="";if(!n||t<1||t>v)return r;do{t%2&&(r+=n),(t=yt(t/2))&&(n+=n)}while(t);return r}function Xe(n,t){return Fi(Oi(n,t,oc),n+"")}function Qe(n){return Qr(Pa(n))}function nu(n,t){var r=Pa(n);return Wi(r,fe(t,0,r.length))}function tu(n,t,r,e){if(!ea(n))return n;for(var i=-1,o=(t=bu(t,n)).length,a=o-1,c=n;null!=c&&++ii?0:i+t),(r=r>i?i:r)<0&&(r+=i),i=t>r?0:r-t>>>0,t>>>=0;for(var o=e(i);++u>>1,o=n[i];null!==o&&!sa(o)&&(r?o<=t:o=200){var f=t?null:Hu(n);if(f)return sr(f);o=!1,u=nr,c=new Vr}else c=t?[]:a;n:for(;++e=e?n:iu(n,t,r)}var Au=ht||function(n){return vt.clearTimeout(n)};function Iu(n,t){if(t)return n.slice();var r=n.length,e=Kn?Kn(r):new n.constructor(r);return n.copy(e),e}function Eu(n){var t=new n.constructor(n.byteLength);return new Jn(t).set(new Jn(n)),t}function ku(n,t){var r=t?Eu(n.buffer):n.buffer;return new n.constructor(r,n.byteOffset,n.length)}function Ou(n,t){if(n!==t){var r=n!==u,e=null===n,i=n==n,o=sa(n),a=t!==u,c=null===t,f=t==t,l=sa(t);if(!c&&!l&&!o&&n>t||o&&a&&f&&!c&&!l||e&&a&&f||!r&&f||!i)return 1;if(!e&&!o&&!l&&n1?r[i-1]:u,a=i>2?r[2]:u;for(o=n.length>3&&"function"==typeof o?(i--,o):u,a&&bi(r[0],r[1],a)&&(o=i<3?u:o,i=1),t=On(t);++e-1?i[o?t[a]:a]:u}}function Mu(n){return ui((function(t){var r=t.length,e=r,o=Zr.prototype.thru;for(n&&t.reverse();e--;){var a=t[e];if("function"!=typeof a)throw new Rn(i);if(o&&!c&&"wrapper"==ci(a))var c=new Zr([],!0)}for(e=c?e:r;++e1&&m.reverse(),p&&lc))return!1;var l=o.get(n),s=o.get(t);if(l&&s)return l==t&&s==n;var h=-1,p=!0,v=2&r?new Vr:u;for(o.set(n,t),o.set(t,n);++h-1&&n%1==0&&n1?"& ":"")+t[e],t=t.join(r>2?", ":" "),n.replace(fn,"{\n/* [wrapped with "+t+"] */\n")}(e,function(n,t){return Ot(d,(function(r){var e="_."+r[0];t&r[1]&&!zt(n,e)&&n.push(e)})),n.sort()}(function(n){var t=n.match(ln);return t?t[1].split(sn):[]}(e),r)))}function Ti(n){var t=0,r=0;return function(){var e=xr(),i=16-(e-r);if(r=e,i>0){if(++t>=800)return arguments[0]}else t=0;return n.apply(u,arguments)}}function Wi(n,t){var r=-1,e=n.length,i=e-1;for(t=t===u?e:t;++r1?n[t-1]:u;return r="function"==typeof r?(n.pop(),r):u,ao(n,r)}));function vo(n){var t=Nr(n);return t.__chain__=!0,t}function _o(n,t){return t(n)}var go=ui((function(n){var t=n.length,r=t?n[0]:0,e=this.__wrapped__,i=function(t){return ce(t,n)};return!(t>1||this.__actions__.length)&&e instanceof Jr&&wi(r)?((e=e.slice(r,+r+(t?1:0))).__actions__.push({func:_o,args:[i],thisArg:u}),new Zr(e,this.__chain__).thru((function(n){return t&&!n.length&&n.push(u),n}))):this.thru(i)}));var yo=Cu((function(n,t,r){Wn.call(n,r)?++n[r]:ae(n,r,1)}));var mo=$u(Zi),wo=$u(Ji);function bo(n,t){return(Ko(n)?Ot:ve)(n,li(t,3))}function xo(n,t){return(Ko(n)?Bt:_e)(n,li(t,3))}var jo=Cu((function(n,t,r){Wn.call(n,r)?n[r].push(t):ae(n,r,[t])}));var Ao=Xe((function(n,t,r){var u=-1,i="function"==typeof t,o=Go(n)?e(n.length):[];return ve(n,(function(n){o[++u]=i?Et(t,n,r):ze(n,t,r)})),o})),Io=Cu((function(n,t,r){ae(n,r,t)}));function Eo(n,t){return(Ko(n)?Ft:Me)(n,li(t,3))}var ko=Cu((function(n,t,r){n[r?0:1].push(t)}),(function(){return[[],[]]}));var Oo=Xe((function(n,t){if(null==n)return[];var r=t.length;return r>1&&bi(n,t[0],t[1])?t=[]:r>2&&bi(t[0],t[1],t[2])&&(t=[t[0]]),Je(n,me(t,1),[])})),Bo=pt||function(){return vt.Date.now()};function So(n,t,r){return t=r?u:t,t=n&&null==t?n.length:t,Qu(n,s,u,u,u,u,t)}function Ro(n,t){var r;if("function"!=typeof t)throw new Rn(i);return n=da(n),function(){return--n>0&&(r=t.apply(this,arguments)),n<=1&&(t=u),r}}var zo=Xe((function(n,t,r){var e=1;if(r.length){var u=lr(r,fi(zo));e|=f}return Qu(n,e,t,r,u)})),Co=Xe((function(n,t,r){var e=3;if(r.length){var u=lr(r,fi(Co));e|=f}return Qu(t,e,n,r,u)}));function Fo(n,t,r){var e,o,a,c,f,l,s=0,h=!1,p=!1,v=!0;if("function"!=typeof n)throw new Rn(i);function _(t){var r=e,i=o;return e=o=u,s=t,c=n.apply(i,r)}function g(n){return s=n,f=Ci(y,t),h?_(n):c}function d(n){var r=n-l;return l===u||r>=t||r<0||p&&n-s>=a}function y(){var n=Bo();if(d(n))return m(n);f=Ci(y,function(n){var r=t-(n-l);return p?br(r,a-(n-s)):r}(n))}function m(n){return f=u,v&&e?_(n):(e=o=u,c)}function w(){var n=Bo(),r=d(n);if(e=arguments,o=this,l=n,r){if(f===u)return g(l);if(p)return Au(f),f=Ci(y,t),_(l)}return f===u&&(f=Ci(y,t)),c}return t=ma(t)||0,ea(r)&&(h=!!r.leading,a=(p="maxWait"in r)?wr(ma(r.maxWait)||0,t):a,v="trailing"in r?!!r.trailing:v),w.cancel=function(){f!==u&&Au(f),s=0,e=l=o=f=u},w.flush=function(){return f===u?c:m(Bo())},w}var Lo=Xe((function(n,t){return he(n,1,t)})),To=Xe((function(n,t,r){return he(n,ma(t)||0,r)}));function Wo(n,t){if("function"!=typeof n||null!=t&&"function"!=typeof t)throw new Rn(i);var r=function(){var e=arguments,u=t?t.apply(this,e):e[0],i=r.cache;if(i.has(u))return i.get(u);var o=n.apply(this,e);return r.cache=i.set(u,o)||i,o};return r.cache=new(Wo.Cache||Gr),r}function Uo(n){if("function"!=typeof n)throw new Rn(i);return function(){var t=arguments;switch(t.length){case 0:return!n.call(this);case 1:return!n.call(this,t[0]);case 2:return!n.call(this,t[0],t[1]);case 3:return!n.call(this,t[0],t[1],t[2])}return!n.apply(this,t)}}Wo.Cache=Gr;var Do=xu((function(n,t){var r=(t=1==t.length&&Ko(t[0])?Ft(t[0],Xt(li())):Ft(me(t,1),Xt(li()))).length;return Xe((function(e){for(var u=-1,i=br(e.length,r);++u=t})),Jo=Ce(function(){return arguments}())?Ce:function(n){return ua(n)&&Wn.call(n,"callee")&&!Vn.call(n,"callee")},Ko=e.isArray,Yo=wt?Xt(wt):function(n){return ua(n)&&ke(n)==F};function Go(n){return null!=n&&ra(n.length)&&!na(n)}function Vo(n){return ua(n)&&Go(n)}var Ho=Dt||mc,Xo=bt?Xt(bt):function(n){return ua(n)&&ke(n)==b};function Qo(n){if(!ua(n))return!1;var t=ke(n);return t==x||"[object DOMException]"==t||"string"==typeof n.message&&"string"==typeof n.name&&!aa(n)}function na(n){if(!ea(n))return!1;var t=ke(n);return t==j||t==A||"[object AsyncFunction]"==t||"[object Proxy]"==t}function ta(n){return"number"==typeof n&&n==da(n)}function ra(n){return"number"==typeof n&&n>-1&&n%1==0&&n<=v}function ea(n){var t=typeof n;return null!=n&&("object"==t||"function"==t)}function ua(n){return null!=n&&"object"==typeof n}var ia=xt?Xt(xt):function(n){return ua(n)&&gi(n)==I};function oa(n){return"number"==typeof n||ua(n)&&ke(n)==E}function aa(n){if(!ua(n)||ke(n)!=k)return!1;var t=Yn(n);if(null===t)return!0;var r=Wn.call(t,"constructor")&&t.constructor;return"function"==typeof r&&r instanceof r&&Tn.call(r)==Mn}var ca=jt?Xt(jt):function(n){return ua(n)&&ke(n)==B};var fa=At?Xt(At):function(n){return ua(n)&&gi(n)==S};function la(n){return"string"==typeof n||!Ko(n)&&ua(n)&&ke(n)==R}function sa(n){return"symbol"==typeof n||ua(n)&&ke(n)==z}var ha=It?Xt(It):function(n){return ua(n)&&ra(n.length)&&!!at[ke(n)]};var pa=Yu($e),va=Yu((function(n,t){return n<=t}));function _a(n){if(!n)return[];if(Go(n))return la(n)?vr(n):Ru(n);if(tt&&n[tt])return function(n){for(var t,r=[];!(t=n.next()).done;)r.push(t.value);return r}(n[tt]());var t=gi(n);return(t==I?cr:t==S?sr:Pa)(n)}function ga(n){return n?(n=ma(n))===p||n===-1/0?17976931348623157e292*(n<0?-1:1):n==n?n:0:0===n?n:0}function da(n){var t=ga(n),r=t%1;return t==t?r?t-r:t:0}function ya(n){return n?fe(da(n),0,g):0}function ma(n){if("number"==typeof n)return n;if(sa(n))return _;if(ea(n)){var t="function"==typeof n.valueOf?n.valueOf():n;n=ea(t)?t+"":t}if("string"!=typeof n)return 0===n?n:+n;n=Ht(n);var r=yn.test(n);return r||wn.test(n)?st(n.slice(2),r?2:8):dn.test(n)?_:+n}function wa(n){return zu(n,La(n))}function ba(n){return null==n?"":su(n)}var xa=Fu((function(n,t){if(Ii(t)||Go(t))zu(t,Fa(t),n);else for(var r in t)Wn.call(t,r)&&ee(n,r,t[r])})),ja=Fu((function(n,t){zu(t,La(t),n)})),Aa=Fu((function(n,t,r,e){zu(t,La(t),n,e)})),Ia=Fu((function(n,t,r,e){zu(t,Fa(t),n,e)})),Ea=ui(ce);var ka=Xe((function(n,t){n=On(n);var r=-1,e=t.length,i=e>2?t[2]:u;for(i&&bi(t[0],t[1],i)&&(e=1);++r1),t})),zu(n,oi(n),r),e&&(r=le(r,7,ri));for(var u=t.length;u--;)pu(r,t[u]);return r}));var Da=ui((function(n,t){return null==n?{}:function(n,t){return Ke(n,t,(function(t,r){return Sa(n,r)}))}(n,t)}));function $a(n,t){if(null==n)return{};var r=Ft(oi(n),(function(n){return[n]}));return t=li(t),Ke(n,r,(function(n,r){return t(n,r[0])}))}var Ma=Xu(Fa),Na=Xu(La);function Pa(n){return null==n?[]:Qt(n,Fa(n))}var qa=Uu((function(n,t,r){return t=t.toLowerCase(),n+(r?Za(t):t)}));function Za(n){return Qa(ba(n).toLowerCase())}function Ja(n){return(n=ba(n))&&n.replace(xn,ur).replace(nt,"")}var Ka=Uu((function(n,t,r){return n+(r?"-":"")+t.toLowerCase()})),Ya=Uu((function(n,t,r){return n+(r?" ":"")+t.toLowerCase()})),Ga=Wu("toLowerCase");var Va=Uu((function(n,t,r){return n+(r?"_":"")+t.toLowerCase()}));var Ha=Uu((function(n,t,r){return n+(r?" ":"")+Qa(t)}));var Xa=Uu((function(n,t,r){return n+(r?" ":"")+t.toUpperCase()})),Qa=Wu("toUpperCase");function nc(n,t,r){return n=ba(n),(t=r?u:t)===u?function(n){return ut.test(n)}(n)?function(n){return n.match(rt)||[]}(n):function(n){return n.match(hn)||[]}(n):n.match(t)||[]}var tc=Xe((function(n,t){try{return Et(n,u,t)}catch(n){return Qo(n)?n:new In(n)}})),rc=ui((function(n,t){return Ot(t,(function(t){t=Di(t),ae(n,t,zo(n[t],n))})),n}));function ec(n){return function(){return n}}var uc=Mu(),ic=Mu(!0);function oc(n){return n}function ac(n){return We("function"==typeof n?n:le(n,1))}var cc=Xe((function(n,t){return function(r){return ze(r,n,t)}})),fc=Xe((function(n,t){return function(r){return ze(n,r,t)}}));function lc(n,t,r){var e=Fa(t),u=Ae(t,e);null!=r||ea(t)&&(u.length||!e.length)||(r=t,t=n,n=this,u=Ae(t,Fa(t)));var i=!(ea(r)&&"chain"in r&&!r.chain),o=na(n);return Ot(u,(function(r){var e=t[r];n[r]=e,o&&(n.prototype[r]=function(){var t=this.__chain__;if(i||t){var r=n(this.__wrapped__),u=r.__actions__=Ru(this.__actions__);return u.push({func:e,args:arguments,thisArg:n}),r.__chain__=t,r}return e.apply(n,Lt([this.value()],arguments))})})),n}function sc(){}var hc=Zu(Ft),pc=Zu(St),vc=Zu(Ut);function _c(n){return xi(n)?Jt(Di(n)):function(n){return function(t){return Ie(t,n)}}(n)}var gc=Ku(),dc=Ku(!0);function yc(){return[]}function mc(){return!1}var wc=qu((function(n,t){return n+t}),0),bc=Vu("ceil"),xc=qu((function(n,t){return n/t}),1),jc=Vu("floor");var Ac,Ic=qu((function(n,t){return n*t}),1),Ec=Vu("round"),kc=qu((function(n,t){return n-t}),0);return Nr.after=function(n,t){if("function"!=typeof t)throw new Rn(i);return n=da(n),function(){if(--n<1)return t.apply(this,arguments)}},Nr.ary=So,Nr.assign=xa,Nr.assignIn=ja,Nr.assignInWith=Aa,Nr.assignWith=Ia,Nr.at=Ea,Nr.before=Ro,Nr.bind=zo,Nr.bindAll=rc,Nr.bindKey=Co,Nr.castArray=function(){if(!arguments.length)return[];var n=arguments[0];return Ko(n)?n:[n]},Nr.chain=vo,Nr.chunk=function(n,t,r){t=(r?bi(n,t,r):t===u)?1:wr(da(t),0);var i=null==n?0:n.length;if(!i||t<1)return[];for(var o=0,a=0,c=e(gt(i/t));oi?0:i+r),(e=e===u||e>i?i:da(e))<0&&(e+=i),e=r>e?0:ya(e);r>>0)?(n=ba(n))&&("string"==typeof t||null!=t&&!ca(t))&&!(t=su(t))&&ar(n)?ju(vr(n),0,r):n.split(t,r):[]},Nr.spread=function(n,t){if("function"!=typeof n)throw new Rn(i);return t=null==t?0:wr(da(t),0),Xe((function(r){var e=r[t],u=ju(r,0,t);return e&&Lt(u,e),Et(n,this,u)}))},Nr.tail=function(n){var t=null==n?0:n.length;return t?iu(n,1,t):[]},Nr.take=function(n,t,r){return n&&n.length?iu(n,0,(t=r||t===u?1:da(t))<0?0:t):[]},Nr.takeRight=function(n,t,r){var e=null==n?0:n.length;return e?iu(n,(t=e-(t=r||t===u?1:da(t)))<0?0:t,e):[]},Nr.takeRightWhile=function(n,t){return n&&n.length?_u(n,li(t,3),!1,!0):[]},Nr.takeWhile=function(n,t){return n&&n.length?_u(n,li(t,3)):[]},Nr.tap=function(n,t){return t(n),n},Nr.throttle=function(n,t,r){var e=!0,u=!0;if("function"!=typeof n)throw new Rn(i);return ea(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),Fo(n,t,{leading:e,maxWait:t,trailing:u})},Nr.thru=_o,Nr.toArray=_a,Nr.toPairs=Ma,Nr.toPairsIn=Na,Nr.toPath=function(n){return Ko(n)?Ft(n,Di):sa(n)?[n]:Ru(Ui(ba(n)))},Nr.toPlainObject=wa,Nr.transform=function(n,t,r){var e=Ko(n),u=e||Ho(n)||ha(n);if(t=li(t,4),null==r){var i=n&&n.constructor;r=u?e?new i:[]:ea(n)&&na(i)?Pr(Yn(n)):{}}return(u?Ot:xe)(n,(function(n,e,u){return t(r,n,e,u)})),r},Nr.unary=function(n){return So(n,1)},Nr.union=eo,Nr.unionBy=uo,Nr.unionWith=io,Nr.uniq=function(n){return n&&n.length?hu(n):[]},Nr.uniqBy=function(n,t){return n&&n.length?hu(n,li(t,2)):[]},Nr.uniqWith=function(n,t){return t="function"==typeof t?t:u,n&&n.length?hu(n,u,t):[]},Nr.unset=function(n,t){return null==n||pu(n,t)},Nr.unzip=oo,Nr.unzipWith=ao,Nr.update=function(n,t,r){return null==n?n:vu(n,t,wu(r))},Nr.updateWith=function(n,t,r,e){return e="function"==typeof e?e:u,null==n?n:vu(n,t,wu(r),e)},Nr.values=Pa,Nr.valuesIn=function(n){return null==n?[]:Qt(n,La(n))},Nr.without=co,Nr.words=nc,Nr.wrap=function(n,t){return $o(wu(t),n)},Nr.xor=fo,Nr.xorBy=lo,Nr.xorWith=so,Nr.zip=ho,Nr.zipObject=function(n,t){return yu(n||[],t||[],ee)},Nr.zipObjectDeep=function(n,t){return yu(n||[],t||[],tu)},Nr.zipWith=po,Nr.entries=Ma,Nr.entriesIn=Na,Nr.extend=ja,Nr.extendWith=Aa,lc(Nr,Nr),Nr.add=wc,Nr.attempt=tc,Nr.camelCase=qa,Nr.capitalize=Za,Nr.ceil=bc,Nr.clamp=function(n,t,r){return r===u&&(r=t,t=u),r!==u&&(r=(r=ma(r))==r?r:0),t!==u&&(t=(t=ma(t))==t?t:0),fe(ma(n),t,r)},Nr.clone=function(n){return le(n,4)},Nr.cloneDeep=function(n){return le(n,5)},Nr.cloneDeepWith=function(n,t){return le(n,5,t="function"==typeof t?t:u)},Nr.cloneWith=function(n,t){return le(n,4,t="function"==typeof t?t:u)},Nr.conformsTo=function(n,t){return null==t||se(n,t,Fa(t))},Nr.deburr=Ja,Nr.defaultTo=function(n,t){return null==n||n!=n?t:n},Nr.divide=xc,Nr.endsWith=function(n,t,r){n=ba(n),t=su(t);var e=n.length,i=r=r===u?e:fe(da(r),0,e);return(r-=t.length)>=0&&n.slice(r,i)==t},Nr.eq=Po,Nr.escape=function(n){return(n=ba(n))&&H.test(n)?n.replace(G,ir):n},Nr.escapeRegExp=function(n){return(n=ba(n))&&on.test(n)?n.replace(un,"\\$&"):n},Nr.every=function(n,t,r){var e=Ko(n)?St:ge;return r&&bi(n,t,r)&&(t=u),e(n,li(t,3))},Nr.find=mo,Nr.findIndex=Zi,Nr.findKey=function(n,t){return $t(n,li(t,3),xe)},Nr.findLast=wo,Nr.findLastIndex=Ji,Nr.findLastKey=function(n,t){return $t(n,li(t,3),je)},Nr.floor=jc,Nr.forEach=bo,Nr.forEachRight=xo,Nr.forIn=function(n,t){return null==n?n:we(n,li(t,3),La)},Nr.forInRight=function(n,t){return null==n?n:be(n,li(t,3),La)},Nr.forOwn=function(n,t){return n&&xe(n,li(t,3))},Nr.forOwnRight=function(n,t){return n&&je(n,li(t,3))},Nr.get=Ba,Nr.gt=qo,Nr.gte=Zo,Nr.has=function(n,t){return null!=n&&di(n,t,Be)},Nr.hasIn=Sa,Nr.head=Yi,Nr.identity=oc,Nr.includes=function(n,t,r,e){n=Go(n)?n:Pa(n),r=r&&!e?da(r):0;var u=n.length;return r<0&&(r=wr(u+r,0)),la(n)?r<=u&&n.indexOf(t,r)>-1:!!u&&Nt(n,t,r)>-1},Nr.indexOf=function(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var u=null==r?0:da(r);return u<0&&(u=wr(e+u,0)),Nt(n,t,u)},Nr.inRange=function(n,t,r){return t=ga(t),r===u?(r=t,t=0):r=ga(r),function(n,t,r){return n>=br(t,r)&&n=-9007199254740991&&n<=v},Nr.isSet=fa,Nr.isString=la,Nr.isSymbol=sa,Nr.isTypedArray=ha,Nr.isUndefined=function(n){return n===u},Nr.isWeakMap=function(n){return ua(n)&&gi(n)==C},Nr.isWeakSet=function(n){return ua(n)&&"[object WeakSet]"==ke(n)},Nr.join=function(n,t){return null==n?"":yr.call(n,t)},Nr.kebabCase=Ka,Nr.last=Xi,Nr.lastIndexOf=function(n,t,r){var e=null==n?0:n.length;if(!e)return-1;var i=e;return r!==u&&(i=(i=da(r))<0?wr(e+i,0):br(i,e-1)),t==t?function(n,t,r){for(var e=r+1;e--;)if(n[e]===t)return e;return e}(n,t,i):Mt(n,qt,i,!0)},Nr.lowerCase=Ya,Nr.lowerFirst=Ga,Nr.lt=pa,Nr.lte=va,Nr.max=function(n){return n&&n.length?de(n,oc,Oe):u},Nr.maxBy=function(n,t){return n&&n.length?de(n,li(t,2),Oe):u},Nr.mean=function(n){return Zt(n,oc)},Nr.meanBy=function(n,t){return Zt(n,li(t,2))},Nr.min=function(n){return n&&n.length?de(n,oc,$e):u},Nr.minBy=function(n,t){return n&&n.length?de(n,li(t,2),$e):u},Nr.stubArray=yc,Nr.stubFalse=mc,Nr.stubObject=function(){return{}},Nr.stubString=function(){return""},Nr.stubTrue=function(){return!0},Nr.multiply=Ic,Nr.nth=function(n,t){return n&&n.length?Ze(n,da(t)):u},Nr.noConflict=function(){return vt._===this&&(vt._=Nn),this},Nr.noop=sc,Nr.now=Bo,Nr.pad=function(n,t,r){n=ba(n);var e=(t=da(t))?pr(n):0;if(!t||e>=t)return n;var u=(t-e)/2;return Ju(yt(u),r)+n+Ju(gt(u),r)},Nr.padEnd=function(n,t,r){n=ba(n);var e=(t=da(t))?pr(n):0;return t&&et){var e=n;n=t,t=e}if(r||n%1||t%1){var i=Ar();return br(n+i*(t-n+lt("1e-"+((i+"").length-1))),t)}return Ve(n,t)},Nr.reduce=function(n,t,r){var e=Ko(n)?Tt:Yt,u=arguments.length<3;return e(n,li(t,4),r,u,ve)},Nr.reduceRight=function(n,t,r){var e=Ko(n)?Wt:Yt,u=arguments.length<3;return e(n,li(t,4),r,u,_e)},Nr.repeat=function(n,t,r){return t=(r?bi(n,t,r):t===u)?1:da(t),He(ba(n),t)},Nr.replace=function(){var n=arguments,t=ba(n[0]);return n.length<3?t:t.replace(n[1],n[2])},Nr.result=function(n,t,r){var e=-1,i=(t=bu(t,n)).length;for(i||(i=1,n=u);++ev)return[];var r=g,e=br(n,g);t=li(t),n-=g;for(var u=Vt(e,t);++r=o)return n;var c=r-pr(e);if(c<1)return e;var f=a?ju(a,0,c).join(""):n.slice(0,c);if(i===u)return f+e;if(a&&(c+=f.length-c),ca(i)){if(n.slice(c).search(i)){var l,s=f;for(i.global||(i=Bn(i.source,ba(gn.exec(i))+"g")),i.lastIndex=0;l=i.exec(s);)var h=l.index;f=f.slice(0,h===u?c:h)}}else if(n.indexOf(su(i),c)!=c){var p=f.lastIndexOf(i);p>-1&&(f=f.slice(0,p))}return f+e},Nr.unescape=function(n){return(n=ba(n))&&V.test(n)?n.replace(Y,gr):n},Nr.uniqueId=function(n){var t=++Un;return ba(n)+t},Nr.upperCase=Xa,Nr.upperFirst=Qa,Nr.each=bo,Nr.eachRight=xo,Nr.first=Yi,lc(Nr,(Ac={},xe(Nr,(function(n,t){Wn.call(Nr.prototype,t)||(Ac[t]=n)})),Ac),{chain:!1}),Nr.VERSION="4.17.21",Ot(["bind","bindKey","curry","curryRight","partial","partialRight"],(function(n){Nr[n].placeholder=Nr})),Ot(["drop","take"],(function(n,t){Jr.prototype[n]=function(r){r=r===u?1:wr(da(r),0);var e=this.__filtered__&&!t?new Jr(this):this.clone();return e.__filtered__?e.__takeCount__=br(r,e.__takeCount__):e.__views__.push({size:br(r,g),type:n+(e.__dir__<0?"Right":"")}),e},Jr.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()}})),Ot(["filter","map","takeWhile"],(function(n,t){var r=t+1,e=1==r||3==r;Jr.prototype[n]=function(n){var t=this.clone();return t.__iteratees__.push({iteratee:li(n,3),type:r}),t.__filtered__=t.__filtered__||e,t}})),Ot(["head","last"],(function(n,t){var r="take"+(t?"Right":"");Jr.prototype[n]=function(){return this[r](1).value()[0]}})),Ot(["initial","tail"],(function(n,t){var r="drop"+(t?"":"Right");Jr.prototype[n]=function(){return this.__filtered__?new Jr(this):this[r](1)}})),Jr.prototype.compact=function(){return this.filter(oc)},Jr.prototype.find=function(n){return this.filter(n).head()},Jr.prototype.findLast=function(n){return this.reverse().find(n)},Jr.prototype.invokeMap=Xe((function(n,t){return"function"==typeof n?new Jr(this):this.map((function(r){return ze(r,n,t)}))})),Jr.prototype.reject=function(n){return this.filter(Uo(li(n)))},Jr.prototype.slice=function(n,t){n=da(n);var r=this;return r.__filtered__&&(n>0||t<0)?new Jr(r):(n<0?r=r.takeRight(-n):n&&(r=r.drop(n)),t!==u&&(r=(t=da(t))<0?r.dropRight(-t):r.take(t-n)),r)},Jr.prototype.takeRightWhile=function(n){return this.reverse().takeWhile(n).reverse()},Jr.prototype.toArray=function(){return this.take(g)},xe(Jr.prototype,(function(n,t){var r=/^(?:filter|find|map|reject)|While$/.test(t),e=/^(?:head|last)$/.test(t),i=Nr[e?"take"+("last"==t?"Right":""):t],o=e||/^find/.test(t);i&&(Nr.prototype[t]=function(){var t=this.__wrapped__,a=e?[1]:arguments,c=t instanceof Jr,f=a[0],l=c||Ko(t),s=function(n){var t=i.apply(Nr,Lt([n],a));return e&&h?t[0]:t};l&&r&&"function"==typeof f&&1!=f.length&&(c=l=!1);var h=this.__chain__,p=!!this.__actions__.length,v=o&&!h,_=c&&!p;if(!o&&l){t=_?t:new Jr(this);var g=n.apply(t,a);return g.__actions__.push({func:_o,args:[s],thisArg:u}),new Zr(g,h)}return v&&_?n.apply(this,a):(g=this.thru(s),v?e?g.value()[0]:g.value():g)})})),Ot(["pop","push","shift","sort","splice","unshift"],(function(n){var t=zn[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=/^(?:pop|shift)$/.test(n);Nr.prototype[n]=function(){var n=arguments;if(e&&!this.__chain__){var u=this.value();return t.apply(Ko(u)?u:[],n)}return this[r]((function(r){return t.apply(Ko(r)?r:[],n)}))}})),xe(Jr.prototype,(function(n,t){var r=Nr[t];if(r){var e=r.name+"";Wn.call(Cr,e)||(Cr[e]=[]),Cr[e].push({name:t,func:r})}})),Cr[Nu(u,2).name]=[{name:"wrapper",func:u}],Jr.prototype.clone=function(){var n=new Jr(this.__wrapped__);return n.__actions__=Ru(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=Ru(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=Ru(this.__views__),n},Jr.prototype.reverse=function(){if(this.__filtered__){var n=new Jr(this);n.__dir__=-1,n.__filtered__=!0}else(n=this.clone()).__dir__*=-1;return n},Jr.prototype.value=function(){var n=this.__wrapped__.value(),t=this.__dir__,r=Ko(n),e=t<0,u=r?n.length:0,i=function(n,t,r){var e=-1,u=r.length;for(;++e=this.__values__.length;return{done:n,value:n?u:this.__values__[this.__index__++]}},Nr.prototype.plant=function(n){for(var t,r=this;r instanceof qr;){var e=Mi(r);e.__index__=0,e.__values__=u,t?i.__wrapped__=e:t=e;var i=e;r=r.__wrapped__}return i.__wrapped__=n,t},Nr.prototype.reverse=function(){var n=this.__wrapped__;if(n instanceof Jr){var t=n;return this.__actions__.length&&(t=new Jr(this)),(t=t.reverse()).__actions__.push({func:_o,args:[ro],thisArg:u}),new Zr(t,this.__chain__)}return this.thru(ro)},Nr.prototype.toJSON=Nr.prototype.valueOf=Nr.prototype.value=function(){return gu(this.__wrapped__,this.__actions__)},Nr.prototype.first=Nr.prototype.head,tt&&(Nr.prototype[tt]=function(){return this}),Nr}();vt._=dr,(e=function(){return dr}.call(t,r,t,n))===u||(n.exports=e)}.call(this)},625:(n,t,r)=>{"use strict";r.d(t,{Fc:()=>l,NO:()=>c,O:()=>s,YD:()=>o,g5:()=>h,gB:()=>i,gf:()=>a});var e=r(486),u=r.n(e);function i(n,t=1){if(Math.abs(n)<1024)return n+" B";const r=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"];let e=-1;const u=10**t;do{n/=1024,++e}while(Math.round(Math.abs(n)*u)/u>=1024&&e1?"s":"";let i=`${e}
${t} total
${`${n.n_allocations} allocation${u}`}`;return!1===r&&(i=i.concat(`
Thread ID: ${n.thread_id}`)),i}function c(n,t){return function(n,t){let r=u().cloneDeep(n.children);const e=u().filter(r,(function n(r){return r.children&&r.children.length>0&&(r.children=u().filter(r.children,n)),t(r)}));return u().defaults({children:e},n)}(n,(n=>n.thread_id===t))}function f(n,t){function r(n){let e=[];if(t(n)){e=[];for(const t of n.children)e.push(...r(t));let t=u().clone(n);t.children=e,e=[t]}else for(const t of n.children)e.push(...r(t));return e}let e=[];for(let t of n.children)e.push(...r(t));return u().defaults({children:e},n)}function l(n){return f(n,(n=>n.interesting))}function s(n){return f(n,(n=>!n.import_system))}function h(n){return u().reduce(n,((n,t)=>(n.n_allocations+=t.n_allocations,n.value+=t.value,n)),{n_allocations:0,value:0})}},501:(n,t,r)=>{"use strict";r.d(t,{Cd:()=>f,Ji:()=>g,N4:()=>x,Xx:()=>v,YX:()=>l,Z1:()=>b,bf:()=>d,cW:()=>A,ib:()=>m,sO:()=>w});var e=r(625);const u="filter_uninteresting",i="filter_import_system",o="filter_thread";var a=null;let c=new class{constructor(){this.filters={}}registerFilter(n,t){this.filters[n]=t}unRegisterFilter(n){delete this.filters[n]}drawChart(n){let t=n;_.forOwn(this.filters,(n=>{t=n(t)})),function(n){a&&(a.destroy(),d3.selectAll(".d3-flame-graph-tip").remove());a=flamegraph().width(y()).transitionDuration(250).transitionEase(d3.easeCubic).inverted(!0).cellHeight(20).minFrameSize(2).setColorMapper(j).onClick(p).tooltip(d3.tip().attr("class","d3-flame-graph-tip").html((n=>{const t=(0,e.gB)(n.data.value);return(0,e.gf)(n.data,t,merge_threads)})).direction((n=>{const t=(n.x1+n.x0)/2;return.25.25?"w":"n"}))),d3.select("#chart").datum(n).call(a),a.width(y())}(t),a.merge([])}};function f(){return a}function l(){return c}function s(){return location.hash?parseInt(location.hash.substring(1),10):0}function h(){document.getElementById("resetZoomButton").disabled=0==s()}function p(n){n.id!=s()&&(history.pushState({id:n.id},n.data.name,`#${n.id}`),h())}function v(){const n=s(),t=a.findById(n);t&&(a.zoomTo(t),h())}function g(){a.inverted(!a.inverted()),a.resetZoom()}function d(){a.resetZoom()}function y(){return document.getElementById("chart").clientWidth}function m(){c.drawChart(data),location.hash&&v()}function w(){const n=this.dataset.thread;"-0x1"===n?c.unRegisterFilter(o):c.registerFilter(o,(t=>{let r=(0,e.NO)(t,n);const u=(0,e.g5)(r.children);return _.defaults(u,r),r.n_allocations=u.n_allocations,r.value=u.value,r})),c.drawChart(data)}function b(){void 0===this.hideUninterestingFrames&&(this.hideUninterestingFrames=!0),!0===this.hideUninterestingFrames?(this.hideUninterestingFrames=!0,c.registerFilter(u,(n=>(0,e.Fc)(n)))):c.unRegisterFilter(u),this.hideUninterestingFrames=!this.hideUninterestingFrames,c.drawChart(data)}function x(){void 0===this.hideImportSystemFrames&&(this.hideImportSystemFrames=!0),!0===this.hideImportSystemFrames?(this.hideImportSystemFrames=!0,c.registerFilter(i,(n=>(0,e.O)(n)))):c.unRegisterFilter(i),this.hideImportSystemFrames=!this.hideImportSystemFrames,c.drawChart(data)}function j(n,t){return n.highlight?"orange":n.data.name&&n.data.location?(e=n.data.location[1],"py"==(r=void 0===e?e:e.substring(e.lastIndexOf(".")+1,e.length)||e)?d3.schemePastel1[2]:"c"==r||"cpp"==r||"h"==r?d3.schemePastel1[5]:d3.schemePastel1[8]):"#EEE";var r,e}function A(n,t){if(!0===t)return;const r=n.unique_threads;if(!r||r.length<=1)return;document.getElementById("threadsDropdown").removeAttribute("hidden");const e=document.getElementById("threadsDropdownList");for(const n of r){let t=document.createElement("a");t.className="dropdown-item",t.dataset.thread=n,t.text=n,t.onclick=w,e.appendChild(t)}}}},t={};function r(e){var u=t[e];if(void 0!==u)return u.exports;var i=t[e]={id:e,loaded:!1,exports:{}};return n[e].call(i.exports,i,i.exports,r),i.loaded=!0,i.exports}r.n=n=>{var t=n&&n.__esModule?()=>n.default:()=>n;return r.d(t,{a:t}),t},r.d=(n,t)=>{for(var e in t)r.o(t,e)&&!r.o(n,e)&&Object.defineProperty(n,e,{enumerable:!0,get:t[e]})},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(n){if("object"==typeof window)return window}}(),r.o=(n,t)=>Object.prototype.hasOwnProperty.call(n,t),r.nmd=n=>(n.paths=[],n.children||(n.children=[]),n),(()=>{"use strict";var n=r(625),t=r(501),e=null,u=null,i=function(){let n=new Array(packed_data.nodes.children.length);console.log("finding parent index for each node");for(const[t,r]of packed_data.nodes.children.entries())r.forEach((r=>n[r]=t));return console.assert(void 0===n[0],"root node has a parent"),n}();function o(n){console.log("refreshing flame graph!");let r=function(n){console.log("getRangeData");let t={};if(n.hasOwnProperty("xaxis.range[0]"))t={string1:n["xaxis.range[0]"],string2:n["xaxis.range[1]"]};else if(n.hasOwnProperty("xaxis.range"))t={string1:n["xaxis.range"][0],string2:n["xaxis.range"][1]};else{if(null===e)return;{let n=e.layout.xaxis.range;t={string1:n[0],string2:n[1]}}}return t}(n);if(console.log("range data: "+JSON.stringify(r)),null!=u&&JSON.stringify(r)===JSON.stringify(u))return;console.log("showing loading animation"),console.log("showLoadingAnimation"),document.getElementById("loading").style.display="block",document.getElementById("overlay").style.display="block",u=r,console.log("finding range of relevant snapshot");let o=0,a=memory_records.length;if(r){const n=new Date(r.string1).getTime(),t=memory_records.findIndex((t=>t[0]>=n));-1!=t&&(o=t);const e=new Date(r.string2).getTime(),u=memory_records.findIndex((n=>n[0]>e));-1!=u&&(a=u)}console.log("start index is "+o),console.log("end index is "+a),console.log("first possible index is 0"),console.log("last possible index is "+memory_records.length),console.log("constructing tree"),data=function(n,t,r){const{strings:e,nodes:u,unique_threads:o}=n;console.log("constructing nodes");const a=u.name.map(((n,t)=>({name:e[u.name[t]],location:[e[u.function[t]],e[u.filename[t]],u.lineno[t]],value:0,children:u.children[t],n_allocations:0,thread_id:e[u.thread_id[t]],interesting:0!==u.interesting[t],import_system:0!==u.import_system[t]})));console.log("mapping child indices to child nodes");for(const[n,t]of a.entries())t.children=t.children.map((n=>a[n]));return console.log("finding leaked allocations"),n.intervals.forEach((n=>{let[e,u,o,c,f]=n;if(e>=t&&e<=r&&(null===u||u>r))for(;void 0!==o;)a[o].n_allocations+=c,a[o].value+=f,o=i[o]})),console.log("total leaked allocations in range: "+a[0].n_allocations),console.log("total leaked bytes in range: "+a[0].value),a.forEach((n=>{n.children=n.children.filter((n=>n.n_allocations>0))})),a[0]}(packed_data,o,a),console.log("drawing chart"),(0,t.YX)().drawChart(data),console.log("hiding loading animation"),console.log("hideLoadingAnimation"),document.getElementById("loading").style.display="none",document.getElementById("overlay").style.display="none"}var a=null;function c(n){console.log("refreshFlamegraphDebounced"),a&&clearTimeout(a),a=setTimeout((function(){o(n)}),500)}document.addEventListener("DOMContentLoaded",(function(){console.log("main");const r=packed_data.unique_threads.map((n=>packed_data.strings[n]));(0,t.cW)({unique_threads:r},merge_threads),function(n){console.log("init memory graph");const t=n.map((n=>new Date(n[0])));var r=[{x:t,y:n.map((n=>n[1])),mode:"lines",name:"Resident size"},{x:t,y:n.map((n=>n[2])),mode:"lines",name:"Heap size"}];Plotly.newPlot("plot",r,{xaxis:{title:{text:"Time"},rangeslider:{visible:!0}},yaxis:{title:{text:"Memory Size"},tickformat:".4~s",exponentformat:"B",ticksuffix:"B"}},{responsive:!0,displayModeBar:!1}).then((n=>{console.assert(null===e),e=n}))}(memory_records),o({}),location.hash&&(0,t.Xx)(),document.getElementById("invertButton").onclick=t.Ji,document.getElementById("resetZoomButton").onclick=t.bf,document.getElementById("resetThreadFilterItem").onclick=t.sO,document.getElementById("hideUninteresting").onclick=t.Z1.bind(this),document.getElementById("hideImportSystem").onclick=t.N4.bind(this),t.Z1.bind(this)(),document.onkeyup=n=>{"Escape"==n.code&&(0,t.bf)()},document.getElementById("searchTerm").addEventListener("input",(()=>{const n=document.getElementById("searchTerm");(0,t.Cd)().search(n.value)})),window.addEventListener("popstate",t.Xx),window.addEventListener("resize",(0,n.YD)(t.ib)),$('[data-toggle-second="tooltip"]').tooltip(),$('[data-toggle="tooltip"]').tooltip(),console.log("setup reload handler"),document.getElementById("plot").on("plotly_relayout",c),[].slice.call(document.querySelectorAll(".toast")).map((function(n){return new bootstrap.Toast(n,{delay:1e4})})).forEach((n=>n.show()))}))})()})(); \ No newline at end of file diff --git a/src/memray/reporters/templates/flamegraph.html b/src/memray/reporters/templates/flamegraph.html index 7b3f290807..b90014b36a 100644 --- a/src/memray/reporters/templates/flamegraph.html +++ b/src/memray/reporters/templates/flamegraph.html @@ -13,7 +13,7 @@
+ title="Hide CPython eval frames and Memray-related frames" checked>
@@ -44,6 +44,8 @@ The flame graph displays a snapshot of memory used across stack frames at the time when the memory usage was at its peak.

{% endif %} +{% block slider_help %} +{% endblock %}

The vertical ordering of the stack frames corresponds to the order of function calls, from parent to children. The horizontal ordering does not represent the passage of time in the application: they simply represent child frames in arbitrary order. @@ -72,7 +74,11 @@ + +{% block flamegraph_script %} {% endblock %} + +{% endblock %} diff --git a/src/memray/reporters/templates/temporal_flamegraph.html b/src/memray/reporters/templates/temporal_flamegraph.html new file mode 100644 index 0000000000..c0a05501c9 --- /dev/null +++ b/src/memray/reporters/templates/temporal_flamegraph.html @@ -0,0 +1,43 @@ +{% extends "flamegraph.html" %} + +{% block content %} +

+
+ How to use this plot + +
+
+ You can move the plot slider to select different ranges for the flame + graph. The flame graph shows the allocations that are created in the + selected range that are not deallocated before the end of the range. +
+
+ +
+ + +
+
+
+{% endblock %} + +{% block slider_help %} +

+ Initially the report shows allocations made at any time and not freed before + tracking was deactivated. By using the two sliders on the bottom line chart, + you can select a different time range for analysis instead. The flame graph + will be updated to reflect allocations made within your chosen time window + and not freed within it. +

+{% endblock %} + +{% block flamegraph_script %} + +{% endblock %} diff --git a/tests/unit/test_allocation_lifetime_aggregator.py b/tests/unit/test_allocation_lifetime_aggregator.py new file mode 100644 index 0000000000..6635ec9a3b --- /dev/null +++ b/tests/unit/test_allocation_lifetime_aggregator.py @@ -0,0 +1,517 @@ +from dataclasses import dataclass + +from memray import AllocatorType +from memray._memray import AllocationLifetimeAggregatorTestHarness +from memray._memray import Interval + +CALLOC = AllocatorType.CALLOC +FREE = AllocatorType.FREE +MMAP = AllocatorType.MMAP +MUNMAP = AllocatorType.MUNMAP + + +@dataclass(frozen=True) +class Location: + tid: int + native_frame_id: int + frame_index: int + native_segment_generation: int + + +def test_no_allocations_at_start(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + + # WHEN + # THEN + assert [] == list(tester.get_allocations()) + + +def test_allocation_not_reported_when_freed_within_same_snapshot(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=4096, size=1234) + tester.add_allocation(**loc.__dict__, allocator=FREE, address=4096, size=0) + + # THEN + assert [] == list(tester.get_allocations()) + + +def test_allocation_reported_when_freed_within_different_snapshot(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=4096, size=1234) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=FREE, address=4096, size=0) + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == CALLOC + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, 1, 1, 1234)] + + +def test_allocation_reported_when_leaked(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=4096, size=1234) + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == CALLOC + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, None, 1, 1234)] + + +def test_multiple_snapshots_between_allocation_and_deallocation(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.capture_snapshot() + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=4096, size=1234) + tester.capture_snapshot() + tester.capture_snapshot() + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=FREE, address=4096, size=0) + tester.capture_snapshot() + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == CALLOC + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(2, 5, 1, 1234)] + + +def test_allocations_from_same_location_and_snapshot_freed_in_different_snapshots(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=4096, size=1234) + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=8192, size=4321) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=FREE, address=8192, size=0) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=FREE, address=4096, size=0) + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == CALLOC + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(1, 2, 1, 4321), Interval(1, 3, 1, 1234)] + + +def test_allocations_from_same_location_and_different_snapshots_freed_in_one_snapshot(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=4096, size=1234) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=8192, size=4321) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=FREE, address=8192, size=0) + tester.add_allocation(**loc.__dict__, allocator=FREE, address=4096, size=0) + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == CALLOC + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, 2, 1, 1234), Interval(1, 2, 1, 4321)] + + +def test_two_leaked_allocations_from_one_location(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=4096, size=1234) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=8192, size=4321) + tester.capture_snapshot() + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == CALLOC + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, None, 1, 1234), Interval(1, None, 1, 4321)] + + +def test_allocations_made_and_freed_together_are_aggregated(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=4096, size=1234) + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=8192, size=4321) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=FREE, address=8192, size=0) + tester.add_allocation(**loc.__dict__, allocator=FREE, address=4096, size=0) + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == CALLOC + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, 1, 2, 1234 + 4321)] + + +def test_leaked_allocations_within_one_snapshot_are_aggregated(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=4096, size=1234) + tester.add_allocation(**loc.__dict__, allocator=CALLOC, address=8192, size=4321) + tester.capture_snapshot() + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == CALLOC + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, None, 2, 1234 + 4321)] + + +def test_freed_allocations_from_different_locations_are_not_aggregated(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc1 = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + loc2 = Location( + tid=1, + native_frame_id=7, + frame_index=8, + native_segment_generation=9, + ) + free = Location( + tid=0, + native_frame_id=0, + frame_index=0, + native_segment_generation=0, + ) + + # WHEN + tester.add_allocation(**loc1.__dict__, allocator=CALLOC, address=4096, size=1234) + tester.add_allocation(**loc2.__dict__, allocator=CALLOC, address=8192, size=4321) + tester.capture_snapshot() + tester.add_allocation(**free.__dict__, allocator=FREE, address=8192, size=0) + tester.add_allocation(**free.__dict__, allocator=FREE, address=4096, size=0) + + # THEN + alloc1, alloc2 = tester.get_allocations() + assert alloc1.allocator == CALLOC + assert alloc1.native_stack_id == 4 + assert alloc1.stack_id == 5 + assert alloc1.native_segment_generation == 6 + assert alloc1.tid == 1 + assert alloc1.intervals == [Interval(0, 1, 1, 1234)] + + assert alloc2.allocator == CALLOC + assert alloc2.native_stack_id == 7 + assert alloc2.stack_id == 8 + assert alloc2.native_segment_generation == 9 + assert alloc2.tid == 1 + assert alloc2.intervals == [Interval(0, 1, 1, 4321)] + + +def test_leaked_allocations_from_different_locations_are_not_aggregated(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc1 = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + loc2 = Location( + tid=1, + native_frame_id=7, + frame_index=8, + native_segment_generation=9, + ) + + # WHEN + tester.add_allocation(**loc1.__dict__, allocator=CALLOC, address=4096, size=1234) + tester.add_allocation(**loc2.__dict__, allocator=CALLOC, address=8192, size=4321) + + # THEN + alloc1, alloc2 = tester.get_allocations() + assert alloc1.allocator == CALLOC + assert alloc1.native_stack_id == 4 + assert alloc1.stack_id == 5 + assert alloc1.native_segment_generation == 6 + assert alloc1.tid == 1 + assert alloc1.intervals == [Interval(0, None, 1, 1234)] + + assert alloc2.allocator == CALLOC + assert alloc2.native_stack_id == 7 + assert alloc2.stack_id == 8 + assert alloc2.native_segment_generation == 9 + assert alloc2.tid == 1 + assert alloc2.intervals == [Interval(0, None, 1, 4321)] + + +def test_range_freed_in_same_snapshot(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=MMAP, address=4096, size=1234) + tester.add_allocation(**loc.__dict__, allocator=MUNMAP, address=4096, size=1234) + + # THEN + assert [] == list(tester.get_allocations()) + + +def test_range_freed_in_different_snapshot(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=MMAP, address=4096, size=1234) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=MUNMAP, address=4096, size=1234) + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == MMAP + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, 1, 1, 1234)] + + +def test_range_leaked(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=MMAP, address=4096, size=1234) + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == MMAP + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, None, 1, 1234)] + + +def test_shrunk_then_leaked_range(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=MMAP, address=4096, size=1234) + tester.add_allocation(**loc.__dict__, allocator=MUNMAP, address=4096, size=1000) + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == MMAP + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, None, 1, 234)] + + +def test_shrunk_then_freed_range(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=MMAP, address=4096, size=1234) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=MUNMAP, address=4096, size=1000) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=MUNMAP, address=4096, size=1234) + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == MMAP + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, 1, 0, 1000), Interval(0, 2, 1, 234)] + + +def test_split_then_leaked_range(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=MMAP, address=4096, size=1234) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=MUNMAP, address=5000, size=100) + + # THEN + (alloc,) = tester.get_allocations() + + assert alloc.allocator == MMAP + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [Interval(0, 1, 0, 100), Interval(0, None, 1, 1234 - 100)] + + +def test_split_then_freed_range(): + # GIVEN + tester = AllocationLifetimeAggregatorTestHarness() + loc = Location( + tid=1, + native_frame_id=4, + frame_index=5, + native_segment_generation=6, + ) + + # WHEN + tester.add_allocation(**loc.__dict__, allocator=MMAP, address=4096, size=1234) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=MUNMAP, address=5000, size=100) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=MUNMAP, address=4096, size=904) + tester.capture_snapshot() + tester.add_allocation(**loc.__dict__, allocator=MUNMAP, address=5100, size=230) + + # THEN + (alloc,) = tester.get_allocations() + assert alloc.allocator == MMAP + assert alloc.native_stack_id == 4 + assert alloc.stack_id == 5 + assert alloc.native_segment_generation == 6 + assert alloc.tid == 1 + assert alloc.intervals == [ + Interval(0, 1, 0, 100), + Interval(0, 2, 0, 904), + Interval(0, 3, 1, 230), + ] diff --git a/webpack.config.js b/webpack.config.js index f3ee714d8f..5f76e3801f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,6 +6,7 @@ module.exports = { entry: { flamegraph_common: "./src/memray/reporters/assets/flamegraph_common.js", flamegraph: "./src/memray/reporters/assets/flamegraph.js", + temporal_flamegraph: "./src/memray/reporters/assets/temporal_flamegraph.js", table: "./src/memray/reporters/assets/table.js", }, output: {