feat: add performance benchmark script
Déploiement PROD / check (push) Failing after 3m36s
Déploiement PROD / deploy (push) Has been skipped

This commit is contained in:
2026-07-27 09:16:05 +04:00
parent d0f9ee4e6b
commit 745726dad7
2 changed files with 298 additions and 18 deletions
+38 -18
View File
@@ -182,9 +182,6 @@ Toutes ces sources sont modifiables dans `includes/config.local.php` (voir <<fr-
│ ├── sidebar.php # Navigation latérale
│ ├── mobile-menu.php # Menu mobile coulissant
│ ├── hero-section.php # Bannière d'accueil (live/vidéo/playlist)
│ ├── featured-videos.php # ⚠️ legacy, non utilisé par les pages actuelles
│ ├── recent-videos.php # ⚠️ legacy, non utilisé par les pages actuelles
│ ├── pwa-init.php # ⚠️ legacy, PWA désormais inline dans index.php
│ └── lib/
│ └── markdown.php # Rendu Markdown (sous-ensemble PeerTube)
├── js/
@@ -197,6 +194,7 @@ Toutes ces sources sont modifiables dans `includes/config.local.php` (voir <<fr-
│ ├── pleroma-adapter.js # Adaptateur Pleroma → API Mastodon
│ └── pwa-update.js # Enregistrement SW + modal de mise à jour
├── scripts/
│ ├── benchmark.php # Benchmark TTFB froid/chaud des pages clés
│ ├── check.sh # Vérifications qualité en local (avant push)
│ └── warm-cache.php # Préchauffage du cache API (cron)
├── uploads/ # Images d'annonces (non versionné, hors .gitkeep)
@@ -633,10 +631,6 @@ TIP: Si vous déployez une instance indépendante, remplacez `LIBERAPAY_URL` par
|===
| Constante | Défaut | Description
| `TAG_INDEPENDENCE`
| `'indépendance'`
| Tag de la fonction `getIndependenceVideos()` (pas de section d'accueil associée actuellement)
| `SHORTS_MAX_DURATION`
| `180`
| Durée max d'un short (s) ; ratio portrait (`aspectRatio ≤ 1`) aussi requis
@@ -675,9 +669,9 @@ TIP: Si vous déployez une instance indépendante, remplacez `LIBERAPAY_URL` par
| `6`
| Vidéos chargées par clic sur « Voir plus »
| `FEATURED_VIDEOS_COUNT` / `INDEPENDENCE_VIDEOS_COUNT`
| `FEATURED_VIDEOS_COUNT`
| `6`
| Réservés (sections non affichées actuellement)
| Réservé (section non affichée actuellement)
|===
.Cache
@@ -970,6 +964,22 @@ python3 -m venv /tmp/test-venv
NOTE: Les tests E2E démarrent un serveur PHP local (`php -S`) et s'appuient sur la configuration par défaut. Les appels réseau vers PeerTube sont sautés proprement si l'instance est injoignable.
==== ⏱️ Benchmark de performance
Le script `scripts/benchmark.php` mesure le TTFB (_time to first byte_) des pages clés — accueil (`/`), page vidéo (`/video.php?id=…`) et page catégorie (`/categories.php?id=…`) — dans deux conditions :
- *à froid* : cache API vidé (le premier visiteur paie le coût des appels API externes) ;
- *à chaud* : cache API rempli (moyenne sur 3 requêtes).
Il démarre un serveur PHP local (`php -S`), découvre automatiquement une vidéo et une catégorie réelles via l'API PeerTube (les pages correspondantes sont ignorées si l'instance est injoignable) et affiche la taille du cache API avant et après les mesures.
[source,bash]
----
php scripts/benchmark.php
----
NOTE: Le script vide le cache API (`cache/api`) : à utiliser hors production, ou suivi d'un préchauffage (`scripts/warm-cache.php`).
=== 📜 Licence
Copyright (C) 2025 Cédric Famibelle-Pronzola & *ORGANISATION KA INTERNATIONALE*
@@ -1155,9 +1165,6 @@ All of these sources can be changed in `includes/config.local.php` (see <<en-con
│ ├── sidebar.php # Side navigation
│ ├── mobile-menu.php # Slide-in mobile menu
│ ├── hero-section.php # Homepage banner (live/video/playlist)
│ ├── featured-videos.php # ⚠️ legacy, unused by current pages
│ ├── recent-videos.php # ⚠️ legacy, unused by current pages
│ ├── pwa-init.php # ⚠️ legacy, PWA now inlined in index.php
│ └── lib/
│ └── markdown.php # Markdown renderer (PeerTube-flavored subset)
├── js/
@@ -1170,6 +1177,7 @@ All of these sources can be changed in `includes/config.local.php` (see <<en-con
│ ├── pleroma-adapter.js # Pleroma → Mastodon API adapter
│ └── pwa-update.js # SW registration + update modal
├── scripts/
│ ├── benchmark.php # Cold/warm TTFB benchmark of key pages
│ ├── check.sh # Local quality checks (before pushing)
│ └── warm-cache.php # API cache warm-up (cron)
├── uploads/ # Announcement images (not versioned, except .gitkeep)
@@ -1606,10 +1614,6 @@ TIP: If you deploy an independent instance, replace `LIBERAPAY_URL` with your ow
|===
| Constant | Default | Description
| `TAG_INDEPENDENCE`
| `'indépendance'`
| Tag used by `getIndependenceVideos()` (no associated homepage section currently)
| `SHORTS_MAX_DURATION`
| `180`
| Max short duration (s); portrait ratio (`aspectRatio ≤ 1`) also required
@@ -1648,9 +1652,9 @@ TIP: If you deploy an independent instance, replace `LIBERAPAY_URL` with your ow
| `6`
| Videos loaded per "Load more" click
| `FEATURED_VIDEOS_COUNT` / `INDEPENDENCE_VIDEOS_COUNT`
| `FEATURED_VIDEOS_COUNT`
| `6`
| Reserved (sections not currently displayed)
| Reserved (section not currently displayed)
|===
.Cache
@@ -1943,6 +1947,22 @@ python3 -m venv /tmp/test-venv
NOTE: E2E tests start a local PHP server (`php -S`) and rely on the default configuration. Network calls to PeerTube are skipped gracefully if the instance is unreachable.
==== ⏱️ Performance benchmark
The `scripts/benchmark.php` script measures the TTFB (_time to first byte_) of key pages — homepage (`/`), video page (`/video.php?id=…`) and category page (`/categories.php?id=…`) — under two conditions:
- *cold*: API cache emptied (the first visitor pays the cost of external API calls);
- *warm*: API cache filled (average over 3 requests).
It starts a local PHP server (`php -S`), automatically discovers a real video and category through the PeerTube API (the corresponding pages are skipped if the instance is unreachable) and reports the API cache size before and after the measurements.
[source,bash]
----
php scripts/benchmark.php
----
NOTE: The script purges the API cache (`cache/api`): use it outside production, or follow it with a cache warmup (`scripts/warm-cache.php`).
=== 📜 License
Copyright (C) 2025 Cédric Famibelle-Pronzola & *ORGANISATION KA INTERNATIONALE*
+260
View File
@@ -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";