PHPackages                             reliforp/php-memory-dump - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Debugging &amp; Profiling](/categories/debugging)
4. /
5. reliforp/php-memory-dump

ActiveLibrary[Debugging &amp; Profiling](/categories/debugging)

reliforp/php-memory-dump
========================

Capture a PHP process's own memory in reli's RDUMP format from inside the process, in pure PHP, with no extension and no FFI. The dump is analysed offline with reli (finding leaks, reference cycles, memory bottlenecks, etc.).

00PHPCI passing

Since Jun 3Pushed 1mo agoCompare

[ Source](https://github.com/reliforp/php-memory-dump)[ Packagist](https://packagist.org/packages/reliforp/php-memory-dump)[ RSS](/packages/reliforp-php-memory-dump/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependenciesVersions (2)Used By (0)

php-memory-dump
===============

[](#php-memory-dump)

Take a memory dump of the **current PHP process**, from inside the process, in **pure PHP** — no extension, no FFI — for later analysis with [reli](https://github.com/reliforp/reli-prof).

```
use Reliforp\PhpMemoryDump\MemoryDumper;

(new MemoryDumper())->dump('/tmp/app.rdump');
```

This is the pure-PHP sibling of [ext-rdump](https://github.com/reliforp/ext-rdump). ext-rdump is a C extension that takes the dump with `memcpy`; this package produces the **same RDUMP file** (magic `RDUMP`, format version 3) using only `/proc/self/maps`, `/proc/self/mem`, and the PHP binary's own ELF symbol table. The output feeds the very same reli workflow:

```
reli inspector:memory:analyze app.rdump -f rmem -o app.rmem
reli inspector:memory:report  app.rmem
```

Why this works (and how)
------------------------

[](#why-this-works-and-how)

reli normally snapshots a PHP process from the **outside** via `process_vm_readv` / `ptrace`. To do that it locates the engine globals (`executor_globals`, `compiler_globals`, `basic_globals`) by reading the PHP binary's symbol table and adding the load bias from `/proc//maps`, then reads the relevant regions out of the target's address space.

Everything reli does from the outside, a process can do to **itself** from the inside, in plain PHP:

1. **Resolve the engine globals.** Read `/proc/self/maps` to find the PHP binary's load base, parse its ELF `.symtab` / `.dynsym` to get each symbol's link-time address, and add the bias. (Debian/Ubuntu strip the PHP binary, but the three engine globals are *exported* in `.dynsym`, so a stock distro build is still resolvable.)
2. **Read the regions.** `/proc/self/mem` exposes the whole address space as a seekable file: `fseek()` to a virtual address, `fread()` the bytes. On a 64-bit build PHP's `fseek` takes a 64-bit offset, so the entire user address range is reachable.
3. **Write the RDUMP file.** The header records the resolved addresses, the memory map mirrors `/proc/self/maps`, and each captured region is streamed straight from `/proc/self/mem` to disk.

reli then does all the hard work — walking the Zend heap, the object store, class/function tables, the VM stack — **offline**, from the dump file.

Status: it works
----------------

[](#status-it-works)

On a stock Debian PHP 8.4 (NTS, x86-64) a self-dump analysed by reli reconstructs the live state, including the **call stack at the moment of capture** (which, fittingly, shows this library's own dump path):

```
Call Stack at capture:
  #0 fread:-1
  #1 Reliforp\PhpMemoryDump\RegionReader::readAt:90
  #2 Reliforp\PhpMemoryDump\RegionReader::copyInto:75
  #3 Reliforp\PhpMemoryDump\RdumpWriter::writeRegion:129
  #4 Reliforp\PhpMemoryDump\MemoryDumper::dump:98
  #5 :23

```

…and the full type breakdown / object inventory / leak findings reli produces for any other dump.

### Notes on the two worries you'd expect

[](#notes-on-the-two-worries-youd-expect)

- **Does PHP's `fread` cope with procfs?** For `/proc/self/mem`, yes: `fseek` to an absolute address (low heap *and* high stack addresses like `0x7fff…`) followed by `fread` returns the bytes. We read unbuffered and seek before every read. (Reading *another* process's `/proc//mem` from PHP streams is a different story — that's why reli uses FFI/`pread` there — but `self` is well-behaved.)
- **Doesn't the dumper perturb the VM while it runs?** It does — this is PHP code mutating the heap as it dumps it, so the snapshot is not stop-the-world. We keep the window small: the memory map is frozen *before*any output buffer is allocated, and regions are streamed rather than accumulated. The captured heap therefore contains the dumper's own transient objects (you'll see `MapEntry` instances and the like in the report), but the dump is still internally consistent enough for reli to walk. This mirrors the inconsistency ext-rdump documents for busy ZTS processes.

### Memory footprint of the dump itself

[](#memory-footprint-of-the-dump-itself)

The dumper is deliberately frugal so it barely disturbs the heap it is snapshotting. It does **not** slurp the multi-megabyte PHP binary to parse its symbols — only the ELF/program/section headers and the `.dynsym` / `.dynstr`slices are read — and regions are streamed through a small fixed buffer (256 KiB) rather than buffered whole. On a stock PHP 8.4 the measured peak *added* by `dump()` is on the order of **~0.6–1 MiB of `emalloc`**, with **no new 2 MiB ZendMM chunk forced** (`memory_get_peak_usage(true)` delta 0) — and it is independent of how big the dumped heap or the output file is (a 26 MB dump costs the same as a tiny one). Permanent growth left behind is a couple of hundred KiB at most.

Install
-------

[](#install)

```
composer require reliforp/php-memory-dump
```

Usage
-----

[](#usage)

```
use Reliforp\PhpMemoryDump\MemoryDumper;

$result = (new MemoryDumper())->dump('/tmp/app.rdump');
// $result->region_count, $result->total_bytes, $result->memory_area_count

// Self-contained dump (also embeds read-only file-backed segments, so it can
// be analysed on a host without the original binaries). Larger output.
(new MemoryDumper())->dump('/tmp/app-full.rdump', full: true);
```

### From a signal handler

[](#from-a-signal-handler)

```
pcntl_async_signals(true);
pcntl_signal(SIGUSR2, function () {
    (new \Reliforp\PhpMemoryDump\MemoryDumper())->dump('/tmp/sig.rdump');
});
// kill -USR2
```

### Auto-dump on `memory_limit` death

[](#auto-dump-on-memory_limit-death)

`OomDumpHandler::register()` installs a shutdown handler that dumps the process the moment it dies of `memory_limit` exhaustion — the analogue of reli's sidecar client `MemoryLimitHandler::register()`, but doing the dump in-process instead of asking a daemon.

```
use Reliforp\PhpMemoryDump\OomDumpHandler;

// %p -> pid, %t -> unix time, %% -> literal % (so workers don't collide)
OomDumpHandler::register('/var/log/php-oom-%p-%t.rdump');
```

With options:

```
OomDumpHandler::register(
    path: '/var/log/php-oom-%p.rdump',
    full: false,
    on_dump:  fn ($result, $path) => error_log("OOM dump: $path"),
    on_error: fn (\Throwable $e)  => error_log('OOM dump failed: ' . $e->getMessage()),
    reserve_bytes: 4 * 1024 * 1024, // emergency reserve (must be >= 2 MiB)
    memory_limit: -1,               // raised inside the handler before dumping
);
```

Surviving the OOM that you are trying to capture takes some care, because the handler runs *in-process* with the heap already at the wall. Two levers, both on by default:

- **Raising `memory_limit` (the reliable one).** `memory_limit` is enforced on memory actually taken from the OS (ZendMM's *real* usage). The handler lifts it (to unlimited by default — the process is exiting anyway) before dumping, which deterministically gives the dumper the ~one 2 MiB ZendMM chunk of working set it needs. Pass `memory_limit: false` to opt out.
- **The emergency reserve — and why it must be ≥ 2 MiB.** A pre-allocated block is freed first thing in the handler. But freeing a **sub-2 MiB**block only returns it to a ZendMM chunk's free list; the chunk is **not**unmapped, so *real* usage — and thus `memory_limit` headroom — does not change. Measured: a **1 MiB reserve frees zero headroom**, the dump then re-OOMs mid-write, and you get a **truncated, unusable** file (reli refuses it). 2 MiB is the practical minimum (it is a *huge* allocation, unmapped on free); the default 4 MiB leaves margin. The reserve is mainly the fallback for when `ini_set('memory_limit', …)` is restricted.

> **Best-effort, by nature.** A shutdown handler cannot catch *every* OOM: when the exhausting allocation is itself a VM-stack page push, pushing the handler's own call frame needs another allocation that also fails, so the handler body never runs — no reserve or `memory_limit` bump can help, because nothing executes. Catching those needs ext-rdump's C `zend_error_cb` hook. In practice this handler covers the common "accumulated too much, then one more append tipped it over" OOM, and the resulting dump pinpoints the offender — e.g. reli's report flags the very array that overflowed the limit as the dominant retained branch.

Requirements &amp; limitations
------------------------------

[](#requirements--limitations)

- **64-bit little-endian Linux**, PHP **7.0+**, built **NTS**. (The released package is downgraded to 7.0 syntax — see *PHP version support* below — so it matches ext-rdump's 7.0–8.5 reach; develop against PHP 8.1+.)
- **NTS only.** ZTS keeps the engine globals in thread-local storage reached through TSRM; resolving those without walking the TLS block is out of reach for a pure-PHP reader, so ZTS is rejected with a clear error. Use ext-rdump (or reli attaching from the outside) for ZTS.
- The PHP binary must expose `executor_globals` / `compiler_globals` / `basic_globals` in `.symtab` or `.dynsym`. Stock distro builds do (they're exported); a binary stripped of *both* tables is not resolvable.
- Reads `/proc/self/mem`, which a restrictive sandbox/seccomp profile or a Yama `ptrace_scope` policy targeting the self-read path may block.
- The dump is taken synchronously in the calling code; it is not stop-the-world (see above).

### PHP version support (and the downgrade build)

[](#php-version-support-and-the-downgrade-build)

The library is **developed in modern PHP (8.1+)** — readonly properties, constructor promotion, `match`, named arguments — but **released downgraded to PHP 7.0 syntax**, so it installs and runs anywhere from PHP 7.0 to 8.5, the same floor as ext-rdump.

This mirrors reli's own sidecar client (`reliforp/reli-prof-sidecar-client`), which ships the same way. The downgrade is `composer downgrade`:

1. [Rector](https://github.com/rectorphp/rector)'s downgrade level set rewrites 8.x → 7.1 (`withDowngradeSets(php71: true)`).
2. Three small custom rules under `tools/rector/` fill the 7.1 → 7.0 gap that Rector no longer ships: strip nullable type hints (`?T`), `void` return types, and class-constant visibility. (Lifted from reli's `tools/rector/Downgrade*ToPhp70Rector`.)
3. A `sed` pass rewrites `0o…` octal literals.

The result lands in `build/php70/src/`. CI builds it on PHP 8.4, asserts no 7.1+ syntax survives, and runs a smoke dump against the downgraded tree on PHP 7.0 / 7.1 / 7.2 / 7.4 / 8.0 — the runtimes where the published artifact lives but Rector and PHPUnit can't. The git source stays modern; only the release artifact is downgraded.

### Cross-version round-trip against reli

[](#cross-version-round-trip-against-reli)

A separate workflow (`reli-cross-version.yml`, the pure-PHP analogue of ext-rdump's same-named CI) proves the full producer→analyzer contract: it loads the downgraded build on every `php:7.0-cli … php:8.5-cli` image, writes a self-contained (`full: true`) dump, then sets reli up once on PHP 8.4 and analyses every dump — asserting the RDUMP magic, format version 3, the recorded origin PHP-version tag, and that `inspector:memory:analyze` produces a report. The analyzer step reuses ext-rdump's `reli-analyze.sh` almost verbatim, since the RDUMP format is identical regardless of which producer wrote it. A ZTS leg asserts the NTS-only contract fails fast (no bogus dump) on `php:*-zts`.

What gets captured
------------------

[](#what-gets-captured)

Mirroring ext-rdump: every VMA appears in the dump's memory map, and the bytes of every **readable, volatile** mapping are embedded — writable segments (ZendMM chunks, the brk heap, VM stacks, library `.data`/`.bss` where NTS globals live), anonymous mappings, and opcache `/dev/zero` shared memory. With `full: true`, read-only file-backed segments are embedded too. Read-only binary segments left out of a non-full dump are recovered from the on-disk binaries by reli at analysis time (use `full: true`, or reli's `--dependency-root`, to analyse on a different host).

Security
--------

[](#security)

**A dump is a verbatim copy of your process's memory, so treat the file as highly sensitive** — it can contain environment variables, credentials, session tokens, decrypted request/response bodies, and private keys. The file is created `0600`; still, write it somewhere only the intended user can read, move it over a secure channel, and delete it once analysed. See ext-rdump's README for the longer discussion; the same cautions apply.

Relationship to ext-rdump
-------------------------

[](#relationship-to-ext-rdump)

ext-rdumpphp-memory-dumpFormC extensionpure PHP libraryGlobals resolutionlinker (compile-time)ELF symbol table + maps load biasRegion copy`memcpy` / `/proc/self/mem``/proc/self/mem`OOM auto-dump (`zend_error_cb`)yesno (no hook available in pure PHP)ZTSyesnoOutputRDUMP v3RDUMP v3 (same reader)If you can install an extension, ext-rdump is faster, works under ZTS, and can auto-dump on `memory_limit` exhaustion. This package is for when you **can't**add an extension but can still run PHP.

License
-------

[](#license)

MIT.

###  Health Score

20

—

LowBetter than 12% of packages

Maintenance60

Regular maintenance activity

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity14

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/6488121?v=4)[sji](/maintainers/sj-i)[@sj-i](https://github.com/sj-i)

---

Top Contributors

[![sj-i](https://avatars.githubusercontent.com/u/6488121?v=4)](https://github.com/sj-i "sj-i (7 commits)")

### Embed Badge

![Health badge](/badges/reliforp-php-memory-dump/health.svg)

```
[![Health](https://phpackages.com/badges/reliforp-php-memory-dump/health.svg)](https://phpackages.com/packages/reliforp-php-memory-dump)
```

###  Alternatives

[proget-hq/phpstan-yii2

Yii2 extension for PHPStan

52614.0k7](/packages/proget-hq-phpstan-yii2)[upscale/swoole-blackfire

Blackfire profiler integration for Swoole web-server

22126.6k13](/packages/upscale-swoole-blackfire)[wanfeiyy/dd

use laravel dd replace var\_dump

2548.9k7](/packages/wanfeiyy-dd)[fjogeleit/prometheus-messenger-middleware

Prometheus Middleware for the Symfony Messenger Component

2255.2k](/packages/fjogeleit-prometheus-messenger-middleware)[crstauf/query-monitor-extend

Query Monitor Extend

1041.3k](/packages/crstauf-query-monitor-extend)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
