Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ APP_NAME=edu-framework
APP_ENV=prod
## >> Config application

## << Config profiler de code (barre de debug, mode dev uniquement)
## Nécessite l'extension PHP Excimer (installée dans l'image Docker)
## off -> profiler désactivé
## trigger -> profilage à la demande : bouton "Code" de la barre de debug
## (cookie edu_profile=1) ou paramètre ?_profile=1 dans l'URL
## on -> chaque requête est profilée
EDU_PROFILER=trigger
## Nombre de profils conservés dans la base des logs
EDU_PROFILER_KEEP=50
## Image Docker : 1 => avec Excimer (défaut), 0 => sans profiler
EDU_PROFILER_BUILD=1
## >> Config profiler de code

## << Config Mailer
MAILER_DSN="smtp://localhost:1025"
## >> Config Mailer
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, dom, filter, gd, iconv, json, mbstring, pdo, curl
extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, dom, filter, gd, iconv, json, mbstring, pdo, curl, excimer
env:
update: true

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

**Présentation des versions du framework Edu Framework**

## Prochaine version (non publiée)

**new features**

- Profiler de code dans la barre de debug : le temps passé dans chaque fonction PHP d'une requête HTTP est mesuré par l'extension [Excimer](https://www.mediawiki.org/wiki/Excimer) (profiler par échantillonnage toutes les 0,1 ms, compatible FrankenPHP en mode classique et worker). Le profilage est **à la demande** (`EDU_PROFILER=trigger` par défaut) : le nouveau bloc **Code** de la barre de debug pose le cookie `edu_profile=1` (ou paramètre `?_profile=1`) et reste actif jusqu'au clic sur **Désactiver** ; `EDU_PROFILER=on` profile chaque requête, `off` désactive le profiler. Le profil est rattaché au token de la requête et stocké compressé dans la table `serv_code_profiles` (`EDU_PROFILER_KEEP` profils conservés, 50 par défaut). Le bloc affiche le temps profilé et le top 3 des fonctions ; le nouvel onglet **Code** du profiler (`/_debug/profiler#code/{token}`) liste toutes les fonctions (temps propre / inclusif, filtre par origine `app` / `framework` / `vendor`), leurs appelants et appelés, et exporte le profil au format Speedscope pour un flamegraph. Nouvel endpoint interne `GET /edu-code-profile` (mode dev uniquement). L'image Docker installe Excimer (argument de build `EDU_PROFILER`, `EDU_PROFILER_BUILD=0` pour l'exclure) et `php bin/edu check:config` signale l'extension comme recommandée

**documentation**

- update [La barre de debug et le profiler](docs/boost/debug-bar.md) : nouvelle section « Le profiler de code » (installation, modes, lecture du profil, API)

## v2.7.3 - 17/10/2026

**new features**
Expand Down
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ RUN install-php-extensions \
intl \
opcache

# Profiler de code de la barre de debug (Excimer, profiler par échantillonnage)
# Installé uniquement pour l'image de développement :
# docker compose build --build-arg EDU_PROFILER=0 => image sans profiler
# Doc : https://www.mediawiki.org/wiki/Excimer
ARG EDU_PROFILER=1
RUN if [ "$EDU_PROFILER" = "1" ]; then install-php-extensions excimer; fi

# Composer (depuis l'image officielle)
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

Expand Down
10 changes: 9 additions & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@
services:
frankenphp:
container_name: ${APP_NAME}-http-server
build: .
build:
context: .
args:
# Profiler de code de la barre de debug (extension Excimer) :
# 1 => installé (défaut, développement) | 0 => image sans profiler
EDU_PROFILER: ${EDU_PROFILER_BUILD:-1}
# tty: true => logs lisibles en dev ; retirer pour des logs JSON (prod)
tty: true
environment:
# Application (voir .env)
APP_NAME: ${APP_NAME:-edu-framework}
APP_ENV: ${APP_ENV:-dev}
# Profiler de code de la barre de debug : off | trigger | on (voir .env)
EDU_PROFILER: ${EDU_PROFILER:-trigger}
EDU_PROFILER_KEEP: ${EDU_PROFILER_KEEP:-50}
TZ: ${TZ:-Europe/Paris}
# Base de données : mêmes valeurs que le .env, sauf l'hôte et le port
# qui, dans le réseau Docker, sont toujours ceux du service (database:3306)
Expand Down
59 changes: 59 additions & 0 deletions docs/boost/debug-bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,65 @@ Exemple de réponse :
- `queries` : liste chronologique des exécutions (`rows_count` à `null` si le pilote ne fournit pas l'information, Exemple : `-1` pour un `SELECT` sous PostgreSQL) ;
- `groups` : requêtes identiques regroupées avec leur nombre d'exécutions et leur temps cumulé.

## Le profiler de code

Le profiler de code mesure **le temps passé dans chaque fonction PHP** pendant une requête HTTP, pour repérer ce qui ralentit une page. Il s'appuie sur l'extension [Excimer](https://www.mediawiki.org/wiki/Excimer){:target="_blank"} (Wikimedia), un profiler **par échantillonnage** : toutes les 0,1 ms, Excimer relève la pile d'appels en cours. Une fonction présente dans 30 % des relevés a consommé environ 30 % du temps de la requête. Le coût reste faible (quelques %) et fonctionne avec FrankenPHP, y compris en mode worker.

!!! info "Un résultat statistique"

Une fonction très courte (moins de 0,1 ms) peut ne jamais apparaître dans le profil. Les temps affichés sont des estimations : `nombre de relevés × 0,1 ms`.

### Installer l'extension

L'image Docker du framework installe Excimer par défaut (argument de build `EDU_PROFILER=1`). Pour construire une image sans profiler :

```bash
EDU_PROFILER_BUILD=0 docker compose build
```

Hors Docker, installez l'extension avec PECL (`pecl install excimer`) puis activez-la dans le `php.ini` (`extension=excimer`). La commande `php bin/edu check:config` indique si l'extension est chargée.

### Activer le profilage

Le profilage se règle avec la variable `EDU_PROFILER` du `.env` :

```bash
## << Config profiler de code
EDU_PROFILER=trigger
EDU_PROFILER_KEEP=50
## >> Config profiler de code
```

| Valeur | Comportement |
|-----------|------------------------------------------------------------------------------|
| `off` | Profiler désactivé, le bloc **Code** est masqué |
| `trigger` | (défaut) Profilage à la demande : bouton **Code** de la barre de debug, ou paramètre `?_profile=1` dans l'URL |
| `on` | Chaque requête est profilée |

`EDU_PROFILER_KEEP` fixe le nombre de profils conservés (les plus anciens sont supprimés, 50 par défaut).

En mode `trigger`, un clic sur le bloc **Code** pose le cookie `edu_profile=1` et recharge la page : cette page **et les suivantes** sont profilées, jusqu'au clic sur **Désactiver** dans le panneau du bloc.

### Lire le profil

- Le bloc **Code** de la barre de debug affiche le temps profilé et la fonction la plus coûteuse ; son panneau liste le top 3 des fonctions.
- L'onglet **Code** du profiler (`/_debug/profiler#code/{token}`) affiche le tableau de toutes les fonctions, triable par **temps propre** (temps passé dans la fonction elle-même) ou **temps inclusif** (fonction et fonctions appelées), filtrable par origine (`app`, `framework`, `vendor`). Un clic sur une fonction affiche ses appelants et ses appelés.
- Le bouton **Télécharger (Speedscope)** exporte les piles d'appels, à ouvrir sur [speedscope.app](https://www.speedscope.app){:target="_blank"} pour obtenir un *flamegraph*.

### L'API du profil de code

```
GET /edu-code-profile?token=42
```

| Paramètre | Description | Défaut |
|-----------|---------------------------------------------------------------------|--------|
| `token` | Identifiant du log de la requête HTTP (`#42` accepté) | requis |
| `full` | `1` : ajoute toutes les fonctions (`functions`) et le graphe d'appels (`edges`) | — |
| `format` | `speedscope` : retourne le fichier Speedscope de la requête | — |

La réponse contient `status` (`ready`, ou `pending` tant que le profil n'est pas enregistré), `summary` (`driver`, `period_ms`, `samples`, `wall_ms`, `peak_mb`, `functions_count`) et `top` (fonctions les plus coûteuses). Les profils sont stockés, compressés, dans la table `serv_code_profiles` de la base des logs.

## Le stockage des logs

Les requêtes HTTP traitées par le framework sont enregistrées dans une base **SQLite** dédiée :
Expand Down
10 changes: 10 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

**Présentation des versions du framework Edu Framework**

## Prochaine version (non publiée)

**new features**

- Profiler de code dans la barre de debug : le temps passé dans chaque fonction PHP d'une requête HTTP est mesuré par l'extension [Excimer](https://www.mediawiki.org/wiki/Excimer) (profiler par échantillonnage toutes les 0,1 ms, compatible FrankenPHP en mode classique et worker). Le profilage est **à la demande** (`EDU_PROFILER=trigger` par défaut) : le nouveau bloc **Code** de la barre de debug pose le cookie `edu_profile=1` (ou paramètre `?_profile=1`) et reste actif jusqu'au clic sur **Désactiver** ; `EDU_PROFILER=on` profile chaque requête, `off` désactive le profiler. Le profil est rattaché au token de la requête et stocké compressé dans la table `serv_code_profiles` (`EDU_PROFILER_KEEP` profils conservés, 50 par défaut). Le bloc affiche le temps profilé et le top 3 des fonctions ; le nouvel onglet **Code** du profiler (`/_debug/profiler#code/{token}`) liste toutes les fonctions (temps propre / inclusif, filtre par origine `app` / `framework` / `vendor`), leurs appelants et appelés, et exporte le profil au format Speedscope pour un flamegraph. Nouvel endpoint interne `GET /edu-code-profile` (mode dev uniquement). L'image Docker installe Excimer (argument de build `EDU_PROFILER`, `EDU_PROFILER_BUILD=0` pour l'exclure) et `php bin/edu check:config` signale l'extension comme recommandée

**documentation**

- update [La barre de debug et le profiler](boost/debug-bar.md) : nouvelle section « Le profiler de code » (installation, modes, lecture du profil, API)

## v2.7.3 - 17/10/2026

**new features**
Expand Down
3 changes: 3 additions & 0 deletions docs/installation/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ Docker Compose lit le fichier `.env` à la racine du projet et transmet ses vari
| `DB_TYPE` | Type de base (`mysql`, `mariadb`, `sqlite`) | `mysql` |
| `DB_USER`, `DB_PASSWORD`, `DB_NAME` | Identifiants de la base | `root` / `studoo` / `app_db` |
| `MAILER_DSN` | Serveur SMTP pour l'envoi de mails | `smtp://localhost:1025` |
| `EDU_PROFILER` | Profiler de code : `off`, `trigger` (à la demande) ou `on` | `trigger` |
| `EDU_PROFILER_KEEP` | Nombre de profils de code conservés | `50` |
| `EDU_PROFILER_BUILD` | `0` construit l'image sans l'extension Excimer | `1` |

!!! warning "Trois valeurs sont surchargées dans le conteneur"

Expand Down
10 changes: 10 additions & 0 deletions docs/installation/prerequis.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,16 @@ Pour cela, vous devez ouvrir votre fichier php.ini et décommenter la ligne suiv
````
Enlever le point-virgule pour activer l'extension.

#### excimer extension (optionnelle)

L'extension `excimer` active le [profiler de code](../boost/debug-bar.md#le-profiler-de-code) de la barre de debug.
Elle est déjà installée dans l'image Docker du framework. Hors Docker :

````Bash
pecl install excimer
````
Puis ajoutez la ligne `extension=excimer` dans votre fichier php.ini.

___

## :simple-composer: Gestionnaire de dépendance
Expand Down
2 changes: 2 additions & 0 deletions src/EduFramework/Commands/Extends/CkeckStack.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public function render(): void
$this->checkExtension('mbstring');
$this->checkExtension('openssl');
$this->checkExtension('json');
// Extension optionnelle : profiler de code de la barre de debug
$this->checkExtension('excimer');
}

/**
Expand Down
135 changes: 135 additions & 0 deletions src/EduFramework/Core/Controller/Debug/DebugCodeProfileController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

/*
* Edu Framework by studoo
*
* @author Benoit Foujols
*
* Pour les informations complètes sur les droits d'auteur et la licence,
* veuillez consulter le fichier LICENSE qui a été distribué avec ce code source.
*/

namespace Studoo\EduFramework\Core\Controller\Debug;

use Studoo\EduFramework\Core\Controller\ControllerInterface;
use Studoo\EduFramework\Core\Controller\Request;
use Studoo\EduFramework\Core\Logs\LogsService;
use Studoo\EduFramework\Core\Profiler\CodeProfileAggregator;

/**
* Class DebugCodeProfileController
* Endpoint interne de la barre de debug et du profiler : retourne le profil
* du code PHP exécuté pendant une requête HTTP (rattaché par le token du log
* de la requête), au format JSON.
* Cette route est enregistrée uniquement en mode dev (APP_ENV=dev),
* elle n'est donc pas accessible en production.
*
* @package Studoo\EduFramework\Core\Controller\Debug
*/
class DebugCodeProfileController implements ControllerInterface
{
/**
* Retourne le profil de code d'une requête HTTP au format JSON
* Paramètres de la requête :
* - token : identifiant du log de la requête HTTP (obligatoire)
* - full=1 : ajoute le détail de toutes les fonctions et le graphe d'appels
* - format=speedscope : retourne les piles d'appels au format Speedscope
* (fichier à ouvrir sur https://www.speedscope.app pour le flamegraph)
*
* Réponse :
* - token : identifiant du log de la requête HTTP (null si invalide)
* - status : ready (profil enregistré) ou pending (pas encore enregistré :
* la page peut s'afficher avant la fin de la requête)
* - summary : driver, period_ms, samples, wall_ms, peak_mb, functions_count
* - top : fonctions qui consomment le plus de temps propre
* - functions, edges : avec full=1 uniquement
*
* @param Request $request Requête HTTP
* @return string|null
*/
public function execute(Request $request): string|null
{
$speedscope = ((string) $request->get('format') === 'speedscope');

// L'en-tête n'est envoyé que si la sortie n'a pas déjà commencé
// (Exemple : exécution dans un contexte de test)
if (headers_sent() === false) {
header('Content-Type: application/json; charset=utf-8');
}

$token = ltrim((string) $request->get('token'), '#');
if (preg_match('/^\d{1,18}$/', $token) !== 1) {
return json_encode(['token' => null, 'status' => 'pending', 'summary' => null, 'top' => []]);
}

$tokenId = (int) $token;
$full = ((string) $request->get('full') === '1');
$profile = LogsService::getCodeProfile($tokenId, ($full === true || $speedscope === true));

if ($profile === null) {
return json_encode(['token' => $tokenId, 'status' => 'pending', 'summary' => null, 'top' => []]);
}

if ($speedscope === true) {
return $this->speedscope($tokenId, $profile);
}

$response = [
'token' => $tokenId,
'status' => 'ready',
'summary' => [
'driver' => $profile['driver'],
'period_ms' => $profile['period_ms'],
'samples' => $profile['samples'],
'wall_ms' => $profile['wall_ms'],
'peak_mb' => $profile['peak_mb'],
'functions_count' => $profile['functions_count'],
],
'top' => $profile['top'],
];

if ($full === true) {
$response += $this->details($profile);
}

return json_encode($response);
}

/**
* Retourne les piles d'appels du profil au format Speedscope (fichier téléchargé)
*
* @param int $tokenId Identifiant du log de la requête HTTP
* @param array<string, mixed> $profile Profil avec ses données (LogsService::getCodeProfile)
* @return string
*/
private function speedscope(int $tokenId, array $profile): string
{
if (headers_sent() === false) {
header('Content-Disposition: attachment; filename="edu-profile-' . $tokenId . '.speedscope.json"');
}
$data = $profile['data']['speedscope'] ?? [];
if (is_array($data) === true && isset($data['profiles'][0]) === true) {
$data['name'] = 'Edu Framework #' . $tokenId;
$data['profiles'][0]['name'] = 'Requête #' . $tokenId;
}

return (string) json_encode($data);
}

/**
* Détail complet du profil : toutes les fonctions et le graphe d'appels
*
* @param array<string, mixed> $profile Profil avec ses données (LogsService::getCodeProfile)
* @return array{functions: array<int, array<string, mixed>>, edges: array<int, array<string, mixed>>}
*/
private function details(array $profile): array
{
$data = $profile['data'];
$speedscope = (is_array($data['speedscope'] ?? null) === true ? $data['speedscope'] : []);

return [
'functions' => CodeProfileAggregator::byFunction($data),
'edges' => CodeProfileAggregator::callGraph($speedscope, (float) ($data['period_ms'] ?? $profile['period_ms'])),
];
}
}
Loading
Loading