Skip to content

Commit 83df4a2

Browse files
feat(profiling): filter heap-live frees with shadow bitmap
1 parent f7782fd commit 83df4a2

12 files changed

Lines changed: 370 additions & 289 deletions

File tree

profiling/alternatives.md

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,55 @@ reallocates it. The source of truth is currently a `DashMap` keyed by the
55
allocation pointer. Looking up every freed pointer in that map adds overhead to
66
the common case where the allocation was not sampled.
77

8-
The allocation-prefix PoC makes sampled state cheap to query, but relocating
9-
pointers requires a clean epoch after which every pointer passed to the custom
10-
handlers has that prefix. This document describes two fallback alternatives
11-
that preserve the pointer returned by ZendMM if that epoch cannot be guaranteed.
8+
A prefix makes sampled state cheap to query, but relocating pointers requires a
9+
clean epoch after which every pointer passed to the custom handlers has that
10+
prefix. All designs below preserve the pointer returned by ZendMM.
11+
12+
## Shadow bitmap
13+
14+
### Design
15+
16+
Use the allocation address as an exact index into a thread-local sparse bitmap.
17+
ZendMM pointers are at least eight-byte aligned, so each possible allocation
18+
start needs one bit. Within a four-kilobyte application page:
19+
20+
```text
21+
page = pointer >> 12
22+
slot = (pointer & 0xfff) >> 3
23+
word = slot >> 6
24+
mask = 1 << (slot & 63)
25+
```
26+
27+
A page has 512 possible eight-byte-aligned starts, represented by eight `u64`
28+
words, or 64 bytes. A six-level radix tree keyed by `page` allocates those page
29+
bitmaps lazily and covers the full pointer address space without reserving a
30+
multi-terabyte flat shadow mapping.
31+
32+
After a sampled allocation is successfully inserted into the existing
33+
`DashMap`, set its shadow bit. On free or successful realloc, test and clear
34+
the bit. A clear bit skips the `DashMap`; a set bit removes the known
35+
live sample. The pointer passed to the underlying allocator is never changed.
36+
37+
The bitmap is thread-local because ZendMM heaps are thread-local. Cross-thread
38+
freeing would already forward a pointer to the wrong ZendMM heap.
39+
40+
### Benefits
41+
42+
* No pointer relocation or per-allocation size increase.
43+
* Pre-hook allocations naturally have clear bits.
44+
* The common free path uses address arithmetic and radix loads, without hashing.
45+
* No clean epoch, startup reset, or late-ZTS-thread hook is required.
46+
* The design does not depend on private ZendMM layouts.
47+
* It can filter the existing heap-live tracker on every supported PHP version.
48+
49+
### Costs and open questions
50+
51+
* The first tracked address along a new radix path allocates metadata nodes.
52+
* Each application page containing a tracked allocation needs a 64-byte bitmap.
53+
* Empty radix leaves are currently retained until thread teardown. They can be
54+
pruned if long-lived heaps show unbounded metadata growth.
55+
* The `DashMap` remains the source of full `LiveHeapSample` metadata; the bitmap
56+
only makes its negative free-path lookup cheap.
1257

1358
## Allocation footer
1459

@@ -157,17 +202,18 @@ when the expected map layout is unavailable.
157202

158203
## Comparison
159204

160-
| Property | Footer | Chunk-map filter |
161-
|---|---|---|
162-
| Pointer relocation | None | None |
163-
| Per-allocation memory | At least 8 bytes | None |
164-
| Common free path | Block-size query and footer load | Page-map load |
165-
| Private ZendMM layout | Debug trailer only | Chunk map and bin layout |
166-
| Pre-hook allocations | Safe false positives | Naturally unmarked |
167-
| Huge allocations | Supported with block size | Falls back to `DashMap` |
168-
| Neighboring allocators | Requires fallback | Requires fallback |
169-
170-
The footer is the simpler extension-owned design. The chunk-map filter has the
171-
better expected hot-path and memory cost, but it carries substantially more
172-
compatibility risk unless PHP exposes the required metadata through a supported
173-
API.
205+
| Property | Shadow bitmap | Footer | Chunk-map filter |
206+
|---|---|---|---|
207+
| Pointer relocation | None | None | None |
208+
| Per-allocation memory | None | At least 8 bytes | None |
209+
| Common free path | Radix and bit lookup | Block size and footer | Page-map load |
210+
| Private ZendMM layout | None | Debug trailer only | Chunk map and bins |
211+
| Pre-hook allocations | Naturally unmarked | Safe false positives | Naturally unmarked |
212+
| Huge allocations | Supported | Supported | Falls back to `DashMap` |
213+
| Neighboring allocators | Supported | Requires fallback | Requires fallback |
214+
215+
The shadow bitmap is the preferred extension-owned design. The footer is
216+
simpler than modifying ZendMM internals but adds allocation overhead. The
217+
chunk-map filter has the cheapest expected hot path, but it carries
218+
substantially more compatibility risk unless PHP exposes the required metadata
219+
through a supported API.

profiling/benches/heap_live_tracking.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use core::cell::{Cell, UnsafeCell};
22
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
33

44
// Compile the production tracker without linking the PHP extension executable.
5-
#[allow(dead_code)]
5+
#[allow(dead_code, unused_imports)]
66
#[path = "../src/profiling/live_heap.rs"]
77
mod live_heap;
88
use live_heap::{LiveHeapTracker, LocalLiveHeapTracker};

0 commit comments

Comments
 (0)