Pipeline de rendu PRISM
Cette page retrace une seule requête de page PRISM tout au long de l'espace de travail Rust : comment l'itinéraire est mis en correspondance, comment le .tsx correspondant est transformé en un bundle JS évaluable V8 (via le interface tsc-rs et le Passe de compilation PRISM ), comment ce bundle est évalué dans le Pool de rendu V8 et comment le HTML résultant est assemblé et mis en cache.
Il s'agit de l'histoire de l'intégration de trois sous-systèmes qui ont leurs propres pages de référence : lisez ceci d'abord pour voir comment ils s'emboîtent, puis suivez les liens croisés pour plus de profondeur.
Le chemin en un coup d'œil#
HTTP request (GET /pricing)
│
▼
[1] Dispatch bext-server ssr_pipeline::pipeline::handle
│ → on_demand::handle_bext_app (framework = prism)
▼
[2] Route match prism::handle_prism_request → handle_prism_request_inner
│ ROUTE_TABLE_CACHE + bext_turbopack::prism::discover_routes
▼
[3] Compile (cache miss)
│ ┌─ pre_transform_source ──────────────────────────────────────┐
│ │ PRISM compile-fold pass bext_core::transform::prism_compile │ ← tsc-rs AST
│ │ (static JSX → string concat) │
│ └──────────────────────────────────────────────────────────────┘
│ tsc-rs --pipe worker (TscRsPipeWorker.transform_full) ← tsc-rs emit
│ transitive-import BFS + emit_route_bundle (topo-sorted IIFEs)
▼
[4] Evaluate V8 render pool (SubprocessPool / PrismPool)
│ install_prism_context → __bextPrismRender(props)
▼
[5] Collect collect_async_prism_result (drive microtasks/timers)
│ RouteResult { kind: "html" | "response" }
▼
[6] Assemble + cache HTML transforms → ISR / L0 / compressed-body cache
│
▼
HTTP response
Chaque étape ci-dessous nomme la caisse/fichier et les symboles clés afin que vous puissiez accéder à la source. Les chemins sont relatifs à
crates/ dans le espace de travail.
1. Répartition : s'agit-il d'un site PRISM ?#
PRISM est un et non un détecté automatiquement. Un site s'inscrit explicitement avec
[framework] type = "prism" dans bext.config.toml, analysé en
FrameworkConfig::framework_type ( bext-server/src/config.rs ). Il existe deux points d'entrée de répartition en fonction du mode de déploiement ( modes de déploiement ) :
| Mode | Entrée | Remarques |
|---|---|---|
Autonome ( bext run ) |
bext-server/src/handler.rs |
Vérifie framework_type == FrameworkType::Prism, construit un PrismCacheCtx soutenu par le L1/L2 IsrCache, appelle ssr_pipeline::prism::handle_prism_request. |
| Mascarade ( nginx -compat vhost) | ssr_pipeline/on_demand.rs::handle_bext_app |
Lorsque [build] engine = "turbopack", la passerelle transmet à super::prism::handle_prism_request. C'est ainsi que sont desservis docs.bext.dev et les autres sites en cours. |
Les deux convergent vers le pipeline partagé :
pipeline::handle() ssr_pipeline/pipeline.rs
→ gate_static_public() static / public asset fast-path
→ on_demand::handle_bext_app() framework dispatch
→ prism::handle_prism_request() public, single-flight wrapper
→ prism::handle_prism_request_inner() the real work
handle_prism_request enveloppe l'appel interne dans fusionnant un seul vol ( crate::single_flight ) pour les SSR GET sans cookie, de sorte qu'un troupeau tonitruant sur une route froide s'effondre en un seul rendu. Basculer avec
BEXT_SINGLEFLIGHT=0.
Un détail à connaître :
FrameworkType::Prismcorrespond en interne àDetectedFramework::Fetchpour le scanner existant — PRISM est intercepté avant, le scanner s'exécute, donc un outil qui litDetectedFrameworkvoitFetch, et nonPrism.
2. Correspondance d'itinéraire#
Dans handle_prism_request_inner, les chemins spéciaux sont d'abord mis en correspondance ( /islands/<name>.js, /_bext/action/<name>, /_bext/image, fichiers de métadonnées, /api/* ), puis intergiciel est consulté, puis la table de routage des pages :
ROUTE_TABLE_CACHE— unMutex<HashMap<…>>global de processus dansprism.rs, saisi par(app_root, invisible_segments)et contenant la liste de routes par application (unVecdeRouteInfo). En cas d'échec, il appellebext_turbopack::prism::discover_routes, qui parcourtsrc/app/pourpage.tsx/page.jsx.RouteInfo(bext-turbopack/src/prism.rs) porteurl_path,page_path,is_client,is_dynamic,is_catch_all.match_routeeffectue un scan linéaire : itinéraires statiques avant itinéraires dynamiques, fourre-tout ([...slug]) en dernier.route_signals_cachemémorise l'analyse par itinéraire (has_loader,has_action,revalidate, éligibilité au streaming) utilisé pour brancher l'ISR et le comportement de streaming. Voir Données PRISM.
Le cache est invalidé par clear_route_table_cache_for_app(), appelé depuis l'observateur de fichiers bundler.rs lorsqu'un fichier sous src/app/ est modifié. Voir rechargement en direct et invalidation et redémarrages.
Si vous devez déboguer ce que le bundler émet, définissez BEXT_DUMP_PRISM_BUNDLE=1 et redémarrez bext. Le bundle CommonJS complet pour la route correspondante est écrit dans stderr afin que vous puissiez inspecter la disposition IIFE et toutes les concaténations de chaînes repliées.
3. Compiler — Ensemble .tsx → V8#
En cas d'échec du cache, la route correspondante est compilée en un seul bundle CommonJS. L'orchestration réside dans bext-turbopack/src/direct.rs ; l'entrée spécifique à PRISM est compile_route_server dans
bext-turbopack/src/prism.rs, qui définit
jsx_import_source = Some("@bext-stack/framework") et appels
compile_entry_to_js.
3a. Cache de compilation à quatre couches#
compile_entry_to_js vérifie quatre caches avant d'effectuer tout travail :
PATH_MEMO— clé de chemin, basée sur TTL (par défaut 30 s,BEXT_TURBOPACK_PATH_MEMO_TTL_MS). Ignore même la statistique mtime.- Cache de compilation de hachage de contenu — en cours, saisi sur un hachage
u64de l'entrée plus les mtimes de toutes les importations transitives. Survit à l’expiration du TTL. - Cache disque persistant (
compile_disk_cache) — clé de hachage de contenu bundles sous/var/cache/bext/compile, avec vérification transitive-mtime du temps de lecture (auto-correction ; un service modifié rejette l'entrée). Il s'agit du niveau auquel survit à un redémarrage/redéploiement : une route inchangée est servie à partir du disque en80 µs au lieu de réexécuter tsc-rs (8 ms) — éliminant ainsi la tempête de recompilation après le déploiement. Se désinscrire avecBEXT_TURBOPACK_DISK_CACHE=0. - Porte à vol unique —
COMPILE_KEY_LOCKS, un hachage par contenuArc<Mutex<()>>donc N requêtes simultanées pour le même module invalidé ne sont pas chacune compilées à froid. Coupe-circuit :BEXT_TURBOPACK_COMPILE_SINGLEFLIGHT=0.
Ce n'est plus un piège (2026-06-12) : les caches en cours (1, 2) sont toujours mourir avec le processus, mais le niveau de disque (3) signifie qu'un redémarrage n'est plus compile à froid les itinéraires inchangés. Les modifications de la source sont récupérées par le observateur de rechargement en direct (mode travailleur) ou observateur d'itinéraire de mascarade (mode en cours) dans un délai d'environ 2 s — l'entrée sur le disque s'auto-invalide sur le l'heure mtime du fichier a été modifiée, vous n'avez donc plus besoin de
rm -rf .bext+ redémarrage. Voir invalidation et redémarrages.
3b. Le pli de pré-transformation (avant tsc-rs)#
pre_transform_source exécute avant, l'interface TypeScript voit le fichier. Lorsque jsx_import_source == "@bext-stack/framework" et que le fichier est .tsx / .jsx :
- Les fichiers
"use signals"sont acheminés versbext_core::transform::prism_signals::optimize(voir Signaux ). - sinon le Passe de compilation PRISM
(
bext_core::transform::prism_compile::optimize) plie les sous-arbres JSX statiques en concaténations de chaînes directement dans le texte source, à l'aide de l'analyseur/AST tsc-rs (types de nœudstsc_rs_parser::parse+tsc_rs_ast). Il effectue des épissures de plages d'octets sur la source d'origine - c'est et non qui émet JS lui-même. Désactivez-vous avecBEXT_PRISM_COMPILE=0.
La définition de jsx_import_source force également le mode JSX tsc-rs à
react-jsx (exécution automatique), de sorte que l'émetteur produit
require("@bext-stack/framework/jsx-runtime") appelle tout JSX laissé par la passe de pliage. Voir le Exécution JSX.
3c. La transformation tsc-rs (le véritable TS→JS)#
La source repliée est transmise à un sous-processus tsc-rs --pipe persistant — TscRsPipeWorker dans direct.rs :
transform_fullenvoie{ filename, source, options }sous forme d'un seul JSON ligne et relit{ output, imports, dynamic_imports, exports, source_map }. C’est là que TSX devient CommonJS JS.PipeOptionsportetarget: es2022,module: commonjs,jsx,jsx_import_source.- Le travailleur suit le mtime binaire sur le disque et réapparaît automatiquement lorsque vous reconstruisez tsc-rs, afin que les modifications du frontend prennent effet sans redémarrer bext.
- Un pool de ces travailleurs est dimensionné par
BEXT_TURBOPACK_PIPE_POOL_SIZE(par défaut 1) afin que des modules distincts puissent se transformer en parallèle.
La division complète du travail – qui analyse, qui émet – se trouve sur la page Frontal TypeScript.
3d. Fermeture transitive + émission de bundle#
compile_closure effectue un BFS sur les importations de l'entrée, transformant chaque module une fois (résultats mémorisés dans le fichier à clé Blake3).
shared_registry() / ModuleRegistry ), puis emit_route_bundle assemble le tout dans un seul fichier :
- écrit les shim IIFE et les solutions de secours
globalThis.__Layout{0,1,2}, - émet chaque module comme IIFE avec
//# sourceURL=<abs>, - exécute un
topological_sortafin que les modules soient évalués dans l'ordre des dépendances (avec objets__modules[abs]vides pré-enregistrés pour la sécurité des cycles).
BEXT_PRISM_COMPILE_OFFTHREAD=1 déplace toute cette étape vers
spawn_blocking afin qu'une compilation à froid ne bloque pas le thread de travail actix (une atténuation des tempêtes de recompilation — voir le Post-mortem du pool de rendu V8 ). Videz le paquet émis pour inspection avec BEXT_DUMP_PRISM_BUNDLE=1.
4. Évaluer dans V8#
Le bundle compilé (une chaîne partagée Arc ) est évalué par Pool de rendu V8. Il existe deux backends ; la décision d'expédition dans handle_prism_request_inner est à peu près
use_pool = !subprocess_mode_configured() && backend().is_none():
| Back-end | Caisse / fichier | Quand |
|---|---|---|
SubprocessPool |
bext-v8/src/subprocess.rs |
Production. BEXT_V8_POOL=1. N travailleurs hors processus, chacun isolé par V8, restituent les images envoyées sur des sockets Unix encadrés. |
En cours de processus PrismPool |
bext-v8/src/prism_pool.rs |
Threads du système d’exploitation default_pool(), BEXT_PRISM_WORKERS (par défaut 2), chacun étant un isolat de longue durée. |
| Filetage d'évaluation unique | bext-v8/src/eval.rs |
Retomber; un thread bext-v8-eval-worker sérialise tous les rendus. |
Quel que soit le backend, un contexte de rendu est défini par
install_prism_context ( eval.rs ) : un nouveau contexte V8 avec les fonctions de pont enregistrées ( bridge::register_bridge_functions —
__httpFetch, __bextReadChunk, __env, __readFile, …)), les polyfills évalués et le bundle JS évalué. Les contextes chauds sont mis en cache dans prism_contexts (saisi par prism_cache_key(shell, bundle) ), de sorte que les rendus suivants sur le même isolat ignorent la réévaluation du bundle.
Construire un contexte pour la première fois évalue le bundle JS. Cette évaluation est soutenue par un cache de bytecode V8 persistant ( bytecode_disk_cache,
/var/cache/bext/bytecode, saisi par le hachage du bundle + la balise de version V8) : sur une construction de contexte froid, le travailleur restaure le bytecode compilé via
ConsumeCodeCache au lieu de ré-analyser + compiler le JS, donc une construction de contexte après redémarrage est également plus rapide. Se désinscrire avec
BEXT_V8_BYTECODE_DISK_CACHE=0. Avec le disque compilez le cache (§3a), un redémarrage ignore à la fois, la compilation tsc-rs et l'analyse V8.
5. Rendu → RouteResult#
Le rendu est piloté en appelant globalThis.__bextPrismRender(propsJson) :
- tamponné (
render_prism_in_context_buffered) —collect_async_prism_resultinterrogeglobalThis.__bextPrismResult.doneen boucle, pompant des microtâches/minuteries (pump_timers_and_microtasks) jusqu'à ce que le rendu se stabilise, puis renvoie le résultat. - Diffusion (
render_prism_in_context_streaming, inscription facultativeBEXT_PRISM_STREAMING=1) —collect_async_prism_result_progressivepilote la boucle d'événements V8 et pousse chaque nouveau morceau sur un canal Tokio au fur et à mesure qu'il apparaît, de sorte que la sortierenderToReadableStreamsoit progressivement envoyée au fil. Les itinéraires avec unloader/action(qui peuvent rediriger ou définir un statut) reviennent à la mise en mémoire tampon.
Le rendu renvoie un RouteResult sur un cadrage IPC binaire ( \x01v1\n{envelope}\n{body} ) qui évite la surcharge d'échappement JSON.stringify :
kind: "html"— un corps de page normal.kind: "response"— le chargeur/l'action a renvoyé unResponse(redirection,notFound(), statut personnalisé). Extraits de rouillestatus,headersetbodyet construit directement leSiteResponse.
6. Assembler, transformer, mettre en cache#
Le HTML est finalisé via le temps de réponse pipeline de transformation (remplacement de l'îlot de serveur, extraction des indices de préchargement pour HTTP 103 Early Hints) et l'étape de compression du cycle de vie de la demande, puis mis en cache sur plusieurs couches :
| Couche | Où | But |
|---|---|---|
| L0 | bext-server/src/l0_cache.rs |
Cache en cours de processus haut de gamme à la limite d'actix. Seuls les nouveaux hits ISR sont stockés (hôte + chemin + clé de requête). |
| ISR (L1/L2) | bext-core/src/cache/isr.rs (IsrCache) |
Le cache mode = "isr" avec TTL revalidate, invalidation de balise et bouclage périmé pendant la revalidation. L2 est soutenu par Redis pour le multi-instance. Voir mise en cache. |
| Cache latéral à corps comprimé | DashMapPrismStore dans prism.rs |
Les corps pré-compressés (brotli/gzip) afin que les hits chauds évitent la recompression. Budgétisé par BEXT_COMPRESSED_BODIES_MAX_BYTES (par défaut 512 Mio). |
| Table de routage / cache de compilation | §2, §3a | Caches structurels gérés par hachage d'application et de contenu. |
Le site docs.bext.dev que vous lisez exécute mode = "isr" avec
revalidate = 300, donc la plupart des rendus de fragments sont des accès au cache.
7. Le chemin de l'erreur#
Lorsqu'un rendu échoue, prism_error_response ( prism.rs ) renvoie un 500 avec l'erreur apparue dans les en-têtes de la superposition de développement :
x-bext-error-kind,x-bext-error-message,x-bext-error-route, chacun est nettoyé en ASCII visible parto_ascii_header(les tirets, les guillemets bouclés et les ellipses sont pliés ; tout ce qui se trouve en dehors de0x20..=0x7Edevient?). Ceci est important car une valeur d'en-tête non-ASCII rendHeaderValue::TryFroméchoue silencieusement et l'en-tête disparaît.- le corps est une page d'erreur stylisée de
render_prism_error_pageavec des indices heuristiques sur la « cause probable ».
Gotcha : compile les échecs de prendre une branche différente qui rend la page d'erreur directement sans réglage
x-bext-error-message, donc la superposition de développement revient à lire le (éventuellement compressé) corps. Seuls les échecs de rendu ** définissent l'en-tête. Voir codes d'erreur et dépannage.
Sous contre-pression de piscine, la réponse est plutôt un 503 avec
Retry-After: 1 et x-bext-pool-shed: queue-full — voir hangar précoce.
Où aller ensuite#
- Frontend TypeScript (tsc-rs) — l'analyseur/émetteur qui alimente les étapes 3b à 3c.
- Passe de compilation PRISM — qu'est-ce que le pli passer les plis, ce qu'il saute et les chiffres de performances.
- Piscine de rendu V8 — la topologie du pool, chien de garde et résilience qui exécute l’étape 4.
- Transformer le pipeline — tous les la source et la réponse se transforment dans l’ordre d’exécution.
- Cycle de vie des demandes — le HTTP complet pipeline dont ce rendu est une étape.
- PRISM (cadre) — le modèle de création du côté développeur d'applications.