Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Memory Scanner Laravel Package

shipmonk/memory-scanner

ShipMonk's Memory Scanner is a lightweight PHP utility for measuring memory usage around callbacks and code blocks. It helps compare implementations, track allocations, and spot leaks or unexpected memory growth in tests and long-running processes, keeping memory consumption under control.

View on GitHub
Deep Wiki
Context7

Getting Started

Install as a dev dependency only — this is a diagnostic tool, not runtime infrastructure:

composer require --dev shipmonk-rnd/memory-scanner

Check composer.json in the package for the PHP floor (it targets modern PHP 8.x) before adding it to a Laravel app pinned to an older runtime.

Where to look first

  1. The README "Usage" snippet — the public surface is deliberately tiny: one scanner class, one result/exception type, and a callable you hand it.
  2. src/MemoryScanner.php — read the ~200 lines once. You need to understand how it decides something is a leak (it snapshots live objects before/after a callback and reports survivors), because every false positive you hit later comes from that definition.
  3. The exception class it throws on detection — its message is your actual debugging output, and its format is what you'll be parsing when a build goes red.

First use case: assert a service doesn't leak in a test

The highest-value, lowest-risk adoption is a PHPUnit/Pest test around a single hot path — an importer, a report builder, a job handler:

use ShipMonk\MemoryScanner\MemoryScanner;

it('does not leak while building the report', function () {
    $builder = app(ReportBuilder::class);

    // warm caches/container so lazy singletons aren't counted as leaks
    $builder->build();

    (new MemoryScanner())->checkMemoryLeaks(function () use ($builder) {
        $builder->build();
    });
});

That's the whole loop: install, wrap one callable, run the suite. Once that test is green and stable, promote the scanner into the places where leaks actually hurt — Octane workers and queue consumers.

Implementation Patterns

Pattern 1 — middleware around the request in Octane

Octane keeps the container alive across requests, so a leaked singleton degrades the worker over hours. Wrap the unit of work, not the response:

class LeakCheckMiddleware
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! config('app.debug') || ! app()->bound(MemoryScanner::class)) {
            return $next($request);
        }

        $scanner = app(MemoryScanner::class);
        $result  = $scanner->checkMemoryLeaks(fn () => app(ReportBuilder::class)->build());

        return $next($request);
    }
}

Note the shape: the leak check wraps a self-contained call whose return value you don't keep. If you wrap $next($request) itself, the Response object survives by design and will be reported — see Gotchas.

Pattern 2 — queue job middleware

Long-running queue:work processes are where leaks are most expensive. A job middleware gives you per-job attribution, which is far more useful than a single line in the log:

class DetectJobLeaks
{
    public function handle(Job $job, Closure $next): void
    {
        if (! config('queue.leak_check')) {
            $next($job);
            return;
        }

        try {
            (new MemoryScanner())->checkMemoryLeaks(fn () => $next($job));
        } catch (MemoryLeakException $e) {
            report(new RuntimeException("Leak in {$job->resolveName()}", previous: $e));
        }
    }
}

Register it per-job via middleware() rather than globally — you want this on the three jobs that matter, not on all of them.

Pattern 3 — artisan loop guard

For custom long-running commands (while (true) { process(); }), run the scanner on a sampled iteration (every 1000th) rather than every loop, and let it fail loudly. Compare against memory_get_usage() manually alongside it to distinguish "objects retained" from "allocator never returned pages".

Pattern 4 — baseline before asserting

Always do one untracked warm-up call. Laravel lazily resolves bindings, boots Eloquent models (Model::$booted), populates config caches, and registers listeners on first use. That first-call furniture looks exactly like a leak to a naive snapshot diff. Two calls — warm-up then measured — is the pattern that keeps the suite honest.

Pattern 5 — container hygiene between checks

If you assert across several units in one test, reset scoped state so leak A doesn't get attributed to unit B:

app()->forgetInstance(ReportBuilder::class);
app()->forgetScopedInstances(); // Octane/Laravel 10+
gc_collect_cycles();

Integration tip: wrap the package in your own base-test helper (InteractsWithMemory) so the class name appears in exactly one file. With 18 stars and a single maintainer, you want the ability to swap it for a 50-line inline snapshot diff without touching 40 tests.

Gotchas and Tips

The container is a leak by design. Every singleton(), every app(X::class) you never release, every once() memoization (PHP 8.1's per-object WeakMap cache) will survive the callback and be reported. This is the #1 source of noise. Either call forgetInstance()/forgetScopedInstances() before measuring, or configure the package's ignore/filter list for Illuminate\* classes.

Anything your callback returns or stores counts. If the wrapped callable returns an object, or writes to a property/global, that object outlives the check and is indistinguishable from a leak. Wrap work whose output you can discard.

Closures capturing $this are a classic false positive — and a real Laravel bug pattern too. An event listener registered as function () use ($this) that never gets unregistered pins the whole service graph. Check Event::getRawListeners() if a leak points at a listener you thought was transient.

memory_get_usage() lies about bytes. PHP's chunked allocator doesn't return freed pages to the OS; memory_get_usage(true) will happily sit at 4 MB while your actual object graph is flat. This is why object-level scanning beats byte-diffs for leak hunting — but it also means a byte regression test is a different (and flakier) tool.

Garbage collection matters. Run gc_collect_cycles() before the after-snapshot, and never leave gc_disable() in a test bootstrap — reference cycles ($parent->children[] = $child; $child->parent = $parent) are exactly the leaks you're hunting, and disabled GC turns them into permanent survivors.

Xdebug distorts everything. Its memory overhead per function call is enormous and it changes refcount behaviour. Run leak tests with Xdebug off (php -n or a separate CI job); otherwise you'll chase ghosts, or worse, get false negatives.

Eloquent retains more than you think: Model::$booted, Model::$globalScopes, relation caches on loaded models, and Connection::$recordsModified. Hydrating 10k models inside a leak check will light up the report. Chunk with lazyById() and unset, or exclude Illuminate\Database\Eloquent\Model in the filter config.

Don't leave it on in production. Reflection and debug_zval_dump-style introspection are slow and allocate. Gate it behind APP_DEBUG / a dedicated env flag, and prefer sampling over per-request execution.

Extension points to lean on: the per-class ignore/filter list (use it aggressively — vendor namespaces first), and your own assertion wrapper. There's no Laravel service provider in the package, so register the scanner as a scoped binding yourself if you inject it.

Maintenance reality check: 18 stars, one org, zero dependents. Pin an exact version in composer.json and read the source before upgrading. The technique, not the package, is the durable asset — a minimal snapshot-diff of get_object_vars/class counts around a callable is a weekend's work if the package ever stalls on a new PHP minor.

Debugging tip when the report is noisy: halve the wrapped scope repeatedly (whole test → one method → one query) until the leak count drops. The scanner tells you what survived, not where it was allocated; bisecting the callable is still the fastest way to localise it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin