Skip to content

Performance: memory-fragmentation analyzer

The Performance module has a narrow scope. It provides one observation-only tool, MemoryFragmentationAnalyzer, that measures peak and retained memory across marked windows of engine work. Its public surface also includes the immutable snapshot the tool produces. It does not enforce budgets, throttle work, or change engine behavior.

Scope and stability. The real surface of this module is two classes (MemoryFragmentationAnalyzer, MemoryFragmentationSnapshot). It is not a per-operation budget enforcement harness. The performance_budget value in every module’s frontmatter is a documentation convention, not a value this module enforces. The surface is experimental: it is a diagnostic introduced in @since 3.2.0. Its snapshot shape may evolve.

Terminal window
composer require nextpdf/core:^3

Resource utilization is a first-class quality concern for a Portable Document Format (PDF) engine. The minimal observable is the separation between peak memory (the high-water mark during a window) and retained memory (what is still held after the window). This module measures only that.

MemoryFragmentationAnalyzer only observes; it does not mutate writer or document state. reset() runs a garbage collection (GC) cycle and resets PHP’s peak counter so subsequent measurements belong to the window since the reset. mark(string $label) captures a MemoryFragmentationSnapshot at a labeled point. snapshots() returns the captured series. peakDelta() and retainedDelta() report the peak and retained change across the run.

MemoryFragmentationSnapshot is a final readonly value object: a labeled point with transientBytes() (peak minus retained, which is memory that was used and released), retentionRatio() (retained over peak), and toArray() for export. A high transient-bytes value with a low retention ratio indicates churn that a buffer-reuse strategy could remove. Both classes are @since 3.2.0.

ClassKey membersRole
MemoryFragmentationAnalyzerreset(), mark(string $label), snapshots(), peakDelta(), retainedDelta()Observation-only memory analyzer (@since 3.2.0)
MemoryFragmentationSnapshottransientBytes(), retentionRatio(), toArray()Immutable labeled measurement (@since 3.2.0)

Run composer docs:generate-api-php -- --module=Performance to generate the full PHPDoc table.

Instrument a hot path, then read the deltas.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Performance\MemoryFragmentationAnalyzer;
$analyzer = new MemoryFragmentationAnalyzer();
$analyzer->reset();
$analyzer->mark('before-write');
// ... engine work under observation ...
$analyzer->mark('after-write');
printf("Peak delta: %d B, retained delta: %d B\n", $analyzer->peakDelta(), $analyzer->retainedDelta());

Wrap a render and emit the fragmentation snapshot to a metrics sink. Treat a low retention ratio under high transient bytes as a churn signal.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Performance\MemoryFragmentationAnalyzer;
use Psr\Log\LoggerInterface;
final readonly class RenderMemoryProbe
{
public function __construct(private LoggerInterface $logger) {}
/** @param callable():void $render The render closure to observe. */
public function observe(callable $render): void
{
$analyzer = new MemoryFragmentationAnalyzer();
$analyzer->reset();
$analyzer->mark('start');
$render();
$analyzer->mark('end');
foreach ($analyzer->snapshots() as $snapshot) {
$this->logger->info('mem-frag', $snapshot->toArray());
}
}
}
  • reset() calls gc_collect_cycles() and memory_reset_peak_usage(). It has a process-global effect on PHP’s peak counter. Do not interleave it with another component that reads the same counter in the same request.
  • Measurements belong to the window since the last reset(). A mark() without a preceding reset() measures from process start, which is usually not what you want.
  • This is a diagnostic, not a control. It never throttles or aborts work. Do not build back pressure on it.
  • The reproducibility profile is structural: byte figures depend on the runtime, allocator, and GC state. Two runs can differ numerically even for the same logical work.

The analyzer’s own overhead is a garbage collection cycle on reset() and one hrtime() / memory_get_* read per mark(), which is negligible relative to the work it observes. It allocates one small snapshot per mark(). The performance_budget value in this frontmatter is the documentation-wide reference figure; this module does not enforce it.

Memory figures are diagnostic data. They do not contain document content, but a fine-grained memory profile can reveal input size and shape. Treat snapshot exports as internal telemetry, and apply the project’s log-scrubbing obligation before you share them externally. The module performs no input/output (I/O) and embeds no external data. See the engine threat model in /modules/core/security/.

This module makes no normative claim about the PDF specification. It is a memory diagnostic and implements no standardized protocol with clauses to cite. Its architectural rationale references the resource-utilization quality view from the ISO/IEC/IEEE 42010 architecture-description framework. That reference is an architecture-practice alignment, not a PDF citation. Engine conformance is validated by the oracle and golden suites described in /modules/core/conformance/.