feat: add performance benchmark script
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
<?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";
|
||||
Reference in New Issue
Block a user