[Bug] The export dialog runs six full deck scans on the UI thread before it paints #613

Closed
opened 2026-07-22 16:19:56 +00:00 by brenno · 2 comments
Owner

Found in the pre-publication performance review.

Evidence: lib/widgets/app_shell_main_layout.dart:769bundleFor performs three unmemoised full deck scans per call: PrivacyProjection.forAudience (privacy_projection.dart:170), RedactionManifestService.buildredactedValues (redaction_manifest_service.dart:140), and summarisePrivacyForExport(source, PrivacyScanner(…).scan(source)) (app_shell_main_layout.dart:815), plus generateDeck and _expandForExport.

It is called at :821 (for hasPrivacyFindings) and again at lib/widgets/dialogs/export_dialog.dart:166 with the same arguments — both synchronously, before the dialog first frame. Cost per scan, by the repository own measurement in lib/services/privacy/privacy_scan_memo.dart:14: ~1.07 ms per slide, so ~208 ms at 200 slides, and that is the unexpanded count. Every profile change in the dialog (export_dialog.dart:172) costs another three.

Why this matters now: six full scans plus two manifest builds on the UI thread is roughly a second and a half of frozen window at 200 slides, with no spinner, immediately after clicking Export. The rasterising path afterwards is exemplary — progress, phases and cancellation are fully wired (slide_rasterizer.dart:97-102, export_dialog.dart:349-367) — so the only place the export feels like it has hung is the place that is not doing anything yet.

Proposal: derive hasPrivacyFindings from the already memoised privacyRawScanProvider rather than from a full bundle, and let redactedValues and summarise reuse the projection scan within a single bundleFor instead of repeating it. One scan per bundle, one bundle for the dialog.

Found in the pre-publication performance review. **Evidence:** `lib/widgets/app_shell_main_layout.dart:769` — `bundleFor` performs **three** unmemoised full deck scans per call: `PrivacyProjection.forAudience` (`privacy_projection.dart:170`), `RedactionManifestService.build` → `redactedValues` (`redaction_manifest_service.dart:140`), and `summarisePrivacyForExport(source, PrivacyScanner(…).scan(source))` (`app_shell_main_layout.dart:815`), plus `generateDeck` and `_expandForExport`. It is called at `:821` (for `hasPrivacyFindings`) *and again* at `lib/widgets/dialogs/export_dialog.dart:166` with the same arguments — both synchronously, before the dialog first frame. Cost per scan, by the repository own measurement in `lib/services/privacy/privacy_scan_memo.dart:14`: ~1.07 ms per slide, so ~208 ms at 200 slides, and that is the unexpanded count. Every profile change in the dialog (`export_dialog.dart:172`) costs another three. **Why this matters now:** six full scans plus two manifest builds on the UI thread is roughly a second and a half of frozen window at 200 slides, with no spinner, immediately after clicking Export. The rasterising path afterwards is exemplary — progress, phases and cancellation are fully wired (`slide_rasterizer.dart:97-102`, `export_dialog.dart:349-367`) — so the only place the export feels like it has hung is the place that is not doing anything yet. **Proposal:** derive `hasPrivacyFindings` from the already memoised `privacyRawScanProvider` rather than from a full bundle, and let `redactedValues` and `summarise` reuse the projection scan within a single `bundleFor` instead of repeating it. One scan per bundle, one bundle for the dialog.
Author
Owner

Opgepakt. Tak: perf/exportdialoog-scans-613. Reikwijdte: app_shell_main_layout.dart (bundleFor + hasPrivacyFindings), privacy_projection.dart en redaction_manifest_service.dart (een al gemaakte scan mogen meekrijgen), plus tests.

Opgepakt. Tak: `perf/exportdialoog-scans-613`. Reikwijdte: `app_shell_main_layout.dart` (`bundleFor` + `hasPrivacyFindings`), `privacy_projection.dart` en `redaction_manifest_service.dart` (een al gemaakte scan mogen meekrijgen), plus tests.
Author
Owner

Opgelost in b5ea59c2 + 35830c70 + de dekkingstest (PR #680), op main.

Beide voorstellen uit je issue zitten erin: hasPrivacyFindings komt uit privacyExportSummaryProvider in plaats van uit een hele bundel, en forAudience / RedactionManifestService nemen desgewenst een al gemaakte scan mee. Laten ze hem weg, dan scannen ze zelf zoals voorheen — elke andere aanroeper blijft ongemoeid. Zes scans werden er één, plus één per profiel- of diepgangkeuze.

De test bewaakt niet de winst maar de voorwaarde. Een scan tellen kan niet van buitenaf; of de meegegeven scan werkelijk gebruikt wordt wél. Een lege scan meegeven moet niets redigeren en een leeg manifest opleveren — doet de code dat niet, dan scande ze alsnog zelf en is de herhaling er gewoon nog, hoe groen de rest ook staat.

Twee dingen die erbij hoorden. De wijziging duwde _MainLayoutState over zijn klasseplafond, dus is de bundelfabriek verhuisd naar lib/services/export_bundle.dart — het is exportlogica, geen schermlogica, en hij hoort naast ExportBundle, in het bestand waarvan de kop de projectiegrens al beschrijft die deze functie bewaakt. Daarmee viel meteen de dekkingsvloer: dat bestand stond op 6,7%, omdat de fabriek tot nu toe alleen via een widgettest bereikbaar was en daar nooit doorlopen werd. Nu is hij gewoon een functie met invoer en uitvoer, en heeft hij zeven tests. Dat is achteraf de grootste opbrengst van deze wijziging.

Wat er níét in zit: het rasteren zelf blijft even duur; daar zat het probleem niet — dat pad had al voortgang, fasen en annuleren.

Opgelost in `b5ea59c2` + `35830c70` + de dekkingstest (PR #680), op main. Beide voorstellen uit je issue zitten erin: `hasPrivacyFindings` komt uit `privacyExportSummaryProvider` in plaats van uit een hele bundel, en `forAudience` / `RedactionManifestService` nemen desgewenst een al gemaakte scan mee. Laten ze hem weg, dan scannen ze zelf zoals voorheen — elke andere aanroeper blijft ongemoeid. Zes scans werden er één, plus één per profiel- of diepgangkeuze. **De test bewaakt niet de winst maar de voorwaarde.** Een scan tellen kan niet van buitenaf; of de meegegeven scan werkelijk gebruikt wordt wél. Een lege scan meegeven moet niets redigeren en een leeg manifest opleveren — doet de code dat niet, dan scande ze alsnog zelf en is de herhaling er gewoon nog, hoe groen de rest ook staat. **Twee dingen die erbij hoorden.** De wijziging duwde `_MainLayoutState` over zijn klasseplafond, dus is de bundelfabriek verhuisd naar `lib/services/export_bundle.dart` — het is exportlogica, geen schermlogica, en hij hoort naast `ExportBundle`, in het bestand waarvan de kop de projectiegrens al beschrijft die deze functie bewaakt. Daarmee viel meteen de dekkingsvloer: dat bestand stond op 6,7%, omdat de fabriek tot nu toe alleen via een widgettest bereikbaar was en daar nooit doorlopen werd. Nu is hij gewoon een functie met invoer en uitvoer, en heeft hij zeven tests. Dat is achteraf de grootste opbrengst van deze wijziging. **Wat er níét in zit:** het rasteren zelf blijft even duur; daar zat het probleem niet — dat pad had al voortgang, fasen en annuleren.
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#613
No description provided.