[Prestaties][P2] SVG-afbeeldingen (#2666): elke weergave leest en saneert opnieuw op de UI-isolate, flitst leeg en decodeert ingesloten PNG op 4096 px #2682

Closed
opened 2026-10-31 09:47:49 +00:00 by brenno · 4 comments
Owner

Symptoom

Sinds de SVG-import (#2666, 30-10) doet elke plek die een SVG-afbeelding toont het hele laadwerk opnieuw bij elke mount. Het gaat om de miniatuur, de editorpreview, de presentator (huidige én volgende dia), de beamer, het zoomvenster en de beeldkiezer. Per mount:

  • het bestand lezen (tot 64 MiB);
  • UTF-8 decoderen, XmlDocument.parse, de allow-list toepassen, opnieuw serialiseren en encoderen — synchroon op de UI-isolate;
  • daarna nog eens parsen in SvgPicture.string.

Tijdens het laden staat er een lege SizedBox. Bij een diawissel in presentator en beamer, bij scrollen door de dialijst en na elke editor-remount (undo/redo zet de editor op een nieuwe revisie) flitst het beeld dus weg.

Een SVG die alleen een PNG insluit (dat doen veel exporttools) wordt bij elke mount opnieuw op maximaal 4096 px gedecodeerd, langs de ImageCache heen.

Oorzaak

  • lib/widgets/safe_svg_image.dart:84-118: _load() hoort bij de State; tussen instanties is er geen cache. sanitizeImportedSvgBytes (lib/utils/sanitize_svg.dart:27-43, met XmlDocument.parse op r. 73) is synchroon.
  • r. 123-125: zolang ConnectionState != done volgt een lege SizedBox. De rasterroute gebruikt juist gaplessPlayback: true, omdat een leeg frame tussen dia's "fataal is bij video-opname" (lib/widgets/slides/previews/media_previews_image.dart:294-297).
  • r. 137: cappedMemoryImage(png) krijgt bij elke lading een nieuw Uint8List. De identiteitssleutel mist daardoor altijd, terwijl de documentatie van cappedMemoryImage (lib/utils/image_limits.dart:379-381) juist om hetzelfde object vraagt. Zonder maxEdge is er bovendien geen maat-decode, zoals #2625 die voor foto's invoerde.
  • De sync-IO-poort (#2636) ziet dit niet: hij telt alleen *Sync(-aanroepen, geen zwaar parsen.

Voorgestelde oplossing

  1. Een procesbrede LRU-cache die _SafeSvgData (de gesaneerde string plus het ingesloten-PNG-object) bewaart. Sleutel: (pad, mtime, grootte), of de bytes-identiteit bij mem:. Begrens hem in bytes (bijv. 32 MiB totaal). Dit is hetzelfde patroon als de contrast-memo (averageImageColor).
  2. Saneer boven een drempel (bijv. 256 KiB) in een isolate (Isolate.run). Kleinere SVG's mogen synchroon blijven.
  3. Laadfase: toon het vorige beeld tot het nieuwe klaar is (bewaar de laatste _SafeSvgData in de State), en lever een cache-hit synchroon af via SynchronousFuture. Dan is er geen leeg frame meer.
  4. Ingesloten PNG: laat hem via ResolvedDecodeImage lopen, met cappedMemoryImage(png, maxEdge: plan.edge) en het gecachete Uint8List. Dan werken de ImageCache en de maat-trede van #2625 weer.

Wat overeind moet blijven

  • Elke SVG gaat door de allow-list voordat hij getekend wordt. De cache bewaart alleen het gesaneerde resultaat.
  • De projectgrens blijft bij de lezing gecontroleerd (containedRealPathOrNull); een ontsnapping geeft de outsideDeck-placeholder.
  • Een ongeldige of te grote SVG gaat naar de errorBuilder.

Toets

  • Unit: twee SafeSvgImage.file op hetzelfde pad saneren samen één keer (teller-naad).
  • Widget: wissel tussen twee dia's met dezelfde SVG. Geen enkel frame toont de lege SizedBox.
  • Meting zoals in EPIC #2637: saneertijd voor SVG's van 100 KiB, 1 MiB en 10 MiB, vóór en na. Nog niet gemeten.

Gevonden bij codelezing tijdens de vervolginspectie van 31-10-2026. Deze code landde ná de prestatieronde van 29-10.

## Symptoom Sinds de SVG-import (#2666, 30-10) doet elke plek die een SVG-afbeelding toont het hele laadwerk opnieuw bij **elke mount**. Het gaat om de miniatuur, de editorpreview, de presentator (huidige én volgende dia), de beamer, het zoomvenster en de beeldkiezer. Per mount: - het bestand lezen (tot 64 MiB); - UTF-8 decoderen, `XmlDocument.parse`, de allow-list toepassen, opnieuw serialiseren en encoderen — **synchroon op de UI-isolate**; - daarna nog eens parsen in `SvgPicture.string`. Tijdens het laden staat er een lege `SizedBox`. Bij een diawissel in presentator en beamer, bij scrollen door de dialijst en na elke editor-remount (undo/redo zet de editor op een nieuwe revisie) flitst het beeld dus weg. Een SVG die alleen een PNG insluit (dat doen veel exporttools) wordt bij elke mount opnieuw op maximaal 4096 px gedecodeerd, langs de ImageCache heen. ## Oorzaak - `lib/widgets/safe_svg_image.dart:84-118`: `_load()` hoort bij de State; tussen instanties is er geen cache. `sanitizeImportedSvgBytes` (`lib/utils/sanitize_svg.dart:27-43`, met `XmlDocument.parse` op r. 73) is synchroon. - r. 123-125: zolang `ConnectionState != done` volgt een lege `SizedBox`. De rasterroute gebruikt juist `gaplessPlayback: true`, omdat een leeg frame tussen dia's "fataal is bij video-opname" (`lib/widgets/slides/previews/media_previews_image.dart:294-297`). - r. 137: `cappedMemoryImage(png)` krijgt bij elke lading een nieuw `Uint8List`. De identiteitssleutel mist daardoor altijd, terwijl de documentatie van `cappedMemoryImage` (`lib/utils/image_limits.dart:379-381`) juist om hetzelfde object vraagt. Zonder `maxEdge` is er bovendien geen maat-decode, zoals #2625 die voor foto's invoerde. - De sync-IO-poort (#2636) ziet dit niet: hij telt alleen `*Sync(`-aanroepen, geen zwaar parsen. ## Voorgestelde oplossing 1. Een procesbrede LRU-cache die `_SafeSvgData` (de gesaneerde string plus het ingesloten-PNG-object) bewaart. Sleutel: (pad, mtime, grootte), of de bytes-identiteit bij `mem:`. Begrens hem in bytes (bijv. 32 MiB totaal). Dit is hetzelfde patroon als de contrast-memo (`averageImageColor`). 2. Saneer boven een drempel (bijv. 256 KiB) in een isolate (`Isolate.run`). Kleinere SVG's mogen synchroon blijven. 3. Laadfase: toon het vorige beeld tot het nieuwe klaar is (bewaar de laatste `_SafeSvgData` in de State), en lever een cache-hit synchroon af via `SynchronousFuture`. Dan is er geen leeg frame meer. 4. Ingesloten PNG: laat hem via `ResolvedDecodeImage` lopen, met `cappedMemoryImage(png, maxEdge: plan.edge)` en het **gecachete** `Uint8List`. Dan werken de ImageCache en de maat-trede van #2625 weer. ## Wat overeind moet blijven - Elke SVG gaat door de allow-list voordat hij getekend wordt. De cache bewaart alleen het gesaneerde resultaat. - De projectgrens blijft bij de lezing gecontroleerd (`containedRealPathOrNull`); een ontsnapping geeft de outsideDeck-placeholder. - Een ongeldige of te grote SVG gaat naar de `errorBuilder`. ## Toets - Unit: twee `SafeSvgImage.file` op hetzelfde pad saneren samen één keer (teller-naad). - Widget: wissel tussen twee dia's met dezelfde SVG. Geen enkel frame toont de lege `SizedBox`. - Meting zoals in EPIC #2637: saneertijd voor SVG's van 100 KiB, 1 MiB en 10 MiB, vóór en na. Nog niet gemeten. Gevonden bij codelezing tijdens de vervolginspectie van 31-10-2026. Deze code landde ná de prestatieronde van 29-10.
Author
Owner

Opgepakt. Tak: fix/vervolginspectie-3110. Verwachte reikwijdte: de bestanden die in de oorzaak van dit issue staan, plus een regressietest.

Opgepakt. Tak: fix/vervolginspectie-3110. Verwachte reikwijdte: de bestanden die in de oorzaak van dit issue staan, plus een regressietest.
Author
Owner

Opgepakt. Tak: fix/vervolginspectie-3110. Verwachte reikwijdte: lib/widgets/safe_svg_image.dart, lib/utils/sanitize_svg.dart, lib/utils/image_limits.dart.

Opgepakt. Tak: fix/vervolginspectie-3110. Verwachte reikwijdte: lib/widgets/safe_svg_image.dart, lib/utils/sanitize_svg.dart, lib/utils/image_limits.dart.
Author
Owner

Staat op main in a6b4dc066 (9bad356e5). Een geïmporteerde SVG wordt één keer opgeschoond en blijft staan bij een diawissel. Saneertijden van 100 KiB, 1 MiB en 10 MiB zijn niet gemeten.

Staat op main in a6b4dc066 (9bad356e5). Een geïmporteerde SVG wordt één keer opgeschoond en blijft staan bij een diawissel. Saneertijden van 100 KiB, 1 MiB en 10 MiB zijn niet gemeten.
Author
Owner

Opgelost op main in a6b4dc066. De zin „elke mount leest en saneert opnieuw en flitst leeg“ is gedekt door de procescache en het blijven staan van het vorige beeld in lib/widgets/safe_svg_image.dart. Niet meegenomen: de saneertijden van 100 KiB, 1 MiB en 10 MiB zijn niet gemeten.

Opgelost op main in a6b4dc066. De zin „elke mount leest en saneert opnieuw en flitst leeg“ is gedekt door de procescache en het blijven staan van het vorige beeld in lib/widgets/safe_svg_image.dart. Niet meegenomen: de saneertijden van 100 KiB, 1 MiB en 10 MiB zijn niet gemeten.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#2682
No description provided.