perf(preview): trois optimisations du chemin de preview, appliquées une par une - #206
Merged
Merged
Conversation
…ur pour rien
`Decoder::seek_to` faisait systématiquement `av_seek_frame(BACKWARD)` +
`avcodec_flush_buffers`, puis redécodait depuis l'image clé précédente — jusqu'à
`gop_size` frames (60 sur nos captures), et deux fois puisque l'écran et la webcam
ont chacun leur décodeur. Mesuré à ~50 ms par pas de scrub, sur le chemin exact de
l'édition en pause.
Or le cas dominant n'est pas un saut : c'est « la frame suivante » (scrub,
pas-à-pas) ou « la même frame » (un paramètre a changé, la scène est recomposée au
même instant). Deux chemins rapides couvrent ça :
1. la frame courante est déjà celle demandée (à moins d'une demi-durée de frame)
→ on la rend, zéro décodage ;
2. la cible est devant et à moins de 0,5 s → on déroule depuis la position
courante, sans jeter l'état.
Recul ou saut lointain : seek complet, inchangé. Le seuil de 0,5 s est le demi-GOP
de nos captures, au-delà duquel repartir d'une clé redevient moins cher.
Le critère d'arrêt de `decode_forward_to` est la MÊME expression que celui du seek
complet, donc les deux chemins rendent la même frame : optimisation, pas changement
de comportement. `cur_pts` est nécessaire parce qu'un `AVFrame` fraîchement alloué a
un `best_effort_timestamp` indéterminé — sans lui, impossible de savoir si l'état
courant est exploitable ; il est remis à `None` à chaque flush.
Première optimisation appliquée seule sur une branche partie de release/v1.8.0,
après qu'une pile de sept changements simultanés se soit révélée impossible à
attribuer. Vérifié : 101 tests du crate. NON vérifié : le ressenti au scrub, et le
comportement au franchissement de clip.
…'arrêt
Le tick rAF de `VirtualPreview` publiait `updateVirtualTime(...)` dérivé de
`v.currentTime` en permanence, y compris quand la lecture est arrêtée. Pendant la
lecture c'est la bonne source — le média avance, la tête le suit. À l'arrêt c'est
l'inverse : l'utilisateur possède la tête de lecture et le `<video>` doit la SUIVRE.
Deux symptômes en découlaient :
- un scrub était écrasé à chaque frame par la position d'un élément encore en
train de chercher ;
- près d'une frontière de clips, `locateSourcePosition` ne résolvait pas, et le
repli `seekToVirtualTimeRef(nextClip.timelineStartSec)` renvoyait la tête au
DÉBUT du clip voisin — le tressaillement au passage d'un clip à l'autre, visible
dans les deux sens.
`clockRef` et `setSourceTimeSec` continuent d'être publiés : la webcam et le calque
curseur ont besoin du temps source même à l'arrêt. Seule la position de la TIMELINE
cesse d'être dictée par le média.
Deuxième optimisation appliquée seule. Correctif déjà constaté efficace sur le saut
du curseur lors d'un essai précédent, mais il y était noyé parmi sept changements
simultanés, donc non attribuable ; isolé ici pour être jugé pour lui-même.
À surveiller : ce tick réécrivait aussi la position 60 fois par seconde, ce qui
faisait re-déclencher la bascule de clip par accident jusqu'à ce qu'elle prenne.
Si le passage d'un clip à l'autre se dégrade, c'est que cette réparation par
tremblement masquait un défaut du verrou de bascule dans `NativeCompositorOverlay`
— à traiter alors comme un troisième correctif distinct.
Vérifié : tsc, 66/66 tests src/components/ai-edition.
`latest_frame_since` clonait le buffer RGBA à chaque livraison — un memcpy `O(w·h)` mesuré à 1,5 ms en 1280×720 et 3,4 ms en 1920×1080, payé sur le THREAD PRINCIPAL de Node, celui-là même qui doit rester libre pour que React peigne la tête de lecture. Le buffer est désormais EMPORTÉ (`take`). Le thread de rendu le remplace à chaque frame composée et le consommateur garde ses pixels peints sur le canvas : personne ne relit jamais la même génération, donc la copie ne servait à rien. La génération passe dans un `AtomicU64` séparé : elle était dérivée du slot (`slot.map(|g| g+1).unwrap_or(1)`), or vider le slot la ferait repartir à 1 — le consommateur recevrait des générations déjà peintes et boucherait. Séquence identique à l'ancienne tant que le slot n'est pas vidé. Contrepartie assumée : une relecture forcée (`sinceGen = 0`) après la première ne retrouve rien tant qu'une nouvelle frame n'est pas composée. Sans conséquence ici — le seul consommateur ne l'utilise qu'au montage, et un redimensionnement provoque de toute façon une recomposition. Troisième optimisation appliquée seule. Invisible par construction : elle libère du temps de thread principal, elle ne change aucun pixel. Le critère de validation est donc « rien ne se dégrade », pas « on voit mieux ». Vérifié : 101 tests du crate.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Trois changements indépendants sur le chemin de preview, chacun isolé dans son
commit et validé séparément dans l'app avant d'appliquer le suivant.
Ce que ça contient
ef6705c3seek_tone jette plus l'état du décodeur quand la cible est la frame courante ou à moins de 0,5 s devant45a945c8<video>ne dicte plus la position de la timeline à l'arrêt5850228elatest_frame_sinceemporte le buffer au lieu de le clonerPourquoi ces trois-là
seek_tofaisait systématiquementav_seek_frame(BACKWARD)+avcodec_flush_bufferspuis redécodait depuis l'image clé précédente — jusqu'à 60 frames, deux fois (écran +
webcam), soit ~50 ms par pas de scrub mesurés. Le cas dominant en édition n'est pourtant
pas un saut mais « la frame suivante » ou « la même frame ». Le critère d'arrêt du
déroulement est la même expression que celui du seek complet : les deux chemins rendent
la même frame.
Le tick rAF de
VirtualPreviewpubliait la position de la timeline en permanence, ycompris à l'arrêt. Or à l'arrêt c'est l'utilisateur qui possède la tête de lecture et le
<video>doit la suivre. Sans ce garde, un scrub était écrasé à chaque frame par laposition d'un élément encore en train de chercher, et près d'une frontière de clips le
repli
seekToVirtualTimeRef(nextClip.timelineStartSec)renvoyait la tête au début du clipvoisin.
latest_frame_sinceclonait le buffer RGBA à chaque livraison — 1,5 ms en 1280×720,3,4 ms en 1920×1080 — sur le thread principal de Node, celui qui doit rester libre pour
que React peigne la tête de lecture. La génération passe dans un
AtomicU64séparé, sansquoi vider le slot la ferait repartir à 1 et rejouerait des générations déjà peintes.
Méthode
Une première tentative empilait sept changements simultanés et s'est révélée impossible à
attribuer : des régressions (décalage audio/vidéo, mauvais clip affiché) coexistaient avec
des gains réels sans qu'on puisse dire lequel venait d'où. Elle a été abandonnée au profit
de cette branche, repartie de
release/v1.8.0avec un changement à la fois et un testutilisateur entre chaque.
Ce qui n'est PAS dedans, délibérément
VideoDecodercôté renderer).Techniquement fonctionnel et mesuré — 2,4 Ko/frame au lieu de 3,08 Mo, encodage 0,97 ms,
un vsync d'entrée dans Chromium contre trois pour MSE +
<video>. Mais il ajoute unelatence vidéo que l'audio, qui sort du
<video>du DOM, ne suit pas : décalage A/V parconstruction, absent de la référence. À reprendre avec une vraie stratégie de synchro.
DO_NOT_WAIT). Gain mesuré important à grandetaille de panneau (1080p 18,5 → 40,3 fps, attente GPU 11 → 0,4 ms), mais il publie la
frame N−1, donc faux à l'arrêt. Le garde-fou correspondant n'a pas pu être validé : le
compositing en pause n'est pas reproductible dans ce projet (3 revisites sur 6 au même
timestamp donnent des images différentes, y compris sur du code non modifié), donc aucun
oracle d'image ne peut trancher aujourd'hui.
Vérifié
cargo test -p openscreen-compositor: 101 tests.tsc --noEmitpropre. Tests vitest deszones touchées. Chaque commit testé dans l'app avant d'appliquer le suivant.
Non vérifié
Le gain du chemin rapide de seek n'est pas perceptible à l'usage — il est mesuré, pas
ressenti. Il reste défendable comme retrait de gaspillage, mais si vous préférez une
branche strictement composée de changements perçus,
ef6705c3est celui à retirer.