Files
annu-kute-ced/scripts/benchmark.php
T
cedric 745726dad7
Déploiement PROD / check (push) Failing after 3m36s
Déploiement PROD / deploy (push) Has been skipped
feat: add performance benchmark script
2026-07-27 09:16:05 +04:00

261 lines
8.6 KiB
PHP

<?php
/**
* Mesure les performances des pages clés du site (TTFB à froid / à chaud).
*
* Démarre un serveur PHP local (php -S) à la racine du projet, vide le cache
* API (cache/api), puis mesure le TTFB (time to first byte) des pages clés :
* - à froid : cache vide (le premier visiteur paie les appels API externes) ;
* - à chaud : cache rempli (moyenne sur plusieurs requêtes).
* La taille du cache API est affichée avant et après les mesures.
*
* Pages mesurées : accueil (/), page vidéo (/video.php?id=…) et page
* catégorie (/categories.php?id=…). Les identifiants de vidéo et de catégorie
* sont découverts automatiquement via l'API PeerTube ; si l'instance est
* injoignable, les pages correspondantes sont ignorées avec un avertissement.
*
* ⚠️ Le script vide le cache API (cache/api) : à utiliser hors production,
* ou suivi d'un préchauffage (scripts/warm-cache.php).
*
* Usage :
* php scripts/benchmark.php
*/
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit("Ce script ne s'exécute qu'en ligne de commande.\n");
}
const WARM_RUNS = 3; // Nombre de requêtes à chaud par page (moyenne)
const REQUEST_TIMEOUT = 120; // Secondes (à froid, l'accueil paie tous les appels API)
const STARTUP_TIMEOUT = 15; // Délai d'attente du démarrage de php -S
$rootDir = dirname(__DIR__);
$cacheDir = $rootDir . '/cache/api';
require_once $rootDir . '/includes/config.php';
// Arrêt du serveur local quoi qu'il arrive (exit, erreur fatale…)
$serverProcess = null;
$serverLogFile = null;
register_shutdown_function(function () use (&$serverProcess, &$serverLogFile) {
if (is_resource($serverProcess)) {
proc_terminate($serverProcess);
proc_close($serverProcess);
}
if ($serverLogFile !== null) {
@unlink($serverLogFile);
}
});
/**
* Trouve un port TCP libre sur 127.0.0.1
*
* @return int Port libre
*/
function findFreePort() {
$sock = @stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
if ($sock === false) {
return 8000;
}
$name = stream_socket_get_name($sock, false);
fclose($sock);
return (int) substr($name, strrpos($name, ':') + 1);
}
/**
* Vide le cache API
*
* @param string $cacheDir Répertoire du cache
* @return int Nombre de fichiers supprimés
*/
function purgeApiCache($cacheDir) {
$deleted = 0;
foreach (glob($cacheDir . '/cache_*.json') ?: [] as $file) {
if (@unlink($file)) {
$deleted++;
}
}
return $deleted;
}
/**
* Statistiques du cache API
*
* @param string $cacheDir Répertoire du cache
* @return array ['files' => nombre de fichiers, 'bytes' => taille totale en octets]
*/
function getCacheStats($cacheDir) {
$files = glob($cacheDir . '/cache_*.json') ?: [];
$bytes = 0;
foreach ($files as $file) {
$bytes += (int) filesize($file);
}
return ['files' => count($files), 'bytes' => $bytes];
}
/**
* Formate une taille en octets de façon lisible
*
* @param int $bytes Taille en octets
* @return string Taille formatée (o, Ko ou Mo)
*/
function formatBytes($bytes) {
if ($bytes >= 1048576) {
return round($bytes / 1048576, 2) . ' Mo';
}
if ($bytes >= 1024) {
return round($bytes / 1024, 1) . ' Ko';
}
return $bytes . ' o';
}
/**
* Aligne une chaîne à droite sur une largeur donnée (compatible UTF-8)
*
* @param string $str Chaîne à compléter
* @param int $len Largeur cible
* @return string Chaîne complétée d'espaces
*/
function padRight($str, $len) {
$width = function_exists('mb_strwidth') ? mb_strwidth($str) : strlen($str);
return $str . str_repeat(' ', max(0, $len - $width));
}
/**
* Démarre un serveur PHP local (php -S) sur la racine du projet
*
* @param string $rootDir Racine du projet (document root)
* @param int $port Port d'écoute
* @return array [resource $process, string $logFile]
*/
function startPhpServer($rootDir, $port) {
$logFile = tempnam(sys_get_temp_dir(), 'benchmark-php-server-');
$process = proc_open(
['php', '-S', '127.0.0.1:' . $port, '-t', $rootDir],
[
0 => ['file', '/dev/null', 'r'],
1 => ['file', $logFile, 'w'],
2 => ['file', $logFile, 'a'],
],
$pipes,
$rootDir
);
if ($process === false) {
exit("❌ Impossible de démarrer php -S\n");
}
// Attendre que le serveur accepte des connexions
$deadline = microtime(true) + STARTUP_TIMEOUT;
while (microtime(true) < $deadline) {
$status = proc_get_status($process);
if (!$status['running']) {
echo "❌ Le serveur PHP s'est arrêté au démarrage :\n" . (string) @file_get_contents($logFile);
exit(1);
}
$fp = @fsockopen('127.0.0.1', $port, $errno, $errstr, 0.5);
if ($fp !== false) {
fclose($fp);
return [$process, $logFile];
}
usleep(200000);
}
echo "❌ Le serveur PHP ne répond pas après " . STARTUP_TIMEOUT . " s\n";
exit(1);
}
/**
* Mesure le TTFB et le temps total d'une page via HTTP
*
* @param string $url URL complète de la page
* @return array ['code' => int, 'ttfb' => float, 'total' => float] (secondes)
*/
function measurePage($url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => REQUEST_TIMEOUT,
]);
curl_exec($ch);
$result = [
'code' => (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE),
'ttfb' => curl_getinfo($ch, CURLINFO_STARTTRANSFER_TIME),
'total' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
];
curl_close($ch);
return $result;
}
/**
* Calcule le TTFB moyen d'une série de mesures
*
* @param array $measures Mesures retournées par measurePage()
* @return float TTFB moyen en secondes
*/
function averageTtfb($measures) {
return array_sum(array_column($measures, 'ttfb')) / count($measures);
}
// --- Découverte des identifiants réels via l'API PeerTube ---
echo "🔎 Découverte d'une vidéo et d'une catégorie via l'API PeerTube…\n";
$pages = ['Accueil (/)' => '/index.php'];
$recentVideos = getRecentVideos(1);
if (!empty($recentVideos)) {
$pages['Vidéo (/video.php?id=…)'] = '/video.php?id=' . urlencode($recentVideos[0]['id']);
} else {
echo "⚠️ Aucune vidéo trouvée (instance PeerTube injoignable ?) : page vidéo ignorée\n";
}
$displayCategories = getDisplayCategories();
if (!empty($displayCategories)) {
$pages['Catégorie (/categories.php?id=…)'] = '/categories.php?id=' . urlencode($displayCategories[0]['id']);
} else {
echo "⚠️ Aucune catégorie trouvée : page catégorie ignorée\n";
}
// --- Préparation : état du cache, purge et démarrage du serveur ---
$initialStats = getCacheStats($cacheDir);
$purged = purgeApiCache($cacheDir);
$port = findFreePort();
echo "🚀 Démarrage de php -S sur 127.0.0.1:{$port}\n";
[$serverProcess, $serverLogFile] = startPhpServer($rootDir, $port);
$baseUrl = 'http://127.0.0.1:' . $port;
// --- Mesure à froid (cache API vide) ---
echo "\n📏 Mesure à froid (cache API vidé, {$purged} fichier(s) supprimé(s))…\n";
$results = [];
foreach ($pages as $label => $path) {
$cold = measurePage($baseUrl . $path);
$results[$label] = ['path' => $path, 'cold' => $cold, 'warm' => []];
printf(" - %s HTTP %d TTFB %7.0f ms\n", padRight($label, 34), $cold['code'], $cold['ttfb'] * 1000);
}
// --- Mesure à chaud (cache API rempli) ---
echo "\n📏 Mesure à chaud (" . WARM_RUNS . " requêtes par page, moyenne)…\n";
foreach ($results as $label => &$result) {
for ($i = 0; $i < WARM_RUNS; $i++) {
$result['warm'][] = measurePage($baseUrl . $result['path']);
}
printf(" - %s HTTP %d TTFB %7.0f ms\n", padRight($label, 34), $result['warm'][0]['code'], averageTtfb($result['warm']) * 1000);
}
unset($result);
$finalStats = getCacheStats($cacheDir);
// --- Résumé ---
echo "\n===== Résumé =====\n";
printf("%s %14s %14s %8s\n", padRight('Page', 34), 'TTFB froid', 'TTFB chaud', 'Gain');
foreach ($results as $label => $result) {
$coldTtfb = $result['cold']['ttfb'];
$warmTtfb = averageTtfb($result['warm']);
$gain = $coldTtfb > 0 ? (int) round((1 - $warmTtfb / $coldTtfb) * 100) : 0;
printf("%s %12.0f ms %12.0f ms %7d %%\n", padRight($label, 34), $coldTtfb * 1000, $warmTtfb * 1000, $gain);
}
echo "\nCache API au départ : {$initialStats['files']} fichier(s), " . formatBytes($initialStats['bytes']) . "\n";
echo "Cache API après mesures : {$finalStats['files']} fichier(s), " . formatBytes($finalStats['bytes']) . "\n";