TemplateService: ReconcileDescendantsAsync still loads the whole tracked template graph on member add/update #86

Closed
opened 2026-11-09 12:55:16 -05:00 by dohertj2 · 1 comment
Owner

After #66, member add/update loads only the analysis closure for collision/lock checks, but TemplateService.ReconcileDescendantsAsync still calls GetAllTemplatesAsync (tracked, with script bodies) on every add/update to propagate to derived templates. That is now the dominant cost of these paths on a large library. Load only the edited template's derived descendants plus what they resolve against (reuse the #66 closure walk in the descendant direction), with an equivalence test like #66's.

After #66, member add/update loads only the analysis closure for collision/lock checks, but `TemplateService.ReconcileDescendantsAsync` still calls `GetAllTemplatesAsync` (tracked, with script bodies) on every add/update to propagate to derived templates. That is now the dominant cost of these paths on a large library. Load only the edited template's derived descendants plus what they resolve against (reuse the #66 closure walk in the descendant direction), with an equivalence test like #66's.
dohertj2 added the type:debtpriority:p3 labels 2026-11-09 12:55:16 -05:00
Author
Owner

Fixed on main c29b99c6: ReconcileDescendantsAsync loads only the edited template's derived subtree + its parent chain (new TemplateReconcileClosure — the #66 walk run downward; repo GetTemplateReconcileClosureAsync, same tracked fully-loaded shape as GetAllTemplatesAsync). Compositions not needed (reconcile follows inheritance only). A leaf template costs one empty query and loads nothing. Equivalence: TemplateReconcileClosureTests (deep subtree, ancestor, composed module, look-alike trees, parent cycle, dangling parent) — identical adds/updates/deletes/audit rows/saves and resolver output for every template; 17 tests fail on the old service, 24 with the ancestor walk removed. Load: 5 templates / 10 attributes vs 47 / 94 (recorded SQL on SQLite). Trade-off: subtree case = 4 loads + 1 empty query instead of one whole-graph load, each bounded by the subtree. Evidence: TemplateEngine 574/574, ConfigurationDatabase 326 (133 skipped), ManagementService 721/721; re-run green after rebasing onto #79/#71/#69; slnx 0 warnings.

Fixed on main c29b99c6: ReconcileDescendantsAsync loads only the edited template's derived subtree + its parent chain (new TemplateReconcileClosure — the #66 walk run downward; repo `GetTemplateReconcileClosureAsync`, same tracked fully-loaded shape as GetAllTemplatesAsync). Compositions not needed (reconcile follows inheritance only). A leaf template costs one empty query and loads nothing. Equivalence: TemplateReconcileClosureTests (deep subtree, ancestor, composed module, look-alike trees, parent cycle, dangling parent) — identical adds/updates/deletes/audit rows/saves and resolver output for every template; 17 tests fail on the old service, 24 with the ancestor walk removed. Load: 5 templates / 10 attributes vs 47 / 94 (recorded SQL on SQLite). Trade-off: subtree case = 4 loads + 1 empty query instead of one whole-graph load, each bounded by the subtree. Evidence: TemplateEngine 574/574, ConfigurationDatabase 326 (133 skipped), ManagementService 721/721; re-run green after rebasing onto #79/#71/#69; slnx 0 warnings.
Sign in to join this conversation.