/
cloud-castle
/
memcached
Обзор
Документация
Войти
/
cloud-castle
/
memcached
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
9
CI/CD
Аналитика
main
benchmarks/compare.php
270 строк
10 KB
Алексей Зорин
build: сравнительные замеры и автогенерация таблиц
27 июл 2026, 15:04
27 июл 2026, 15:04
5fb327d
Код
Авторство
О чём код?
<?php declare(strict_types=1); /** * Сравнительный прогон cloud-castle/memcached против клиентов Memcached * на ОДИНАКОВОЙ операции: записать значение и прочитать его назад. * * Все участники работают с одним и тем же сервером, поэтому сравнивается * именно накладной расход клиента, а не скорость сети. `ext-memcached` * помечен базовым уровнем: это расширение на C, оно не является * composer-библиотекой и показано для контекста. * * Память измеряется в ИЗОЛЯЦИИ: при `--memory <target>` в процесс попадает * только целевая библиотека. Пишет var/comparison/results.json. * * @internal инструмент разработки */ require __DIR__ . '/../vendor/autoload.php'; const ITERATIONS = 20000; const RUNS = 3; /** @var array<string, callable(): callable(): mixed> $makeOperation */ $makeOperation = require __DIR__ . '/competitors.php'; $memoryTarget = null; $leakTarget = null; foreach ($argv as $index => $arg) { if ($arg === '--memory' && isset($argv[$index + 1])) { $memoryTarget = $argv[$index + 1]; } if ($arg === '--leak' && isset($argv[$index + 1])) { $leakTarget = $argv[$index + 1]; } } if ($memoryTarget !== null) { $make = $makeOperation[$memoryTarget] ?? null; if ($make === null) { fwrite(STDERR, "Неизвестный участник: {$memoryTarget}\n"); exit(1); } // Память ИМЕННО библиотеки, а не процесса: baseline снимается до создания // клиента (PHP, автолоадер и скрипт уже загружены и вычитаются). gc_collect_cycles(); if (\function_exists('memory_reset_peak_usage')) { memory_reset_peak_usage(); } $baseUsage = memory_get_usage(false); $operation = $make(); for ($i = 0; $i < 2000; $i++) { $operation(); } $library = (\function_exists('memory_reset_peak_usage') ? memory_get_peak_usage(false) : memory_get_usage(false)) - $baseUsage; echo max(0, (int) round($library / 1024)); exit(0); } if ($leakTarget !== null) { $make = $makeOperation[$leakTarget] ?? null; if ($make === null) { fwrite(STDERR, "Неизвестный участник: {$leakTarget}\n"); exit(1); } // Утечки: прогрев, замер, десять повторных циклов, повторный замер. $operation = $make(); for ($i = 0; $i < 2000; $i++) { $operation(); } gc_collect_cycles(); $before = memory_get_usage(); for ($cycle = 0; $cycle < 10; $cycle++) { for ($i = 0; $i < 2000; $i++) { $operation(); } } gc_collect_cycles(); echo (int) round((memory_get_usage() - $before) / 1024); exit(0); } /** @var array<string, callable(): mixed> $operations */ $operations = []; foreach ($makeOperation as $name => $make) { try { $operations[$name] = $make(); } catch (Throwable $error) { fwrite(STDERR, sprintf("Участник %s недоступен: %s\n", $name, $error->getMessage())); } } foreach ($operations as $name => $operation) { if ($operation() !== 'value') { fwrite(STDERR, "ВНИМАНИЕ: {$name} не вернул значение при round-trip\n"); } } $performance = []; $memory = []; $leaks = []; foreach ($operations as $name => $operation) { $operation(); $best = INF; for ($run = 0; $run < RUNS; $run++) { $start = hrtime(true); for ($i = 0; $i < ITERATIONS; $i++) { $operation(); } $best = min($best, (float) (hrtime(true) - $start) / 1e6); } $performance[$name] = round($best, 1); $command = sprintf('%s %s --memory %s', escapeshellarg(PHP_BINARY), escapeshellarg(__FILE__), escapeshellarg($name)); $memory[$name] = (int) trim((string) shell_exec($command)); $command = sprintf('%s %s --leak %s', escapeshellarg(PHP_BINARY), escapeshellarg(__FILE__), escapeshellarg($name)); $leaks[$name] = (int) trim((string) shell_exec($command)); } /** @var array<string, string> $sourceDirs */ $sourceDirs = [ 'cloud-castle/memcached' => __DIR__ . '/../src', 'symfony/cache' => __DIR__ . '/../vendor/symfony/cache', 'illuminate/cache' => __DIR__ . '/../vendor/illuminate/cache', 'matthiasmullie/scrapbook' => __DIR__ . '/../vendor/matthiasmullie/scrapbook/src', 'phpfastcache/phpfastcache' => __DIR__ . '/../vendor/phpfastcache/phpfastcache/lib/Phpfastcache', 'tedivm/stash' => __DIR__ . '/../vendor/tedivm/stash/src', 'laminas/laminas-cache' => __DIR__ . '/../vendor/laminas/laminas-cache/src', ]; $composerJsons = [ 'cloud-castle/memcached' => __DIR__ . '/../composer.json', 'symfony/cache' => __DIR__ . '/../vendor/symfony/cache/composer.json', 'illuminate/cache' => __DIR__ . '/../vendor/illuminate/cache/composer.json', 'matthiasmullie/scrapbook' => __DIR__ . '/../vendor/matthiasmullie/scrapbook/composer.json', 'phpfastcache/phpfastcache' => __DIR__ . '/../vendor/phpfastcache/phpfastcache/composer.json', 'tedivm/stash' => __DIR__ . '/../vendor/tedivm/stash/composer.json', 'laminas/laminas-cache' => __DIR__ . '/../vendor/laminas/laminas-cache/composer.json', ]; $quality = []; foreach ($sourceDirs as $name => $dir) { if (!is_dir($dir)) { continue; } $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)); $phpFiles = 0; $strictFiles = 0; $classFiles = 0; $finalFiles = 0; /** @var SplFileInfo $file */ foreach ($files as $file) { if ($file->getExtension() !== 'php' || str_contains($file->getPathname(), '/Tests/')) { continue; } $phpFiles++; $source = (string) file_get_contents($file->getPathname()); if (preg_match('/declare\s*\(\s*strict_types\s*=\s*1\s*\)/', $source) === 1) { $strictFiles++; } if (preg_match('/^(?:final\s+|abstract\s+)?(?:readonly\s+)?class\s/m', $source) === 1) { $classFiles++; if (preg_match('/^final\s/m', $source) === 1) { $finalFiles++; } } } $lint = []; $lintExit = 0; exec(sprintf( '%s %s --no-colors %s 2>&1', escapeshellarg(PHP_BINARY), escapeshellarg(__DIR__ . '/../vendor/bin/parallel-lint'), escapeshellarg($dir), ), $lint, $lintExit); /** @var array{require?: array<string, string>} $composer */ $composer = json_decode((string) file_get_contents($composerJsons[$name]), true); $require = $composer['require'] ?? []; $runtimeDeps = count(array_filter( array_keys($require), static fn (string $dep): bool => $dep !== 'php' && !str_starts_with($dep, 'ext-'), )); $quality[$name] = [ 'lint_errors' => $lintExit === 0 ? 0 : max(1, count($lint) - 1), 'strict_percent' => $phpFiles === 0 ? 0 : (int) round(100 * $strictFiles / $phpFiles), 'final_percent' => $classFiles === 0 ? 0 : (int) round(100 * $finalFiles / $classFiles), 'runtime_deps' => $runtimeDeps, 'php_constraint' => $require['php'] ?? '—', 'files' => $phpFiles, ]; } $result = require __DIR__ . '/matrix.php'; $result['generated_at'] = gmdate('c'); $result['php_version'] = PHP_VERSION; $result['competitors'] = array_keys($operations); $result['performance'] = [ 'operation' => 'set + get на общем сервере Memcached, ' . number_format(ITERATIONS, 0, '.', ' ') . ' раз (минимум из ' . RUNS . ')', 'unit' => 'мс', 'results' => $performance, ]; $result['memory'] = [ 'operation' => 'Память самой библиотеки (классы + структуры данных): пик рабочей фазы ' . 'минус baseline, снятый до создания клиента в изолированном процессе, — ' . 'стоимость PHP и автолоадера вычтена', 'unit' => 'KB', 'results' => $memory, ]; $result['leaks'] = [ 'operation' => 'Рост памяти за 20 000 операций после прогрева и gc_collect_cycles ' . '(изолированный процесс, только целевая библиотека; 0 — утечек нет)', 'unit' => 'KB', 'results' => $leaks, ]; $result['quality'] = [ 'metrics' => [ 'lint_errors' => ['label' => 'Синтаксические ошибки (phplint)', 'best' => 'min'], 'strict_percent' => ['label' => 'Файлы со strict_types, %', 'best' => 'max'], 'final_percent' => ['label' => 'final-классы, %', 'best' => 'max'], 'runtime_deps' => ['label' => 'Runtime-зависимостей', 'best' => 'min'], 'files' => ['label' => 'Файлов исходников', 'best' => 'min'], ], 'php_constraints' => array_combine( array_keys($quality), array_map(static fn (array $row): string => (string) $row['php_constraint'], $quality), ), 'results' => $quality, ]; @mkdir(__DIR__ . '/../var/comparison', 0777, true); file_put_contents( __DIR__ . '/../var/comparison/results.json', json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ); printf("Сравнение записано: var/comparison/results.json (%d участников)\n", count($operations));