Пишем на PHP и не теряем память!

Валентин @vudaltsov

Пых / PHP Point / PHP умирает?!

✅ RFC: new without parentheses

                
                    // PHP <8.4
                    $request = (new Psr7Request())->withMethod('GET')->withUri('/hello');

                    // PHP >=8.4
                    $request = new Psr7Request()->withMethod('GET')->withUri('/hello');
                
            

Ставь RoadRunner сегодня вечером!

max_worker_memory

                
                    http:
                      address: 0.0.0.0:80
                      pool:
                        debug: false
                        num_workers: 20
                        supervisor:
                          # выставляй ~80-90% от memory_limit
                          max_worker_memory: 100
                
            

Что такое утечка памяти?

Почему их сложно искать и предотвращать?

Не мешай PHP убираться!

                
                    $a = 'PHP';
                    $b = $a;
                    $c = $a;

                    xdebug_debug_zval('a'); // refcount=3, is_ref=0

                    $b = 42;

                    xdebug_debug_zval('a'); // refcount=2, is_ref=0

                    unset($c);

                    xdebug_debug_zval('a'); // refcount=1, is_ref=0
                
            

composer/composer#8200

composer/composer#8200

Декомпозируй код!

                
                    // ...

                    private function getRuleSetSize(/** ... */)
                    {
                        $solver = new Solver($policy, $pool, $installedRepo, $this->io);

                        try {
                            $solver->solve($request, $this->ignorePlatformReqs);

                            return $solver->getRuleSetSize();
                        } catch (SolverProblemsException $e) {
                            // ...
                        }
                    }
                
            

Не зацикливай объекты!

                
                    final class Foo
                    {
                        public string $string = 'GC TEST!!!';
                        public self $self;
                    }

                    for ($i = 0; $i <= 10_000_000; ++$i) {
                        $object = new Foo();
                        $object->self = $object;
                    }

                    echo memory_get_peak_usage() / 1024 / 1024, PHP_EOL;
                
            
                
                    time php -dzend.enable_gc=0 -dmemory_limit=-1 gc-test.php

                    Memory: 891.31MiB
                    Time:   0.47s
                
            
                
                    time php -dzend.enable_gc=1 -dmemory_limit=-1 gc-test.php

                    Memory: 1.26MiB
                    Time:   0.63s
                
            

Попробуй выключить GC!


                
                    final readonly class Foo
                    {
                        /**
                         * @var list<Closure>
                         */
                        private array $callbacks;

                        public function __construct()
                        {
                            $this->callbacks = [
                                function ($stackPos) {
                                     // ...
                                },
                            ];
                        }
                    }
                
            

nikic/PHP-Parser#8200

                
                    use PhpParser\Lexer;
                    use PhpParser\Parser\Php7;

                    gc_disable();

                    for ($i = 0; $i < 4; ++$i) {
                        new Php7(new Lexer());
                        echo memory_get_usage().PHP_EOL;
                    }

                    // 7992640
                    // 8279744
                    // 8558624
                    // 8853888
                
            
                
                    class Php8 extends ParserAbstract
                    {
                        // ...

                        protected function initReduceCallbacks(): void
                        {
                            $this->reduceCallbacks = [
                                1 => function ($stackPos) {
                                     $this->semValue = $this->handleNamespaces(/** ... */);
                                },
                                // ...
                            ];
                        }
                    }
                
            

nikic/PHP-Parser#af14fdb

                
                    class Php8 extends ParserAbstract
                    {
                        // ...

                        protected function initReduceCallbacks(): void
                        {
                            $this->reduceCallbacks = [
                                1 => static function ($self, $stackPos) {
                                     $self->semValue = $self->handleNamespaces(/** ... */);
                                },
                                // ...
                            ];
                        }

                        protected function doParse()
                        {
                            // ...
                                    $callback($this, $stackPos);
                            // ...
                        }
                    }
                
            

Как тестировать?

                
                    final class PhpParserMemoryTest extends TestCase
                    {
                        #[RunInSeparateProcess]
                        public function testItIsGarbageCollected(): void
                        {
                            gc_disable();
                            $phpParser = (new ParserFactory())->createForHostVersion();
                            $weakReference = WeakReference::create($phpParser);

                            unset($phpParser);

                            self::assertNull($weakReference->get());
                        }
                    }
                
            

Используй статические замыкания!

PHP-CS-Fixer: static_lambda

                
                    final readonly class A
                    {
                        public function __construct()
                        {
                            static fn (): self => $this;
                        }
                    }
                
            
                
                    Psalm output:

                    ERROR: InvalidScope - 5:31 - Invalid reference to $this in a static context
                
            

Читай построчно!

                
                    final class CsvParser
                    {
                        /**
                         * @return Generator<list<string>>
                         */
                        public function parse(string $file): Generator
                        {
                            $handler = fopen($file, 'rb');
                            flock($handler, LOCK_SH);

                            while (false !== $row = fgetcsv($handler)) {
                                yield $row;
                            }

                            flock($handler, LOCK_UN);
                            fclose($handler);
                        }
                    }
                
            

Читай из БД через курсор!

                
                    final readonly class CursorReader
                    {
                        public function __construct(private PDO $postgres) {}

                        /** @return Generator<string> */
                        public function read(): Generator
                        {
                            $cursor = uniqid('cursor_');
                            $this->connection->exec(
                                "declare {$cursor} cursor for
                                select id from my_table",
                            );

                            do {
                                $batchSize = 0;
                                $result = $this->connection->query(
                                    "fetch 1000 from {$cursor}"
                                );

                                foreach ($result as ['id' => $id]) {
                                    ++$batchSize;
                                    yield $id;
                                }
                            } while ($batchSize === 1000);
                        }
                    }
                
            
                
                    $postgres = new PDO('...');

                    $reader = new CursorReader($postgres);

                    foreach ($reader->read() as $id) {
                        var_dump($id);
                    }
                
            
                
                    $postgres = new PDO('...');

                    $reader = new CursorReader($postgres);

                    $postgres->beginTransaction();

                    try {
                        foreach ($reader->read() as $id) {
                            var_dump($id);
                        }

                        $postgres->commit();
                    } catch (Throwable $exception) {
                        $postgres->rollback();

                        throw $exception;
                    }
                
            

Пиши в бд эффективно!

                
                    insert into table_name (col_1, col_2)
                    values
                        (1, 1),
                        (1, 2),
                        (1, 3),
                        ...
                
            
                
                    final readonly class EffectiveWriter
                    {
                        public function __construct(
                            private PDO $postgres,
                        ) {}

                        /**
                         * @param iterable<array> $rows
                         */
                        public function write(iterable $rows): void
                        {
                            $file = tempnam(sys_get_temp_dir(), 'EffectiveWriter');
                            $handle = fopen($file, 'wb');

                            foreach ($rows as $row) {
                                fwrite($handle, encodeRow($row));
                            }

                            $this->postgres->pgsqlCopyFromFile('my_table', $file);

                            fclose($handle);
                            unlink($file);
                        }
                    }
                
            

Мемоизация

  • Зачем?
  • Где?
  • Сколько?

Мемоизируешь — вытесняй!

  • LRU — вытесняется не использованный дольше всего
  • MRU — вытесняется последний использованный
  • LFU — вытесняется реже всего использованный
  • SNLRU, ARC, MQ, 2Q и др.
                
                        /**
                         * @template T
                         * @param callable(): T $factory
                         * @return T
                         */
                        public function get(string $key, callable $factory): mixed
                        {
                            if (array_key_exists($key, $this->itemsByKey)) {
                                $value = $this->items[$key];
                                unset($this->items[$key]);
                                $this->items[$key] = $value;

                                return $value;
                            }

                            $value = $factory();
                            $this->items[$key] = $value;

                            if (count($this->items) > $this->capacity) {
                                array_shift($this->items);
                            }

                            return $value;
                        }
                
            

Не вытесняешь — чисти!

Symfony kernel.reset tag

                
                    return static function (ContainerConfigurator $di): void {
                        $di->services()
                            ->defaults()
                                ->autowire()
                                ->autoconfigure()
                            ->set(MyService::class)
                                ->tag('kernel.reset', ['method' => 'trololoshka']);
                    };
                
            
                
                    class Registry
                    {
                        // ...

                        public function reset(): void
                        {
                            // примерный код

                            foreach ($this->managers as $manager) {
                                $manager->clear();
                            }
                        }
                    }
                
            

Инструменты

Xdebug Profiler

                
                    xdebug.mode=profile
                    xdebug.output_dir=/path/to/profiles/dir
                
            

PhpStorm

PhpStorm

memprof PHP extension

Blackfire

Открыть этот отчёт

Meminfo

                
                    $ bin/analyzer summary dump.json

                    +----------+-----------------+-----------------------------+
                    | Type     | Instances Count | Cumulated Self Size (bytes) |
                    +----------+-----------------+-----------------------------+
                    | string   | 132             | 7079                        |
                    | MyClassA | 100             | 7200                        |
                    | array    | 10              | 720                         |
                    | integer  | 5               | 80                          |
                    | float    | 2               | 32                          |
                    | null     | 1               | 16                          |
                    +----------+-----------------+-----------------------------+
                
            

Meminfo

                
                    $ bin/analyzer top-children dump.json

                    +-----+----------------+----------+
                    | Num | Item ids       | Children |
                    +-----+----------------+----------+
                    | 1   | 0x7ffff4e22fe0 | 1000000  |
                    | 2   | 0x7fffe780e5c8 | 11606    |
                    | 3   | 0x7fffe9714ef0 | 11602    |
                    | 4   | 0x7fffeab63ca0 | 3605     |
                    | 5   | 0x7fffd3161400 | 2400     |
                    +-----+----------------+----------+
                
            

Полезные ссылки

Спасибо за внимание!

QR code

@vudaltsov / Пых / PHP Point / PHP умирает?!