20 KiB
Fiche STE-H7 — Niveau N3 — Modèle prévu (modèle de l'agent lp-cart) | utilisé claude-opus-5-5 — Agent lp-cart (DIAG, lecture seule) — Skills sk-protocol, lp-commandlets (lecture), lp-paths ; ue-cpp-build préchargé, non utilisé ; lp-doc (gabarits)
STE-H7-DIAG : chargements synchrones (FlushAsyncLoading) à l'apparition des PNJ, coût et voies comparées (2026-10-11)
Résultat : hypothèse réfutée comme cause principale. Les FlushAsyncLoading (FAL) existent au SPAWN, mais ils ne pèsent que
5 % du temps des trames SPAWN. Le reste vient surtout du Blueprint relancé par les actions latentes (FlushLatentActions) :
64 % dans la capture 0, 78 % dans la capture 1. Chiffres de la capture 0 : 41 trames SPAWN, 5 753 ms au total, médiane 123 ms,
dont 301 ms de FAL et 3 656 ms de FlushLatentActions. Un chargement synchrone coûte 4,2 ms en médiane (max 26,9 ms). Les
FAL des vêtements (DressSavedActual → GI getSkeletalMeshFromPath) ne donnent pas de saccade : sur 75 trames, médiane
17 ms, 2 seulement ≥ 50 ms. Les « 21 FAL seuls » de STE-H4-MESURE viennent d'un décalage d'une trame dans la méthode. Corrigée,
la capture 0 n'a que 2 trames longues dominées par un FAL (408 ms, chargement initial) et la capture 1 aucune. Les noms des
paquets chargés ne figurent dans aucune source disponible : il faut une mesure et 5 dumps (§ 5). Aucun éditeur, commandlet,
build ni jeu n'a été lancé ; seul ce fichier est écrit.
Copie D:\Game\LifePlay-master 5.8, HEAD 4283c13 (fiche, rubrique 3). Abréviations (chemins absolus) :
- M =
%USERPROFILE%\AppData\Local\Temp\claude\D--Game-LifePlay-master-5-8\37530d8d-c780-4a57-8fbd-bcb010aa6408\scratchpad\M-H4-LifePlay.log(22 488 lignes horodatées). - CSV0 / CSV1 =
D:\Game\LifePlay_Build_5.8_STE-J5_Development\Windows\LifePlay\Saved\Profiling\CSV\Profile(20261011_012223).csv/Profile(20261011_012509).csv(9 000 trames chacune ; en-tête en ligne 1 et en fin de fichier). - Dumps
D:\Game\LifePlay-master 5.8\Docs\Migration\etat\ST\STE\dumps\: PEG4 =Person__EventGraph_D4.txt, LMIN =GI__LoadMeshIfNeeded_D4.txt, RM5 =Room__EventGraph_V5.txt. - UE =
D:\Game\Launcher\Engine\UE_5.8\Engine\Source\Runtime\(moteur installé, lu).
1. Symptôme et reproduction
- Symptôme (
STE-H4-MESURE.md:38-40, 45) : 46 trames SPAWN ≥ 50 ms (5,5 s), dont 38 avec FAL ; 21 trames « FAL seul » (4,5 s) ; 422 FAL dans le journal. Jeu non relancé ; relecture de M, CSV0 et CSV1. - Recompte (Grep
FlushAsyncLoading, count, sur M) : 422 lignes, toutesLogStreaming: Display:. Files d'attente : 418 ×1 QueuedPackages, 0 AsyncPackages, 2 ×0, 0, 1 ×8, 0(M:806), 1 ×0, 1. Identifiants entre parenthèses : 8 → 649, strictement croissants. AsyncLoadRequests_2026.10.11-01.25.09.log:1et…01.27.47.log:1: « Captured 0 unique callstacks » (fichiers vides).- Captures CSV dans M : début M:966 (trame
[1]) et M:11330 (trame[3]), fin M:11313 et M:18504. La 3e capture (M:18511) est interrompue et écartée (STE-H4-MESURE.md:14).
2. Hypothèse testée (une seule)
H7 : les trames SPAWN longues et les trames « FAL seul » viennent des chargements synchrones d'assets de personnage
(maillages, vêtements) déclenchés sur le chemin du SPAWN.
Réfutée si le temps passé dans FlushAsyncLoading ne fait qu'une petite part de la durée de ces trames.
3. Essai et résultat brut
Méthode : 7 passages Python en lecture seule (Constats). Chaque ligne de M est rattachée à une ligne CSV selon le numéro de
trame modulo 1000 et le temps écoulé depuis le début de la capture, puis lue avec les colonnes Exclusive/GameThread/*.
3.1 Ce que dit une ligne FAL (établi, moteur)
- La ligne est écrite avant l'attente (UE
CoreUObject\Private\Serialization\AsyncPackageLoader.cpp:297puisFlushLoading:306), seulement siIsAsyncLoading() && IsInGameThread()(:290). Elle n'est enDisplayqu'une fois par trame ; les suivantes de la même trame sont enLog(:295-301). OrLogStreamingest par défaut enDisplay(UECore\Public\CoreGlobals.h:43) : le journal ne montre que le premier FAL de chaque trame. - Chaque appel ajoute un événement CSV (
CSV_EVENT_GLOBAL_NOLOG, :279) et compte son temps dansExclusive/GameThread/FlushAsyncLoading(:278). - Un chargement synchrone (
LoadObject,LoadSynchronous,Load Asset Blocking…) appelleLoadPackageAsync, puisFlushAsyncLoading(RequestID)(UECoreUObject\Private\UObject\UObjectGlobals.cpp:1759-1764). Le nombre entre parenthèses est donc l'identifiant de la demande.1 QueuedPackages(418 lignes sur 422) = un paquet racine par chargement. - Aucune source disponible ne nomme les paquets. La ligne FAL ne porte pas de nom. Le traceur
TrackAsyncLoadRequestsnote le nom de chaque paquet demandé (TrackRequest(InPackagePath.GetDebugName(), …), AsyncPackageLoader.cpp:599 ; actif hors Shipping et Test, :28) et l'écrit à la fin d'une capture CSV (:398-401). Mais il est désactivé par défaut (TrackAsyncLoadRequests.Enable= 0, :365-367 ; retour immédiat :488) : d'où les fichiers vides.
3.2 Alignement journal ↔ CSV (établi)
- Le décalage 0 donne 215 lignes CSV0 sur 215 et 122 lignes CSV1 sur 122 : chaque trame qui porte un FAL dans M porte aussi un événement FAL dans le CSV.
FrameTimede la ligne r mesure la trame r − 1 : la valeur est prise au début de la trame, juste aprèsUpdateTimeAndHandleMaxTickRate(UECore\Private\ProfilingDebugging\CsvProfiler.cpp:4369-4388). Contrôle sur les 68 lignes ≥ 50 ms de CSV0 : Σ|FT[r] − GameThreadTime[r − 1]| = 378 ms, contre 11 380 ms avec GT[r]. CSV1 : 356 contre 5 564 ms. Une trame longue r se lit donc en FT[r + 1], avec les colonnes exclusives et le contenu du journal de la ligne r.STE-H4-MESURE.md§ 4 attribue la durée de la trame i − 1 au contenu de la trame i. Son script mesuret(1re ligne de i) − t(1re ligne de i − 1)et classe par les lignes de i (scratchpad\mesure_h4.py:88-108). Le décalage est le même que celui deFrameTime, et:52teste aussi FT sur la ligne de l'événement. Contrôle : les 34 trames SPAWN longues de cette méthode situées dans les captures ont toutes FT[i] ≥ 50, mais seulement 23 ont FT[i − 1] ≥ 50.- CSV1 n'a pas de colonne
Exclusive/GameThread/FlushAsyncLoading(513 colonnes, 131 événements FAL). Le temps des FAL n'est donc mesuré que dans CSV0. Cause : incertaine.
3.3 Q-a : ce qui déclenche les FAL
Contexte des 422 FAL. Classe = la dernière ligne Blueprint écrite avant le FAL dans la même trame :
| Contexte | FAL | Écart médian jusqu'à la ligne suivante | Temps FAL dans CSV0 |
|---|---|---|---|
DressSavedActual (test19) |
147 | 38 ms | 557 ms |
index SPAWN |
40 | 25 ms | 221 ms |
CHECKING: (voisinage) |
50 | 16 ms | 166 ms |
| aucune ligne Blueprint avant, dans la trame | 183 | 49 ms | 597 ms |
autre (stop getting npcs, inside box) |
2 | 15 ms | 18 ms |
-
Vêtements (chaîne établie, nœud de chargement estimé fort). Dans Person, l'événement
DressSavedActualActual(K2Node_CustomEvent_23, PEG4:10550) est réarmé parK2_SetTimerDelegate(_142, PEG4:10553-10561). La chaîne :PrintString 'DressSavedActual (test19)'(_231, PEG4:10535-10540) →GI.getSkeletalMeshFromPath(_190, PEG4:10519-10524), dont lePathvient deSavedClothes[0]→Cloth.Base.SkeletalMeshPath(GetArrayItem_4,BreakStruct_7,_8, PEG4:10509-10533, 10546-10549) →currentSM. Dans le journal, le FAL suit l'impression dans la même milliseconde (M:1614-1615, 1625-1626, 1653-1654, 1662-1663, 1669-1670). Le paquet est le vêtement nommé ensuite parDressed <article>pour le même acteur (ex. M:1625-1626 → M:1659Office-Attire-Skirt_1_F; estimé). Le contenu degetSkeletalMeshFromPath(nœud de chargement exact) n'est pas dumpé. -
SPAWN (nœud non établi). Chaîne : Room
GenerateUnknownNpcs(RM5:2254) → GIGeneratePerson(RM5:2260) →index SPAWN→ PersonRandomizeAll(pile de script M:1563-1578). Le FAL tombe entreindex SPAWNetwear glasses (test2206)(M:1589-1591, 1598-1600, 1606-1608). Cette impression n'est ni dans PEG4 ni dans RM5 (Greptest2206→ 0 sur les deux). Candidats de GI, tous à cache « IfNeeded » :LoadMeshIfNeeded:IsValid(MeshSexRace[i].Mesh); brancheIs Not Valid→getSkeletalMeshFromPath→Array_Set(cache du corps, LMIN:140-157, 193-196), seulement si raceDaz,LegacyPercentage > 0,Dog,HorseouTentacles(LMIN:171-235) ;LoadHairMeshIfNeededsurIs Not Valid(PEG4:9244-9248, 9587-9591) ;LoadOtherMeshIfNeeded_0dans unForEach(PEG4:9483-9489) ;LoadDefaultTextures(PEG4:17892).
Dumps requis (§ 5).
-
183 FAL sans ligne Blueprint avant. Ce sont des chaînes, un FAL toutes les 3 trames après un
LOAD ROOM: M:1285-1322 (17 FAL après M:1271), M:3038-3060 (12 FAL). Elles voisinent avecMeubles assis ou couches(LPShowCoupleSubsystem.cpp:468) et des lignes F5.OpenUseFurniturene charge rien : seulementDoesPackageExist(LPShowCoupleSubsystem.cpp:405-470, :425). Chaque FAL de ces chaînes coûte de 1 à 19 ms dans des trames de 17 ms (CSV0) : pas de saccade. Source : incertaine (mobilier du lieu, estimé). -
C++ (établi, Grep
LoadObject|StaticLoadObject|LoadSynchronous|TryLoad\(|LoadPackage|…surSource). Chargements synchrones en jeu :LPModDailies.cpp:126: une seule fois, siLoaded.Num() == 0(:122-135) ;LPShowCoupleSubsystem.cpp:178: paire d'animations de scène ;LPDirectControlSubsystem.cpp:961;LPSettingsSubsystem.cpp:474.
Aucun n'est sur le chemin du SPAWN (estimé). Plugins de la copie : Grep des mêmes motifs sur
Plugins\*.cpp→ 0 ;ModdingExtraBPLibrary.h:31-50ne lit que des fichiers texte.
3.4 Q-b : coût (établi sur CSV0, sauf mention)
- Total des FAL dans CSV0 : 1 559 ms sur 165,5 s, soit 0,94 % (344 événements sur 215 lignes). Par chargement : médiane 4,2 ms, p90 10,7 ms, max 26,9 ms.
- Trames ≥ 50 ms, avec la correction d'une trame (§ 3.2), classées par le contenu de la trame mesurée. Colonnes en ms :
| CSV0 | Trames | Durée | GameThread | FlushLatentActions |
FAL | TimerManager |
|---|---|---|---|---|---|---|
| SPAWN | 35 | 5 616 | 5 552 | 3 560 | 301 | 195 |
| autre (dont démarrage) | 10 | 5 185 | 5 094 | 389 | 0 | 360 |
GenerateRecs (H8) |
8 | 2 230 | 2 201 | 1 | 16 | 1 410 |
CHECKING: |
8 | 814 | 791 | 417 | 0 | 111 |
| FAL sans autre marqueur | 2 | 408 | 389 | 65 | 312 | 0 |
| DESTROY | 1 | 290 | 276 | 0 | 0 | 0 |
DressSavedActual |
2 | 163 | 165 | 0 | 6 | 140 |
| écriture LLM / sans ligne | 2 | 143 | 92 | 0 | 0 | 67 |
| Total | 68 | 14 849 | 635 (4,3 %) |
CSV1 (pas de colonne FAL) : SPAWN 16 trames, 2 377 ms, FlushLatentActions 1 858 ms (78 %) ; autre 31 trames, 3 263 ms ;
DESTROY 5, 1 464 ms ; sans ligne 10, 808 ms ; GenerateRecs 1, 240 ms ; LLM 1, 97 ms. Aucune trame longue « FAL seul ».
- Par SPAWN. Les 41 trames SPAWN de CSV0 durent 5 753 ms au total (médiane 123 ms). Elles contiennent
FlushLatentActions3 656 ms (64 %), FAL 301 ms (5,2 %) et GameThread 5 687 ms ; 40 événements FAL, soit ≈ 7,3 ms par trame SPAWN. CSV1 : 24 trames, 2 563 ms, médiane 115 ms,FlushLatentActions1 986 ms (77 %).FlushLatentActions= traitement des actions latentes restantes (UEEngine\Private\LevelTick.cpp:1795-1797) ; le Room a desDelay(RM5:270, 420). Estimé : la chaîne du SPAWN (GeneratePerson,RandomizeAll,LoadPreset…) reprend après unDelay. Son Blueprint est alors compté sous ce nom, hors portées plus fines comme FAL. - Premiers SPAWN contre suivants. 95
index SPAWN, 6 acteurs seulement (mannequins recyclés). Part avec un FAL dans la même trame : 4 sur 6 à la première apparition, 51 sur 89 aux reprises (57 %). Pas d'effet de cache visible. Estimé : l'asset diffère d'un PNJ à l'autre, ou il est libéré entre deux usages. - Vêtements. 239
DressSavedActual, dont 147 suivis d'un FAL ; 145 articles distincts (appariés à la ligneDressedsuivante du même acteur, 0 non apparié). FAL à la première pose : 121 sur 145 ; aux poses suivantes : 26 sur 94. Cache partiel (estimé : l'asset reste chargé tant qu'il est référencé). Coût : 75 trames dans CSV0, médiane 17 ms, 2 seulement ≥ 50 ms ; 477 ms de FAL au total, absorbés par la marge (GameThread médian 5,4 ms,STE-H4-MESURE.md:22). - Les « 21 FAL seuls » de STE-H4-MESURE. 6 tombent hors des captures. Sur les 14 restantes, après correction, il ne reste
que les 2 trames de CSV0 (408 ms, dont 312 ms de FAL). L'une est la ligne 44 : 87 événements FAL, 306 ms, journal M:996 à
23:22:24, chargement initial, avant le premier
LOAD ROOM(M:1246). Les autres sont les trames qui suivent une trame longue d'une autre classe (décalage du § 3.2). -llmcsv: l'écriture LLM coûte 75 ms dans les trames SPAWN longues de CSV0 (colonneExclusive/GameThread/LLM) : effet faible.
3.5 Q-c : correctifs comparés (sans décision)
Effets chiffrés sur CSV0 (estimés, sauf les coûts actuels, établis au § 3.4).
| Voie | Où (nœuds, fichiers) | Effet attendu sur FAL | Effet sur les saccades | Mémoire | Risque principal |
|---|---|---|---|---|---|
| P1 Précharger à l'entrée du lieu | Room, au LOAD ROOM : chargement asynchrone des chemins connus (vêtements SavedClothes, cheveux, corps) par le nœud latent Async Load Asset dans l'EventGraph, ou un sous-système C++ à créer dans Source\LifePlay (FStreamableManager::RequestAsyncLoad, poignées gardées jusqu'au SPAWN) ; SPAWN après la fin du chargement |
SPAWN −301 ms (−40 FAL), vêtements −477 ms | trames SPAWN −5 % (médiane ≈ 123 → ≈ 116 ms), toujours ≥ 50 ms | mêmes assets, chargés plus tôt ; poignées à libérer | PNJ inconnus tirés au hasard au SPAWN : rien à précharger sans réordonner (tirer, charger, puis faire apparaître) ; logique Blueprint plus lourde |
| P2 Asynchrone sur place (vêtements) | Person DressSavedActualActual : remplacer l'appel bloquant GI.getSkeletalMeshFromPath (_190, PEG4:10519) par Async Load Asset (permis dans un événement), puis currentSM à la fin |
vêtements −147 FAL (−477 ms) | ≈ nul : 2 trames longues sur 75, 6 ms de FAL | inchangée | l'acteur recyclé (ResetVars) avant la fin du chargement : garde IsValid ou compteur de génération ; vêtement visible 1 à 2 trames plus tard |
| P3 Garder une référence (cache) | GI : table chemin → SkeletalMesh alimentée par getSkeletalMeshFromPath, comme MeshSexRace pour le corps (LMIN:140-157) |
reprises seulement : vêtements ≤ −26 FAL ; SPAWN ≤ −51 FAL si l'asset passe par la même fonction (inconnu) | SPAWN ≤ −5 % | croît avec les articles distincts (145 dans la session ; maillages 70-80 Mo au total, STE-H4-MESURE.md:30) : plafond ou vidage au changement de carte à prévoir ; non chiffré |
croissance mémoire ; vidage à définir |
| Abstention sur FAL | — | 0 | 0 | 0 | aucun ; le coût principal reste dans FlushLatentActions (§ 5, H7b) |
Le temps visé par la fiche (≈ 10 s) n'est pas du FAL. Dans CSV0, les FAL font 635 ms sur 14 849 ms de trames longues. Aucune des voies P1-P3 ne fait passer une trame SPAWN sous 50 ms (estimé, médiane 123 ms).
4. Verdict : réfutée (cause principale) ; FAL présent mais minoritaire
- Établi :
- une ligne FAL = premier chargement synchrone de la trame, écrit avant l'attente, sans nom de paquet (§ 3.1) ;
FrameTime[r]= durée de la trame r − 1, et la méthode de STE-H4-MESURE § 4 porte le même décalage (§ 3.2) ;- FAL = 5,2 % des trames SPAWN et 4,3 % des trames longues (CSV0),
FlushLatentActions= 64 % des SPAWN (CSV0) et 77 % (CSV1) ; - chaîne des vêtements jusqu'à
getSkeletalMeshFromPath; 147 FAL sur 239DressSavedActual; trames de vêtements non longues ; - « FAL seul » long : 2 trames dans CSV0 (chargement initial), 0 dans CSV1.
- Estimé : le FAL des vêtements a lieu dans
getSkeletalMeshFromPath; le Blueprint du SPAWN est relancé par unDelaydu Room ; il n'y a pas de cache efficace au SPAWN (57 % des reprises rechargent) ; effets des voies P1-P3. - Incertain : le nœud et le paquet du FAL du SPAWN ; la source des chaînes de FAL après
LOAD ROOM; l'absence de la colonne FAL dans CSV1 ; les fonctions Blueprint qui font les 3,6 s deFlushLatentActions.
5. Hypothèse suivante proposée, et à quel niveau
- H7b (N3, lp-cart, puis mesure) : le coût des trames SPAWN vient du Blueprint relancé par actions latentes
(
GeneratePerson,RandomizeAll,LoadPreset, morphs, matériaux,PlayDaily), pas des chargements. Il faudrait le coût par fonction Blueprint (Unreal Insights ; que les noms des fonctions Blueprint y figurent est incertain) et les dumps ci-dessous. - Dumps manquants (lot STE-D9), arrêt sur ce point pour le nœud et le paquet du SPAWN (rien lancé ici ; éditeur fermé ;
lp-commandlets) :.\Tools\RunCommandlet.ps1 -Lot STE-D9 -Commandlet LPDumpFunction -Args '-BP=GI -Fn=getSkeletalMeshFromPath -Pins -Exec' .\Tools\RunCommandlet.ps1 -Lot STE-D9 -Commandlet LPDumpFunction -Args '-BP=GI -Fn=LoadHairMeshIfNeeded -Pins -Exec' .\Tools\RunCommandlet.ps1 -Lot STE-D9 -Commandlet LPDumpFunction -Args '-BP=GI -Fn=LoadOtherMeshIfNeeded_0 -Pins -Exec' .\Tools\RunCommandlet.ps1 -Lot STE-D9 -Commandlet LPDumpFunction -Args '-BP=GI -Fn=LoadDefaultTextures -Pins -Exec' .\Tools\RunCommandlet.ps1 -Lot STE-D9 -Commandlet LPDumpFunction -Args '-BP=GI -Fn=GeneratePerson -Pins -Exec' - Mesure pour nommer les paquets (même build, même parcours, sans
-llm -llmcsv) :& "D:\Game\LifePlay_Build_5.8_STE-J5_Development\Windows\LifePlay.exe" -csvGpuStats '-ExecCmds=TrackAsyncLoadRequests.Enable 1, csvprofile frames=9000'. Le rapportSaved\Profiling\AsyncLoadRequests_*.logest écrit à la fin de la capture (AsyncPackageLoader.cpp:398-401, 599). Il ne sépare pas les demandes synchrones des asynchrones (estimé) ; son format exact est incertain. - Pour la fiche H8 (information, sans toucher à son livrable) : avec la correction d'une trame, les 8 trames longues
GenerateRecsde CSV0 font 2 230 ms, dont 1 410 ms deTimerManager.
Questions à l'utilisateur
- Q-H7-1 : les FAL ne font que 5 % des trames SPAWN. Faut-il les traiter (P1, P2 ou P3), ou s'abstenir et ouvrir H7b sur
FlushLatentActions(64 à 77 %) ? - Q-H7-2 : lancer les 5 dumps STE-D9 du § 5 (nœud du FAL au SPAWN, contenu de
getSkeletalMeshFromPath) ? - Q-H7-3 : refaire une mesure avec
TrackAsyncLoadRequests.Enable 1, sans-llm -llmcsv, pour nommer les paquets ? - Q-H7-4 : si les FAL sont traités, quelle voie : P2 (vêtements asynchrones, local, gain invisible), P3 (cache, mémoire à plafonner) ou P1 (préchargement au lieu, gain ≈ 5 % au SPAWN, plus invasif) ?
- Q-H7-5 : faut-il corriger la méthode de classement des trames (décalage d'une trame de
mesure_h4.pyet deFrameTime) avant d'arbitrer H8, et reclasserSTE-H4-MESURE.md§ 4 ? - Q-H7-6 : pour P3, quel budget mémoire pour les vêtements gardés (à mesurer par
obj list class=SkeletalMeshet LLMMeshes) ?
Fichiers à commiter (dépôt jeu, branche ue5.8, ticket au gardien) :
- A
Docs/Migration/etat/ST/STE/STE-H7-DIAG.md
Tours : 46 / 60
Constats
- Outils hors Grep, Read et Glob, tous en lecture seule : 7 passages Python lus sur l'entrée standard
(
python -I - <M> <CSV0> <CSV1> <<'EOF' … EOF), aucun fichier écrit. Ils ont servi à l'alignement, aux classes, aux coûts et aux appariements des vêtements. Le code n'est pas rangé : l'outilTools/FrameStats.pydéjà proposé par STE-H4 devrait intégrer le décalage deFrameTime(§ 3.2). - Grep sur un dossier sous
Docs\Migration\etatsaute les.txt, ignorés par git (0 résultat surdumps\). Il faut passer le chemin du fichier. Grep sur des.uassetn'est pas probant :getSkeletalMeshFromPath→ 0 dansContent\BPs, alors que le nœud existe (PEG4:10519). getSkeletalMeshFromPathest une fonction de GI (appel GI_C::getSkeletalMeshFromPath, PEG4:10519). LMIN:145 l'affiche?::.- Rien n'a été lancé (éditeur, commandlet, build, jeu) ; aucune écriture dans
SourceniContent; aucune commande git qui écrit. - Nombre de tours estimé ;
python -I Tools/FicheStats.py --lot STEfait foi.