heapspy 0.1.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
Binary file
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: heapspy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
5
|
+
Classifier: Environment :: Console
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
8
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
11
|
+
Classifier: Programming Language :: Rust
|
|
12
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
13
|
+
Classifier: Topic :: System :: Monitoring
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Summary: Find what keeps memory alive in a CPython process, from the outside
|
|
16
|
+
Keywords: memory,leak,heap,profiler,cpython,debugging
|
|
17
|
+
License-Expression: MIT
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
20
|
+
Project-URL: Documentation, https://github.com/Heknon/heapspy#readme
|
|
21
|
+
Project-URL: Homepage, https://github.com/Heknon/heapspy
|
|
22
|
+
Project-URL: Issues, https://github.com/Heknon/heapspy/issues
|
|
23
|
+
Project-URL: Repository, https://github.com/Heknon/heapspy
|
|
24
|
+
|
|
25
|
+
# heapspy
|
|
26
|
+
|
|
27
|
+
Read the object graph of a running Python process from the outside, and find out what is keeping memory alive.
|
|
28
|
+
|
|
29
|
+
py-spy reads a process's stacks without touching it. heapspy does the same for the heap: it takes a consistent memory image of a CPython process, rebuilds the object graph from it, and computes dominators and retained sizes. Nothing is injected or imported into the target.
|
|
30
|
+
|
|
31
|
+
> **Status: early development.** `heapspy dump` (`--pid`, `--core`, `--minidump`) walks CPython 3.12, 3.13 and 3.14 heaps (default GIL builds, 64-bit) into snapshots, and `analyze`, `query`, `diff` and `flamegraph` work on them. `heapspy record` samples allocation stacks on Linux. Free-threaded and debug builds are refused. The walker's per-version layouts are in [docs/cpython-internals.md](docs/cpython-internals.md). An end-to-end suite ([tests/e2e](tests/e2e/README.md)) checks on Linux that known leaks in real 3.12 to 3.14 processes are found, by `--pid` and from `gcore` cores. The MCP server is in [python/heapspy-mcp](python/heapspy-mcp/README.md). Nothing is published on PyPI yet, and the Windows code paths have only been compile-checked, not run on Windows.
|
|
32
|
+
|
|
33
|
+
## What it answers, and what it doesn't
|
|
34
|
+
|
|
35
|
+
heapspy answers **"what is retained, and by whom?"** For a leak, that is usually the question that matters: the reference holding the memory is the thing you delete to fix it, and that reference is sitting in the heap right now. Eclipse MAT finds JVM leaks the same way, from heap dumps with no allocation stacks.
|
|
36
|
+
|
|
37
|
+
heapspy cannot answer **"where was this allocated?"** for memory that already exists. That history is only recorded if a tracer was running at allocation time. `heapspy record` (M7) samples allocations with their Python stacks from outside the process, for a bounded window, and `analyze --allocations` links them to a snapshot. It uses eBPF on Linux (ptrace for arenas as a fallback), and ETW plus a debugger on Windows, and only sees allocations made while it runs. See [docs/allocation-sampling.md](docs/allocation-sampling.md). For full allocation history from process start, use tracemalloc or memray. For CPU, use py-spy alongside heapspy.
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
target Python process untouched, no agent
|
|
43
|
+
|
|
|
44
|
+
| heapspy dump privileged, read-only
|
|
45
|
+
v Linux: gcore / process_vm_readv
|
|
46
|
+
snapshot.hpy Windows: full minidump / ReadProcessMemory
|
|
47
|
+
|
|
|
48
|
+
| heapspy analyze / query / diff unprivileged, file only
|
|
49
|
+
v
|
|
50
|
+
dominator tree, retained sizes, type histogram, retaining paths, diffs
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`dump` is the only step that touches the target and the only step that needs privileges. It locates `PyRuntime` in libpython (or `python3xx.dll`), walks the GC generation lists to find every tracked container, follows each object's references the way its `tp_traverse` would, and writes a compact binary snapshot.
|
|
54
|
+
|
|
55
|
+
Everything else reads the snapshot file. You can re-analyze without re-dumping, attach a snapshot to a bug report, or diff two captures taken an hour apart.
|
|
56
|
+
|
|
57
|
+
## Planned usage
|
|
58
|
+
|
|
59
|
+
### Capture a snapshot
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
# Dump a live process (brief pause while the image is taken)
|
|
63
|
+
heapspy dump --pid 1234 -o app.hpy
|
|
64
|
+
|
|
65
|
+
# Or convert an image you already have
|
|
66
|
+
heapspy dump --core core.1234 -o app.hpy # Linux ELF core
|
|
67
|
+
heapspy dump --minidump app.dmp -o app.hpy # Windows full minidump
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
By default the snapshot records types, sizes, lengths, and references, but **not** string or bytes contents. Contents often hold tokens, credentials, and user data. To include them, opt in explicitly; values are size-capped and redacted:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
heapspy dump --pid 1234 --capture-contents --max-str 128 -o app.hpy
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Find what retains memory
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
heapspy analyze app.hpy
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Example output (illustrative):
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
Top retainers by retained size
|
|
86
|
+
|
|
87
|
+
retained shallow type root path
|
|
88
|
+
412.6 MiB 36.9 MiB builtins.dict sys.modules['app.cache'].__dict__['CACHE']
|
|
89
|
+
18.2 MiB 1.1 KiB app.events.Registry sys.modules['app.events'].__dict__['REGISTRY']
|
|
90
|
+
4.0 MiB 12.0 KiB builtins.list frame worker:run -> local 'pending'
|
|
91
|
+
|
|
92
|
+
Python graph accounts for 448 MiB of 1.9 GiB RSS. The rest is outside the
|
|
93
|
+
object graph (native allocations, C extensions, untracked objects).
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Drill into a type or object
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
# All instances of a type, with sample holders
|
|
100
|
+
heapspy query app.hpy --type app.session.Session
|
|
101
|
+
|
|
102
|
+
# The labelled chain of references keeping one object alive
|
|
103
|
+
heapspy query app.hpy --retaining-path 0x7f3a2c41b0d0
|
|
104
|
+
|
|
105
|
+
# Where a type, function or module is defined, as recorded in the snapshot
|
|
106
|
+
heapspy query app.hpy --locate app.session.Session
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Reports include source locations where the snapshot has them: functions
|
|
110
|
+
and code objects at `co_filename:co_firstlineno`, classes near the first
|
|
111
|
+
line of their methods (`app/session.py:~14`). See
|
|
112
|
+
[docs/json-schema.md](docs/json-schema.md#source-locations).
|
|
113
|
+
|
|
114
|
+
A retaining path reads root-first, one labelled hop per line:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
sys.modules['app.cache']
|
|
118
|
+
.__dict__['CACHE'] builtins.dict
|
|
119
|
+
['user:8812'] app.session.Session
|
|
120
|
+
.payload builtins.bytes (1.0 MiB)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Compare two captures
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
heapspy dump --pid 1234 -o before.hpy
|
|
127
|
+
# ... let the process run ...
|
|
128
|
+
heapspy dump --pid 1234 -o after.hpy
|
|
129
|
+
|
|
130
|
+
heapspy diff before.hpy after.hpy
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`diff` reports growth by type and by retaining site. Object addresses change between dumps, so matching individual objects across snapshots is approximate. The output says so and reports aggregates.
|
|
134
|
+
|
|
135
|
+
### Retention flame graph
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
heapspy flamegraph app.hpy -o app.svg
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+

|
|
142
|
+
|
|
143
|
+
A retention flame graph shows the dominator tree. A snapshot has no call stacks, so a frame is not a function. Each frame is one hop of a retaining path, and its width is the bytes it retains: the objects in that frame and everything only reachable through them. The bottom row holds the roots (`sys.modules['__main__']`, frame locals, thread state). Each row above it names the reference that holds the next object. To find a leak, look for the widest frame you did not expect and read down to its root. In the image above, `.cell_contents bytearray` under `BUS.listeners` holds 70% of the heap: closures registered as listeners keep their buffers alive. `['SESSIONS'] dict` holds most of the rest.
|
|
144
|
+
|
|
145
|
+
A CPU flame graph is different. Its widths count samples of time, and its frames are call stacks. An allocation flame graph (memray, tracemalloc, or `heapspy flamegraph --allocations` below) counts bytes allocated by each call stack, including memory freed long ago. A retention flame graph counts only memory that is alive in the snapshot, grouped by what keeps it alive, not by where it was allocated.
|
|
146
|
+
|
|
147
|
+
Frame format:
|
|
148
|
+
|
|
149
|
+
| object | frame text | example |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| root object | its root label | `sys.modules['__main__']` |
|
|
152
|
+
| other object | label of the edge into it, a space, its type (`builtins.` dropped) | `['SESSIONS'] dict`, `.listeners list`, `.__closure__[0] cell`, `.cell_contents bytearray` |
|
|
153
|
+
| reached through objects its dominator owns | `...` before the edge label | `...[<index>] bytes` |
|
|
154
|
+
| dominated only by the super-root, not a root itself (shared by several roots) | a top-level frame with `...` | `...['SHARED'] list` |
|
|
155
|
+
| folded objects | `[other]` | |
|
|
156
|
+
|
|
157
|
+
The edge is the one breadth-first search entered the object through, as for `diff` sites ([docs/json-schema.md](docs/json-schema.md)). A `;` inside a label is written as `:`.
|
|
158
|
+
|
|
159
|
+
Merge rules: siblings with the same frame text become one frame. List and tuple indices are written `[<index>]`, so all items of a list merge. Without `--capture-contents`, dict keys are placeholders such as `[<str len=9>]`, so all values whose keys have the same type and length merge. Merged frames merge their children too, so a cache of 100,000 sessions is one wide `['CACHE'] dict` frame with one `[<str len=N>] Session` frame per key length above it. A frame's self bytes are the shallow sizes of the objects merged into it, and its width (self plus everything above it) equals their total retained size.
|
|
160
|
+
|
|
161
|
+
Cutoffs keep the output small on large heaps. Objects more than `--max-depth` levels (default 32) below a root are counted in an `[other]` frame at that depth. A frame narrower than `--min-pct` percent of the reachable bytes (default 0.01) or `--min-bytes N` is folded, with everything above it, into an `[other]` frame beside it. Folding never changes the width of the frames that remain.
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
# Folded stacks (`frame;frame;frame bytes`) for other tools, on stdout
|
|
165
|
+
heapspy flamegraph app.hpy --folded --min-bytes 0 > app.folded
|
|
166
|
+
|
|
167
|
+
# Differential: red frames grew, blue frames shrank, width is AFTER
|
|
168
|
+
heapspy flamegraph --diff before.hpy after.hpy -o diff.svg
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+

|
|
172
|
+
|
|
173
|
+
The differential graph matches frames between the two dumps by frame text, not by object identity, so the matching is approximate in the same way as `diff`. For example, sessions whose keys got one character longer show up as a new frame. `--diff ... --folded` writes `stack before after` lines, the input format of inferno's and FlameGraph's differential mode. The cutoff applies to the wider of the two sides, so a frame is folded in both snapshots or in neither.
|
|
174
|
+
|
|
175
|
+
### Record where memory is allocated (M7)
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
# Sample allocations with their Python stacks for 60 s, then take a snapshot
|
|
179
|
+
heapspy record --pid 1234 --duration 60s -o app.hpya --then-dump app.hpy
|
|
180
|
+
|
|
181
|
+
heapspy analyze app.hpy --allocations app.hpya
|
|
182
|
+
heapspy query app.hpy --type Session --allocations app.hpya
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`record` takes `--events arena,pool,large`, `--interval BYTES` (mean bytes between large-object samples, default 512K), `--max-frames N` (default 64) and `--mechanism auto|ebpf|ptrace|etw|debugger`. The `.hpya` file format is final for version 1.
|
|
186
|
+
|
|
187
|
+
How it records, without running code in the target:
|
|
188
|
+
- **Linux, eBPF** (default; root or `CAP_BPF` + `CAP_PERFMON`, kernel 5.8+): arenas from `mmap` and sampled large objects from allocator uprobes, each with the Python stack read in the kernel at the event.
|
|
189
|
+
- **Linux, ptrace** (fallback, or `--mechanism ptrace`): arenas only, with exact stacks, stopping at system calls.
|
|
190
|
+
- **Windows** (elevated): arenas from ETW (the default), with approximate frames, without stopping the target. `--mechanism debugger` gives exact frames but stops the target's threads for most of the window. Tested on `windows-latest` with CPython 3.12 to 3.14 ([test plan and results](docs/windows-record-test-plan.md)). If heapspy is killed while recording, a guardian process restores the target and stops heapspy's ETW session (`heapspy-<pid>`). A process-tree kill (`taskkill /T`) takes the guardian down too; see [docs/windows.md](docs/windows.md#when-heapspy-is-killed).
|
|
191
|
+
|
|
192
|
+
Pools are never recorded, because pool carving is inlined in every build checked. Large objects cost one uprobe trap per allocator call above 512 bytes (about 0.5 µs). On an extreme benchmark that slowed the target by 30-50%; arenas alone cost a few percent. Numbers are in [docs/allocation-sampling.md](docs/allocation-sampling.md#measurements). Ctrl-C ends a recording early with a valid profile flagged truncated.
|
|
193
|
+
|
|
194
|
+
`analyze --allocations` links snapshot objects to samples by address. Each object gets the latest sample whose range (an arena, a pool, or one large allocation) contains it. It then adds an "Allocated by" section: the stacks that allocated the most during the window, innermost frame first with `file:line`, with estimated bytes allocated and the bytes of live objects linked to each. Under each top retainer it shows the one or two stacks that allocated the memory it retains. `query --type --allocations` shows the same per instance. Read the numbers as an estimate of where growth came from:
|
|
195
|
+
|
|
196
|
+
- Only allocations made while recording are covered. Memory allocated before recording started, and blocks reused from freed memory, have no stack.
|
|
197
|
+
- Links through large-object samples are exact. Links through arena and pool samples are approximate: they name the most recent growth event that created the memory, not necessarily the call that allocated the object. The text output marks each stack `exact` or `approx`.
|
|
198
|
+
- If the profile is from another pid or CPython version, or recording started after the snapshot, heapspy warns and links nothing.
|
|
199
|
+
|
|
200
|
+
`diff --allocations` only checks the profile against AFTER for now; it does not add stacks to growth sites. The JSON fields are `allocations` and `allocated_at` ([docs/json-schema.md](docs/json-schema.md#allocations)), and the join is described in [docs/allocation-sampling.md](docs/allocation-sampling.md#joining-with-a-snapshot).
|
|
201
|
+
|
|
202
|
+
### Allocation flame graph
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
# Width is estimated bytes allocated while recording
|
|
206
|
+
heapspy flamegraph --allocations app.hpya -o alloc.svg
|
|
207
|
+
|
|
208
|
+
# Width is the bytes still alive in the snapshot, by allocating stack
|
|
209
|
+
heapspy flamegraph --allocations app.hpya --retained app.hpy -o alive.svg
|
|
210
|
+
|
|
211
|
+
heapspy flamegraph --allocations app.hpya --folded > alloc.folded
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This is a classic flame graph of the sampled stacks. Frames are Python functions written `function (file:line)`, the outermost frame is at the root, and width is bytes. Without `--retained`, the width is the estimated bytes allocated, which includes memory freed since. With `--retained SNAP`, the width is the bytes of the objects in the snapshot linked to each stack's samples, which shows where the memory still alive was allocated. The subtitle gives the recording length, the sample count, the mechanisms, and whether any frames were matched by time (approximate). `--min-bytes` and `--min-pct` fold narrow frames into `[other]` as for retention graphs. `--max-depth` does not apply.
|
|
215
|
+
|
|
216
|
+
### JSON output
|
|
217
|
+
|
|
218
|
+
Every command except `flamegraph` takes `--json`. The schema is versioned and kept stable, since tooling (including the MCP server below) builds on it. `dump --json` reports what was written; a failed command prints an error document with a stable code such as `not_python` or `permission_denied`. See [docs/json-schema.md](docs/json-schema.md).
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
heapspy analyze app.hpy --json | jq '.top_retainers[0]'
|
|
222
|
+
heapspy dump --pid 1234 -o app.hpy --json | jq '.objects, .pause_ms'
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Permissions
|
|
226
|
+
|
|
227
|
+
Dumping another process needs elevated rights. Analysis does not.
|
|
228
|
+
|
|
229
|
+
| OS | `dump` needs |
|
|
230
|
+
|---------|------------------------------------------------------------------------------|
|
|
231
|
+
| Linux | `CAP_SYS_PTRACE`, or a permissive `kernel.yama.ptrace_scope`. In containers: `SYS_PTRACE` and a shared PID namespace. |
|
|
232
|
+
| Windows | `SeDebugPrivilege` (run elevated) to read another user's process. |
|
|
233
|
+
|
|
234
|
+
Taking the image pauses the target. On a large heap, expect a pause of a few seconds and a multi-gigabyte image file.
|
|
235
|
+
|
|
236
|
+
## Supported targets (v1)
|
|
237
|
+
|
|
238
|
+
| | v1 | Later |
|
|
239
|
+
|---|---|---|
|
|
240
|
+
| OS | Linux, Windows | macOS |
|
|
241
|
+
| CPython | 3.12, 3.13, 3.14 | free-threaded builds |
|
|
242
|
+
| Acquisition | consistent dump (ELF core, full minidump) | live attach with a brief stop |
|
|
243
|
+
| Graph | GC-tracked containers and the leaf objects they reference | full pymalloc arena walk |
|
|
244
|
+
| Native memory | totals only, from memory maps | per-allocation detail |
|
|
245
|
+
|
|
246
|
+
Out of scope for v1: allocation call chains, per-object native or C-extension memory, leaks made only of untracked objects, reads with no pause, and runtimes other than CPython.
|
|
247
|
+
|
|
248
|
+
## Known blind spots
|
|
249
|
+
|
|
250
|
+
- **Untracked objects.** Bare ints, short strings, and raw buffers are not on the GC lists. heapspy sees them only when a tracked container references them. A leak made entirely of floating untracked objects will not show up in v1, and the output will say so.
|
|
251
|
+
- **Native memory.** Memory allocated by C extensions is invisible to the object graph. heapspy reports how much of RSS falls outside the graph rather than guessing.
|
|
252
|
+
- **Struct layouts.** CPython's internal layouts change between versions and builds. heapspy uses DWARF or PDB symbols when available, 3.14's debug offsets section where present, and shipped per-version offset tables otherwise.
|
|
253
|
+
|
|
254
|
+
## Agent integration: heapspy-mcp
|
|
255
|
+
|
|
256
|
+
A second package, `heapspy-mcp` in [`python/heapspy-mcp`](python/heapspy-mcp/README.md), exposes heapspy to coding agents over the Model Context Protocol. It shells out to the `heapspy` binary and parses `--json`. The intended loop: capture a snapshot, summarize it, pick a suspect, trace what retains it, map the holder to `file:line` in your repo, read the code, diagnose. It is built and tested against the documented JSON schema; it becomes useful once `dump` and the analyzer work.
|
|
257
|
+
|
|
258
|
+
Tools:
|
|
259
|
+
|
|
260
|
+
| Tool | Purpose |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `list_python_processes` | candidate PIDs with RSS, command line, Python version |
|
|
263
|
+
| `capture_snapshot` | dump a PID or ingest a core/minidump (privileged, requires confirmation) |
|
|
264
|
+
| `heap_summary` | top retainers and type histogram |
|
|
265
|
+
| `retaining_path` | the chain keeping an object or type alive |
|
|
266
|
+
| `type_detail` | instances, sizes, sample holders for one type |
|
|
267
|
+
| `diff_snapshots` | growth between two captures |
|
|
268
|
+
| `map_to_source` | resolve `module.qualname` to `file:line` |
|
|
269
|
+
|
|
270
|
+
`capture_snapshot` is the only tool that touches a process, and it is gated by confirmation, a process allowlist, and a heap-size limit. The other tools only read snapshot files. Tools return bounded, ranked summaries. The raw graph is never sent to the model, both to keep token use down and to keep memory contents out of transcripts.
|
|
271
|
+
|
|
272
|
+
## Installation (planned)
|
|
273
|
+
|
|
274
|
+
Nothing has been released yet. Until the first release, the commands below will not find a package. heapspy will be a Rust binary distributed as PyPI wheels, the same way py-spy is. The wheels contain only the binary and work with any Python 3.8 or later:
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
pip install heapspy
|
|
278
|
+
pipx install heapspy # or keep it in its own environment
|
|
279
|
+
pip install heapspy-mcp # optional, for agent integration
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Wheels are planned for Linux x86_64 and aarch64 (glibc), Linux x86_64 (musl) and Windows x86_64. Each GitHub release will also have the bare binaries attached.
|
|
283
|
+
|
|
284
|
+
To build from source now, with Rust 1.85 or later:
|
|
285
|
+
|
|
286
|
+
```sh
|
|
287
|
+
cargo install --git https://github.com/Heknon/heapspy heapspy
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
See [docs/releasing.md](docs/releasing.md) for how releases are built and published.
|
|
291
|
+
|
|
292
|
+
## Roadmap
|
|
293
|
+
|
|
294
|
+
| Milestone | Deliverable | Status |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| M1 | Linux core dump reader to snapshot, proven on one `gcore` of a 3.12 process | done |
|
|
297
|
+
| M2 | Analyzer: dominators, retained sizes, histogram, retaining paths, `--json`, property tests | done |
|
|
298
|
+
| M3 | Linux end-to-end test matrix (3.12 to 3.14) passing in CI | passes locally; CI job added (`ci:e2e`), not yet run on GitHub |
|
|
299
|
+
| M4 | Windows minidump backend | done |
|
|
300
|
+
| M5 | Windows end-to-end test matrix passing on `windows-latest` | harness and CI job written, not yet run |
|
|
301
|
+
| M6 | `heapspy-mcp` | done |
|
|
302
|
+
| M7 | `heapspy record`: sampled allocation stacks from outside the process (Linux eBPF, Windows ETW and debugger breakpoints), joined with snapshots. Design in [docs/allocation-sampling.md](docs/allocation-sampling.md) | Linux done, CI job `ci:record`; Windows passes on `windows-latest` except the kill-mid-run safety checks |
|
|
303
|
+
|
|
304
|
+
The end-to-end tests run heapspy against fixture processes with known leaks (an unbounded module-level cache, a listener registry that never shrinks, a closure holding a large object, and others) and check that the analyzer names the right holder, reports a retained size within tolerance, and produces the expected retaining path. See [tests/e2e/README.md](tests/e2e/README.md).
|
|
305
|
+
|
|
306
|
+
## Repository layout
|
|
307
|
+
|
|
308
|
+
| Path | Contents |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `crates/heapspy-snapshot` | snapshot file format: types, streaming writer, mmap reader, test builder |
|
|
311
|
+
| `crates/heapspy-reader` | `MemorySource` trait, ELF core / live Linux / minidump backends, CPython walker, `dump` |
|
|
312
|
+
| `crates/heapspy-analyzer` | JSON report types (schema v1), dominators, retained sizes, paths, diffs |
|
|
313
|
+
| `crates/heapspy-alloc` | allocation profile format (`.hpya`): types, streaming writer, mmap reader, line tables, test builder |
|
|
314
|
+
| `crates/heapspy-record` | `heapspy record`: recorder interface and options; eBPF (`bpf/` holds the committed BPF object and its source), ptrace and Windows recorders |
|
|
315
|
+
| `crates/heapspy` | the `heapspy` command-line tool |
|
|
316
|
+
| `tests/e2e` | end-to-end tests against real CPython processes (pytest) |
|
|
317
|
+
| `docs/snapshot-format.md` | byte-level specification of `.hpy` files |
|
|
318
|
+
| `docs/alloc-format.md` | byte-level specification of `.hpya` files |
|
|
319
|
+
| `docs/allocation-sampling.md` | M7 design: allocation sampling from outside the process |
|
|
320
|
+
| `docs/json-schema.md` | the `--json` output schema; examples in `docs/examples/` |
|
|
321
|
+
| `python/heapspy-mcp` | the `heapspy-mcp` MCP server (Python) |
|
|
322
|
+
| `docs/releasing.md` | how to cut a release and the one-time PyPI setup |
|
|
323
|
+
| `pyproject.toml` | maturin config that packages the `heapspy` binary as a PyPI wheel |
|
|
324
|
+
| `scripts/check-version.py` | checks that the release tag and package versions agree |
|
|
325
|
+
|
|
326
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the local checks and how CI is run.
|
|
327
|
+
|
|
328
|
+
## License
|
|
329
|
+
|
|
330
|
+
MIT. See [LICENSE](LICENSE).
|
|
331
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
heapspy-0.1.0.data/scripts/heapspy.exe,sha256=k3CyN_5EhreuxTMGqmwZuA-PzYN5la7HgY3QUgS3eao,2587648
|
|
2
|
+
heapspy-0.1.0.dist-info/METADATA,sha256=ngeu9agXt0emtl6uTfSYjFez_L4DpA5xISJxmAQ3cUI,22715
|
|
3
|
+
heapspy-0.1.0.dist-info/WHEEL,sha256=Uvw7WSmy7Kj2FBk8Va4L_dq4MDW8X5Ynby-pOX3ZNS0,94
|
|
4
|
+
heapspy-0.1.0.dist-info/licenses/LICENSE,sha256=mt3yTrnts-GMz6yxCWSGJ9kahtE4cOJTuFTQerV8F8k,1087
|
|
5
|
+
heapspy-0.1.0.dist-info/sboms/heapspy.cyclonedx.json,sha256=StnmJPpiuKbDEYv6czmoPsMGsp3pP2kPbHlJEhZoqeA,62624
|
|
6
|
+
heapspy-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ori Harel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|