Aller au contenu

Profiling — introduction

Profile le code lent de ton app, visualise un flame graph dans le dashboard mobile, et détecte automatiquement les régressions de performance entre releases. Modèle “transaction-scoped” — tu choisis quel chemin tu instrumentes plutôt que de payer un sampler en continu.

Le crash monitoring te dit ça plante. Le profiling te dit où c’est lent :

  • Un écran qui met 3 sec à monter mais qui ne crashe jamais → invisible pour le crash monitor, visible avec le profiler
  • Une régression de perf après un refactor → graph cross-release qui flag la fonction coupable
  • Un endpoint backend qui consomme 80 % du CPU sous load → flame graph qui pointe la fonction

C’est le pendant performance du crash monitor.

  1. Le SDK collecte des samples de stack pendant un intervalle (1 ms par défaut sur Hermes/V8)
  2. Le SDK POST le blob brut à /api/profiles à la fin de chaque transaction profilée
  3. Les samples sont agrégés en P50/P95/P99 par fonction × release pour le flame graph + le diff cross-release
import { Pionne } from '@pionne/react-native';
// Sugar — wrap une transaction entière
await Pionne.profile('CheckoutFlow', async () => {
await fetchCart();
await applyDiscount();
await submitOrder();
}, { route: '/checkout' });
// Ou manuel pour plus de contrôle
Pionne.startProfile('HomeScreenMount', { route: '/home' });
// … render path …
const profileId = await Pionne.stopProfile();
console.log('Profile shipped:', profileId);

startProfile renvoie false silencieusement si Hermes n’expose pas le sampler (JSC, certaines vieilles versions). Ton code reste engine-agnostic — pas de runtime check à écrire.

Pionne.profile(name, fn, meta?) est le sucre recommandé : il garantit que stopProfile est appelé même si fn throw, et il retourne ce que fn retourne.

Tu peux poster directement à POST /api/profiles si tu veux profiler un SDK qui n’a pas encore d’implémentation native. Le backend accepte deux formats :

Format produit par Hermes, V8 (--cpu-prof), Chrome DevTools.

{
"name": "CheckoutFlow",
"platform": "react_native",
"release": "1.4.2",
"environment": "production",
"route": "/checkout",
"duration_ms": 2840,
"samples_count": 2840,
"sample_interval_us": 1000,
"samples": {
"traceEvents": [
{ "ph": "X", "name": "App.render", "ts": 0, "dur": 1200 },
{ "ph": "X", "name": "Cart.fetch", "ts": 1200, "dur": 980 },
{ "ph": "X", "name": "Order.submit", "ts": 2180, "dur": 660 }
]
}
}

L’agrégateur ne lit que les events ph: "X" (complete events avec dur explicite). Les paires B/E (begin/end) sont ignorées dans le MVP.

Auth : header X-Pionne-Token: pio_live_… (le même que pour /ingest).

Réponse :

{ "ok": true, "profile_id": 42 }
SDKProfiler natifOverhead pendant capture
RN (Hermes)HermesInternal.enableSamplingProfiler1–3 % CPU
WebPerformance.profile() (Chrome only)2–5 % CPU
NodeV8 inspector3–8 % CPU
PHP (Excimer)wall-clock sampler~1 % wall-clock
FlutterDart VM Profiler2–5 % CPU

Quand le profiler n’est pas actif → 0 % overhead. C’est le gros avantage du modèle transaction-scoped : tu paies le coût uniquement sur les chemins que tu instrumentes.