• Час читання ~12 хв
  • 28.03.2023

Вся наша команда зараз наполегливо працює над наступною версією Flare. Ми повністю переробляємо програму та веб-сайт, і це величезні зусилля, але це, безсумнівно, окупиться в майбутньому.

У цьому переписуванні Flare ми вирішили перейти від ресурсів Laravel до власного пакету даних Laravel для відправки даних на передній план. Це не тільки додає переваги повністю введеної відповіді, але ми також отримуємо визначення TypeScript для наших ресурсів без додаткових зусиль.

Після того, як всі ресурси були вручну перетворені в об'єкти даних, ми були раді перевірити, чи все працює нормально. Хоча ми не потрапляли на жодні винятки чи помилки, деякі сторінки додатків відчували себе надзвичайно повільними, занадто повільними, щоб бути корисними для наших клієнтів.

Ми швидко виявили, що значна частина часу відповіді була витрачена на об'єкти даних. Ми почали з оптимізації об'єктів даних за допомогою лінивого функціоналу, який дозволяє завантажувати певні частини даних пізніше, коли початкова сторінка вже відправлена. Це зробило сторінки швидшими, але ми не хотіли зупинятися на досягнутому, оскільки повністю переробляємо всю програму.

Пакет даних laravel фантастичний для роботи, але він також додає багато складності при виведенні даних. У цій публікації в блозі ми розглянемо, як ми покращили продуктивність пакета і, таким чином, повну програму Flare.

Для початку я познайомлю вас з трьома інструментами, які ви можете використовувати у своєму наборі інструментів для дослідження проблем продуктивності: Xdebug, PHPBench та QCacheGrind.

Xdebug - це розширення PHP, яке забезпечує можливості налагодження та профілювання. Він найбільш відомий своєю складною конфігурацією, яка може уповільнити роботу всієї вашої програми. На щастя, Xdebug версії 3 досягла значного прогресу в цій частині, і налаштувати Xdebug тепер відносно легко.

PHPBench - це інструмент порівняльного аналізу для PHP, який може допомогти вам виміряти продуктивність вашого коду. Це дозволяє створювати тестові кейси та запускати їх кілька разів, щоб отримати уявлення про швидкість вашого коду.

Нарешті, QCacheGrind - це програма, що дозволяє візуально переглядати профілі, згенеровані Xdebug. QCacheGrind доступний для MacOS і Windows. Якщо ви використовуєте Linux, подивіться на KCachegrind, який майже однаковий.

Getting a baseline

Для початку нам потрібно мати можливість перевірити, чи зміни, які ми вносимо в наш код, позитивно чи негативно впливають на продуктивність. Найкращий спосіб зробити це - створити кілька бенчмарків за допомогою PHPBench. Ці контрольні показники будуть виконуватися кілька разів, і середній час, який цей витрата, можна використовувати як метрику, щоб побачити, чи змінили ми ситуацію, оновивши код

. PHPBench працює так само, як PHPUnit. Ви створюєте кілька тестових файлів (наприклад, тестів) з контрольними випадками (наприклад, тестовими випадками). Потім команда PHPBench виконає ці файли, як команда PHPUnit. Тільки він виконує справи кілька разів і відстежує, скільки часу це займає.

Ми починаємо як зазвичай, встановивши PHPBench:Далі ми створюємо файл, який налаштовує PHPbench:

composer require phpbench/phpbench --dev

{
     "$schema":"./vendor/phpbench/phpbench/phpbench.schema.json",
     "runner.bootstrap": "vendor/autoload.php",
     "runner.path" : "benchmarks"
 }

Опція runner.path визначає директорію, де знаходяться наші бенчмарки, тому ми створюємо phpbench.json для них

mkdir benchmarks

новий каталог:Останнє, що потрібно зробити, це створити бенчмарк. Ми називаємо цей файл ExampleBench.php. Зверніть увагу на Bench суфікс. Використання цього суфікса є важливим, оскільки в іншому випадку PHPBench не виконає файл. В рамках ExampleBench.php, ми додаємо ExampleBench клас:Давайте створимо перший бенчмарк:

class ExampleBench
{
     
}

use PhpBench\Attributes\Iterations;
use PhpBench\Attributes\Revs;

class ExampleBench
{
    #[Revs(500), Iterations(5)]
    public function benchPow()
    {
		pow(2, 10);
    }
}

Як бачите, ми оцінимо, скільки часу потрібно, щоб обчислити 2^10. Контрольний випадок, який ми додали, називається benchPow. Зверніть увагу на bench префікс ключового слова, який необхідний PHPBench для пошуку еталонного відмінка.

Ми також додали до методу два атрибути:

Revs, скорочення від оборотів, відноситься до кількості разів, коли еталон виконується послідовно протягом одного вимірювання часу. Чим більше оборотів, тим точніше результат. У цьому випадку ми обчислимо 2^10 колосальні 500 разів!

В ідеальному світі ми закінчили і можемо почати вимірювати. Але виконувати ці обороти кілька разів безпечніше і правильніше, щоб наші вимірювання були стабільними і не надто відрізнялися.

У нас є п'ять ітерацій по 500 оборотів, так що функція військовополонених буде виконуватися за 5 * 500 = 2500 разів.

Тепер настав час запустити PHPBench:І ми отримуємо наступний результат:

vendor/bin/phpbench run  --report=default

Схоже, 0,038 мкс - найкраще середнє значення,

PHPBench (1.2.9) running benchmarks... #standwithukraine
with configuration file: /Users/ruben/Spatie/laravel-data/phpbench.json
with PHP version 8.2.3, xdebug , opcache 

\ExampleBench

    benchPow................................I4 - Mo0.038μs (±3.33%)

Subjects: 1, Assertions: 0, Failures: 0, Errors: 0
+------+--------------+----------+-----+------+------------+----------+--------------+----------------+
| iter | benchmark    | subject  | set | revs | mem_peak   | time_avg | comp_z_value | comp_deviation |
+------+--------------+----------+-----+------+------------+----------+--------------+----------------+
| 0    | ExampleBench | benchPow |     | 500  | 1,812,376b | 0.036μs  | -1.58σ       | -5.26%         |
| 1    | ExampleBench | benchPow |     | 500  | 1,812,376b | 0.038μs  | +0.00σ       | +0.00%         |
| 2    | ExampleBench | benchPow |     | 500  | 1,812,376b | 0.038μs  | +0.00σ       | +0.00%         |
| 3    | ExampleBench | benchPow |     | 500  | 1,812,376b | 0.038μs  | +0.00σ       | +0.00%         |
| 4    | ExampleBench | benchPow |     | 500  | 1,812,376b | 0.040μs  | +1.58σ       | +5.26%         |
+------+--------------+----------+-----+------+------------+----------+--------------+----------------+

яке ми маємо. Давайте подивимося, що найшвидше: Наш бенчмарк тепер буде виглядати так:Запуск PHPBench дає наступні результати:

Отже, в основному, обчислення двох pow(2, 10) функцій коштує на 50

class ExampleBench
{
    #[Revs(500), Iterations(5)]
    public function benchPow()
    {
        pow(2, 20);
    }

    #[Revs(500), Iterations(5)]
    public function benchPowPow()
    {
        pow(2, 10) * pow(2, 10);
    }
}

PHPBench (1.2.9) running benchmarks... #standwithukraine
with configuration file: /Users/ruben/Spatie/laravel-data/phpbench.json
with PHP version 8.2.3, xdebug , opcache 

\ExampleBench

    benchPow................................I4 - Mo0.039μs (±3.78%)
    benchPowPow.............................I4 - Mo0.060μs (±11.25%)

Subjects: 2, Assertions: 0, Failures: 0, Errors: 0
+------+--------------+-------------+-----+------+------------+----------+--------------+----------------+
| iter | benchmark    | subject     | set | revs | mem_peak   | time_avg | comp_z_value | comp_deviation |
+------+--------------+-------------+-----+------+------------+----------+--------------+----------------+
| 0    | ExampleBench | benchPow    |     | 500  | 1,812,376b | 0.040μs  | +0.27σ       | +1.01%         |
| 1    | ExampleBench | benchPow    |     | 500  | 1,812,376b | 0.042μs  | +1.60σ       | +6.06%         |
| 2    | ExampleBench | benchPow    |     | 500  | 1,812,376b | 0.038μs  | -1.07σ       | -4.04%         |
| 3    | ExampleBench | benchPow    |     | 500  | 1,812,376b | 0.038μs  | -1.07σ       | -4.04%         |
| 4    | ExampleBench | benchPow    |     | 500  | 1,812,376b | 0.040μs  | +0.27σ       | +1.01%         |
| 0    | ExampleBench | benchPowPow |     | 500  | 1,812,376b | 0.076μs  | +1.47σ       | +16.56%        |
| 1    | ExampleBench | benchPowPow |     | 500  | 1,812,376b | 0.072μs  | +0.93σ       | +10.43%        |
| 2    | ExampleBench | benchPowPow |     | 500  | 1,812,376b | 0.060μs  | -0.71σ       | -7.98%         |
| 3    | ExampleBench | benchPowPow |     | 500  | 1,812,376b | 0.060μs  | -0.71σ       | -7.98%         |
| 4    | ExampleBench | benchPowPow |     | 500  | 1,812,376b | 0.058μs  | -0.98σ       | -11.04%        |
+------+--------------+-------------+-----+------+------------+----------+--------------+----------------+

% більше часу, ніж одна pow(2, 20), це досить корисно!

Тепер повернемося до laravel-data, ми хочемо знати, як швидко ми можемо створити об'єкт даних і як швидко ми можемо перетворити його в об'єкт JSON. Ми можемо описати це наступним чином:

class DataBench
{
	#[Revs(500), Iterations(2)]
    public function benchDataCreation()
    {
        MultiNestedData::from([
            'nested' => ['simple' => 'Hello'],
            'nestedCollection' => [
                ['simple' => 'Flare'],
                ['simple' => 'is'],
                ['simple' => 'awesome'],
            ],
        ]);
    }

    #[Revs(500), Iterations(2)]
    public function benchDataTransformation()
    {
        $data = new MultiNestedData(
            new NestedData(new SimpleData('Hello')),
            new DataCollection(NestedData::class, [
                new NestedData(new SimpleData('Flare')),
                new NestedData(new SimpleData('is')),
                new NestedData(new SimpleData('awesome')),
            ])
        );

        $data->toArray();
    }

    #[Revs(500), Iterations(2)]
    public function benchDataCollectionCreation()
    {
        $collection = Collection::times(
            15,
            fn() => [
                'nested' => ['simple' => 'Hello'],
                'nestedCollection' => [
                    ['simple' => 'Flare'],
                    ['simple' => 'is'],
                    ['simple' => 'awesome'],
                ],
            ]
        )->all();

        MultiNestedData::collection($collection);
    }

    #[Revs(500), Iterations(2)]
    public function benchDataCollectionTransformation()
    {
        $collection = Collection::times(
            15,
            fn() => new MultiNestedData(
                new NestedData(new SimpleData('Hello')),
                new DataCollection(NestedData::class, [
                    new NestedData(new SimpleData('Flare')),
                    new NestedData(new SimpleData('is')),
                    new NestedData(new SimpleData('awesome')),
                ])
            )
        )->all();

        $collection = MultiNestedData::collection($collection);

        $collection->toArray();
    }
}

У першому бенчмарку ми створюємо об'єкт даних за допомогою масивів. У другому ми створили об'єкт максимально ефективно, вручну визначивши його, а потім перетворивши на JSON.

Ми також додали два випадки, коли ми робимо те ж саме, але замість того, щоб використовувати об'єкти даних, ми проводимо порівняльний аналіз за допомогою колекцій даних декількох об'єктів даних.

Тепер запустити PHPBench не вдасться, оскільки пакет даних laravel залежить від деякої функціональності Laravel. На щастя, Laravel забезпечує чудову інфраструктуру тестування, яку ми можемо використовувати в рамках нашого бенчмарку. Ми робимо це, використовуючи рису, яка також присутня в базовому тестовому кейсі Laravel:Нам потрібне ще одне оновлення, оскільки ми проводимо порівняльний аналіз пакету ларавелу,

use Tests\CreatesApplication;

class DataBench
{
    use CreatesApplication;

    public function __construct()
    {
        $this->createApplication();
    }
    
    // The benchmarks
}   

а не програми для ларавелуCreatesApplication. Це означає, що ми повинні використовувати рису CreatesApplication з orchestra/testbench упаковки, яка використовується для тестування пакетів Laravel. І нам також потрібно вказати постачальника послуг laravel-data для завантаження пакета:

use Orchestra\Testbench\Concerns\CreatesApplication;

class DataBench
{
    use CreatesApplication;

    public function __construct()
    {
        $this->createApplication();
    }

    protected function getPackageProviders($app)
    {
        return [
            LaravelDataServiceProvider::class,
        ];
    }
    
    // The benchmarks
} 

Чудово, тепер у нас є деякі контрольні показники, готові виміряти швидкість нашого коду!

Getting started with Xdebug

Далі нам потрібно профілювати наш код. Коли ми профілюємо наш код, ми запустимо його трохи інакше, ніж зазвичай. Під час профілювання процес PHP буде записувати кожен виклик функції, який ми робимо, а також відстежувати, скільки часу потрібно для запуску функції. Зрештою, вся ця інформація буде записана у файл, який ми можемо проаналізувати.

Щоб це запрацювало, нам потрібно встановити та ввімкнути Xdebug. Ви можете знайти інструкцію тут.

Для мене на Mac це було так само просто, як це:Нам також потрібно оновити або створити файл Xdebug .ini:На моєму Mac я помістив цей файл тут:

sudo pecl install xdebug

[xdebug]

xdebug.mode=profile
xdebug.start_with_request=yes

nano /opt/homebrew/etc/php/8.2/conf.d/xdebug.ini .

Налаштовуємо два варіанти:

  • xdebug.mode=profile включити профілювання в Xdebug. щоб почати профілювання за допомогою PHPBench.
  • xdebug.start_with_request=yes Коли ви закінчите з профілюванням, не забудьте встановити це на off або trigger. В іншому випадку завантаження всіх ваших програм PHP займе багато часу.

У нас є свої орієнтири, і у нас є свій профайлер. Тепер нам потрібно лише поєднати ці два, і тоді ми зможемо з'ясувати, чому наш код такий повільний!

Profiling the code

Ми повторимо наші бенчмарки, але цього разу ми також будемо профілювати орієнтири. Ми можемо зробити це за допомогою наступної команди:Яка дає наступний результат:

vendor/bin/phpbench xdebug:profile

PHPBench (1.2.9) running benchmarks... #standwithukraine
with configuration file: /Users/ruben/Spatie/laravel-data/phpbench.json
with PHP version 8.2.3, xdebug , opcache 

\DataBench

    benchDataCreation.......................I0 - Mo10.762ms (±0.00%)
    benchDataTransformation.................I0 - Mo1.095ms (±0.00%)
    benchDataCollectionCreation.............I0 - Mo159.879ms (±0.00%)
    benchDataCollectionTransformation.......I0 - Mo13.952ms (±0.00%)

Subjects: 4, Assertions: 0, Failures: 0, Errors: 0

4 profile(s) generated:

/Users/ruben/Spatie/laravel-data/.phpbench/xdebug-profile/3fcb020036c8d7e8efdcddd4dbd66b92.cachegrind.gz
/Users/ruben/Spatie/laravel-data/.phpbench/xdebug-profile/8deba1dcce573e1bf818772ac7a5ace0.cachegrind.gz
/Users/ruben/Spatie/laravel-data/.phpbench/xdebug-profile/06d049dacb2a7a3809e069c6c8289e02.cachegrind.gz
/Users/ruben/Spatie/laravel-data/.phpbench/xdebug-profile/41c9fe618b93431786fff90b1d624b82.cachegrind.gz

Запуск набору бенчмарків тепер займає набагато більше часу, ніж раніше. Зрештою, у нас з'явився новий каталог з нашими профілями:

  • .phpbench/xdebug-profile/3fcb020036c8d7e8efdcddd4dbd66b92.cachegrind.gz
  • .phpbench/xdebug-profile/41c9fe618b93431786fff90b1d624b82.cachegrind.gz

Кожен файл відповідає еталонному випадку у вашому наборі бенчмарків. Найкращий спосіб дізнатися, який файл належить до якого бенчмарку, - це подивитися на порядок виконання бенчмаркових справ і зіставити їх з файлами в тому порядку, в якому вони були створені.

На даний момент всі файли стиснуті. Давайте розпакуємо їх:Нарешті настав час з'ясувати,

gzip --decompress .phpbench/xdebug-profile/*

Analyzing

чому наш код повільний. Це також найскладніший етап процесу, і немає керівництва для швидкого пошуку вузького місця. Ви повинні добре розуміти свій код і мати можливість бачити дивну поведінку в профілях. Деякі речі, які можуть виділятися:функції, які не повинні виконуватися

  • функціями, які виконуються занадто багато
  • функцій, які займають більше часу, ніж очікувалося
  • Давайте відкриємо профіль, де ми створюємо колекцію об'єктів даних:

По-перше, ми збираємося налаштувати QCacheGrind трохи більше. Перейдіть до:

QCacheGrind -> Preferences -> Source Annotations

І додайте шлях, де знаходиться ваш код. Це дозволяє QCacheGrind давати деякі підказки з прикладами коду, який код повільний.

Додаток має три панелі:Зліва: функції, які були названі відсортованими за найтривалішим викликом

  • Праворуч зверху: абоненти вибраної функції Праворуч-внизу:
  • для вибраної функції
  • функціос називається

З лівого боку ми бачимо всі функції, що викликаються. Показник Self показує, скільки часу знадобилося для виклику функції, метрика Incl. показує, скільки часу зайняла функція і всі функції, які вона називала. Так, наприклад, функція PHPBench займає 99.93% часу Incl., але вона проводить лише 0.01% часу в межах функції. Весь інший час використовується для виклику інших функцій, наших бенчмарків зі створенням даних.

Відразу бачимо щось химерне. Ми проводимо 24,34% часу в Illuminate\Container\Container->resolve(). Давайте натиснемо на цю функцію і подивимося, що відбувається:

Ця функція була викликана Illuminate\Foundation\Application->resolve(). Давайте натиснемо на цей. Ми бачимо Illuminate\Container\Container->make() як функцію, зазвичай викликає resolve функцію. Ми продовжуємо натискати на найбільш звані функції, поки не введемо helpers.php файл:З цих даних ми бачимо, що ми багато вирішуємо з контейнера Laravel:

  • Data::from 67500 елементів -> 225000 елементівDataPipeline in a closure (line 75)
  • DataPipeline in a closure (line 69) -> 187500 предметів

Що саме тут відбувається? Давайте подивимося на DataPipeline код:

public function execute(): Collection
{
    /** @var \Spatie\LaravelData\Normalizers\Normalizer[] $normalizers */
    $normalizers = array_map(
        fn (string|Normalizer $normalizer) => is_string($normalizer) ? app($normalizer) : $normalizer,
        $this->normalizers
    );

    /** @var \Spatie\LaravelData\DataPipes\DataPipe[] $pipes */
    $pipes = array_map(
        fn (string|DataPipe $pipe) => is_string($pipe) ? app($pipe) : $pipe,
        $this->pipes
    );
    
    // Other code, hidden to keep things a bit easier to read
}

Цей код не виглядає як проблема продуктивності. Так, ми створюємо ці нормалізатори та труби з контейнера, але поки цей метод не виконується стільки разів, що в кінцевому підсумку не виникне проблем. Цей процес має десь відбутися.

Давайте дізнаємося, звідки називається цей код:

public function execute(string $class, mixed ...$payloads): BaseData
{
    $properties = new Collection();

    foreach ($payloads as $payload) {
        /** @var BaseData $class */
        $pipeline = $class::pipeline();

        foreach ($class::normalizers() as $normalizer) {
            $pipeline->normalizer($normalizer);
        }

        foreach ($pipeline->using($payload)->execute() as $key => $value) {
            $properties[$key] = $value;
        }
    }
    
    // Other code, hidden to keep things a bit easier to read
}

Це може бути краще. Ми створюємо конвеєр для кожного об'єкта даних, який ми будуємо, і, таким чином, ми щоразу вирішуємо всі труби та нормалізатори з контейнера. Щоразу це не потрібно, оскільки трубопровід статично побудований для об'єкта даних і, таким чином, однаковий для всіх об'єктів.

Для кількох об'єктів це буде нормально. Але оскільки у нас є колекції одних і тих же об'єктів (іноді більше 500), ми потрапили в величезну проблему продуктивності.

Тепер кешуємо ці конвеєри у версії 3.2 laravel-data і додаємо деякі інші поліпшення продуктивності. Це повинно багато зробити для нашого виступу! Нарешті, давайте повторимо бенчмарки.

Перед оптимізацією продуктивності:Після оптимізації продуктивності:

benchData...............................I1 - Mo293.376μs (±0.79%)
benchDataCollection.....................I1 - Mo5.619ms (±0.48%)

benchData...............................I1 - Mo125.731μs (±0.57%)
benchDataCollection.....................I1 - Mo2.111ms (±0.82%)

Для простих об'єктів даних ми покращили продуктивність на 57%, а для колекцій даних – на 62%. Це величезне!

In the end

За кілька годин роботи ми різко зменшили штраф за продуктивність на Flare. Профілювання найважливіших частин вашої заявки може бути цінним, але це не так просто!

Ми продовжуємо редизайн та рефакторінг Flare протягом наступних тижнів і сподіваємось на запуск найближчим часом! Попередній перегляд нашого нового дизайну ви можете знайти тут. Ви зацікавлені в бета-тестуванні нової версії Flare? Будь ласка, надішліть нам електронний лист на адресу [захищено електронною поштою]

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Про автора CrazyBoy49z
WORK EXPERIENCE
Контакти
Ukraine, Lutsk
+380979856297