perf(templates): descendant reconcile loads only the subtree + chain (ScadaBridge#86)
TemplateService.ReconcileDescendantsAsync - run after every base member add/update/delete, template and native-alarm-source alike - loaded the whole tracked template graph (every template, member and script body) on each call, which after #66 was the dominant cost of those paths. It now calls the new ITemplateEngineRepository.GetTemplateReconcileClosureAsync: the same tracked, fully-included shape as GetAllTemplatesAsync, restricted to the edited template's derived subtree plus its own inheritance chain. The reconcile is inheritance-only (TemplateInheritanceResolver never follows compositions) and every descendant's chain runs through the edited template, so that set is everything it reads. The walk (Commons TemplateReconcileClosure, the descendant-direction twin of #66's TemplateAnalysisClosure) loads one children batch per subtree depth and one batch per ancestor level, terminates on parent cycles and stops at dangling parents. A template with no children - the common case - costs one empty probe and loads nothing. Evidence: - TemplateReconcileClosureTests: for every template of a graph with a deep subtree, an ancestor above the edited template, a composed module, look-alike unrelated trees, a parent cycle and a dangling parent, the closure-backed reconcile issues exactly the writes (add/update/delete, audit, save) of the old whole-graph reconcile, and the resolver answers identically for every descendant. Fails with the old service (whole-graph load wired to throw) and with the ancestor walk removed (mutation-checked). - TemplateReconcileClosureRepositoryTests (SQLite, recorded SQL): 5 templates / 10 attributes instead of 47 / 94, tracked with script bodies; a leaf costs one probe query and tracks nothing. - TemplateServiceTests: attribute add + update propagate two levels down with GetAllTemplatesAsync wired to throw; existing propagation tests now run against the closure via ServeReconcileClosureFromFullGraph. ResyncInheritedMembersAsync (operator action) still reads the whole graph. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -219,17 +219,22 @@ names, locks and edges: `InstanceService.SetAlarmOverrideAsync` (resolving one a
|
||||
name) and `TemplateDeletionService.CanDeleteTemplateAsync` (inheritor / derived /
|
||||
composer checks). `TemplateFolderService.DeleteFolderAsync`, which only counts
|
||||
templates per `FolderId`, reads the lighter `GetTemplateStubsAsync` identity projection
|
||||
(review 2026-10-21 04/P6, 08/P1). Template member add/update paths still load the whole
|
||||
analysis projection for collision detection; narrowing that to the edited template's
|
||||
chain + composition closure needs a closure-scoped repository read that does not exist
|
||||
yet.
|
||||
(review 2026-10-21 04/P6, 08/P1). Template member add/update paths read only the
|
||||
edited template's inheritance chain + composition closure through
|
||||
`GetTemplateAnalysisClosureAsync` (the same projection, one query per closure depth —
|
||||
Gitea #66).
|
||||
|
||||
The tracked, body-bearing `GetAllTemplatesAsync` remains the read for the two paths
|
||||
that genuinely need it — the inheritance reconciler (which compares and copies
|
||||
script bodies and writes through the loaded entities) and the flattener (which must
|
||||
observe rows an in-flight bundle import has staged on the shared change tracker).
|
||||
`ReconcileDescendantsAsync` additionally accepts an already-loaded tracked graph so
|
||||
a caller holding one does not force a second load.
|
||||
The tracked, body-bearing shape remains the read for the two paths that genuinely need
|
||||
it — the inheritance reconciler (which compares and copies script bodies and writes
|
||||
through the loaded entities) and the flattener (which must observe rows an in-flight
|
||||
bundle import has staged on the shared change tracker). The automatic post-mutation
|
||||
`ReconcileDescendantsAsync` reads only `GetTemplateReconcileClosureAsync` — the edited
|
||||
template's derived subtree plus its own inheritance chain, tracked and fully included,
|
||||
one load per subtree depth and chain level (Gitea #86). The reconcile is
|
||||
inheritance-only, so every descendant's chain lies inside that set; a template with no
|
||||
children costs a single empty probe query. It still accepts an already-loaded tracked
|
||||
graph from a caller holding one. The operator `ResyncInheritedMembersAsync` and the
|
||||
flattener keep reading the whole tracked graph via `GetAllTemplatesAsync`.
|
||||
|
||||
### Native Alarm Source Resolution
|
||||
|
||||
|
||||
Reference in New Issue
Block a user