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:
@@ -1268,18 +1268,14 @@ public class TemplateService
|
||||
/// <param name="user">Username for the audit row.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <param name="loadedTemplates">
|
||||
/// The full, TRACKED template graph if the caller already has one. Reconciling
|
||||
/// writes through these entities and compares script bodies, so this must be
|
||||
/// the tracked <c>GetAllTemplatesAsync</c> shape — never the no-tracking,
|
||||
/// body-free analysis projection. When <see langword="null"/> the graph is
|
||||
/// loaded here.
|
||||
///
|
||||
/// <para>
|
||||
/// Passing it matters because every member Add/Update/Delete on
|
||||
/// <see cref="TemplateService"/> used to load the whole graph once for its own
|
||||
/// checks and then again inside this method. Handing the graph over collapses
|
||||
/// that to one load per operation.
|
||||
/// </para>
|
||||
/// A TRACKED template graph the caller already has, containing at least the
|
||||
/// template's derived subtree and inheritance chain. Reconciling writes through
|
||||
/// these entities and compares script bodies, so this must be the tracked
|
||||
/// <c>GetAllTemplatesAsync</c> shape — never the no-tracking, body-free analysis
|
||||
/// projection. When <see langword="null"/> only the reconcile closure (the
|
||||
/// subtree plus the template's chain, Gitea #86) is loaded here — never the
|
||||
/// whole library, and nothing beyond one probe query when the template has no
|
||||
/// children.
|
||||
/// </param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task ReconcileDescendantsAsync(
|
||||
@@ -1288,7 +1284,12 @@ public class TemplateService
|
||||
CancellationToken cancellationToken = default,
|
||||
IReadOnlyList<Template>? loadedTemplates = null)
|
||||
{
|
||||
var all = loadedTemplates ?? await _repository.GetAllTemplatesAsync(cancellationToken);
|
||||
// The descendants' chains run through templateId and on up its own chain, and
|
||||
// the resolver is inheritance-only, so the subtree + the template's chain is
|
||||
// everything the reconcile reads (#86).
|
||||
var all = loadedTemplates
|
||||
?? await _repository.GetTemplateReconcileClosureAsync(templateId, cancellationToken)
|
||||
?? Array.Empty<Template>();
|
||||
var descendants = GetDescendantIdsBreadthFirst(templateId, all);
|
||||
if (descendants.Count == 0)
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user