phpdompdf

Identifying and Resolving Memory Issues with Dompdf in Symfony Application


Problem Description

I'm encountering an out-of-memory issue in my Symfony application while generating PDF files using Dompdf. I suspect there might be a memory leak in the Dompdf library. To investigate, I wrote a script to create multiple PDF files and monitor memory usage.

use Dompdf\Dompdf;

ini_set('memory_limit', '20M');

print memory_get_usage() . PHP_EOL;

for ($i = 0; $i < 1000; $i ++) {
    print memory_get_usage() . PHP_EOL;
    createPDF();
}

print memory_get_usage() . PHP_EOL;

function createPDF () {
    // instantiate and use the dompdf class
    $dompdf = new Dompdf();
    $dompdf->loadHtml('hello world');
    // (Optional) Setup the paper size and orientation
    $dompdf->setPaper('A4', 'landscape');
    // Render the HTML as PDF
    $dompdf->render();
}

However, running this script results in the following error:

PHP Fatal error: Allowed memory size of 20971520 bytes exhausted (tried to allocate 12288 bytes) in path/to/vendor/dompdf/dompdf/src/Css/Stylesheet.php on line 281

Question

I suspect there might be a memory leak in the Dompdf library. How can I verify whether the issue is indeed related to Dompdf or if there's another underlying problem causing the memory exhaustion?

Additional Information


Solution

  • The problem is that the PHP garbage collector is not kicking in.

    If you add gc_collect_cycles() inside createPDF() you can force garbage collection.

    You can trace the GC via:

    php -dmemory_limit=7M -dxdebug.mode=gcstats -dxdebug.start_with_request=yes min.php
    

    And look for gcstats files in /tmp/ or wherever you set XDebug output to.

    So why is the garbage collector not kicking in? Well, as per https://www.php.net/manual/en/features.gc.collecting-cycles.php:

    When the garbage collector is turned on, the cycle-finding algorithm as described above is executed whenever the root buffer runs full. The root buffer has a fixed size of 10,000 possible roots (although you can alter this by changing the GC_THRESHOLD_DEFAULT constant in Zend/zend_gc.c in the PHP source code, and re-compiling PHP). When the garbage collector is turned off, the cycle-finding algorithm will never run. However, possible roots will always be recorded in the root buffer, no matter whether the garbage collection mechanism has been activated with this configuration setting.

    As the a DomPdf instance takes up quite a hefty chunk of memory with each iteration (a lot of it is in FontMetrics caching), not enough objects (it's a small library) are in the root buffer to spawn the GC before running out of memory.

    If you run gc_status() at the end of createPdf you'll see that after one run you get a mere ~150 roots (depending on the chosen adapter). For 20M you'll get about 7000 roots, short of the 10k threshold. And if you increase the limit to 40M the script finishes successfully with 8 GC runs never running out of memory:

    Garbage Collection Report
    version: 1
    creator: xdebug 3.2.0 (PHP 8.2.1)
    
    Collected | Efficiency% | Duration | Memory Before | Memory After | Reduction% | Function
    ----------+-------------+----------+---------------+--------------+------------+---------
        87815 |    878.15 % | 12.88 ms |      28461832 |      5090000 |    82.12 % | Dompdf\Css\Stylesheet::apply_styles
        88140 |    881.40 % | 14.66 ms |      28556032 |      5090000 |    82.18 % | Dompdf\Css\Stylesheet::apply_styles
        88140 |    881.40 % | 15.01 ms |      28556032 |      5090000 |    82.18 % | Dompdf\Css\Stylesheet::apply_styles
        88140 |    881.40 % | 15.43 ms |      28556032 |      5090000 |    82.18 % | Dompdf\Css\Stylesheet::apply_styles