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:
+39
-8
@@ -80,14 +80,7 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Templates
|
||||
.Include(t => t.Attributes)
|
||||
.Include(t => t.Alarms)
|
||||
.Include(t => t.Scripts)
|
||||
.Include(t => t.Compositions)
|
||||
.Include(t => t.NativeAlarmSources)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync(cancellationToken);
|
||||
return await FullTemplateGraph().ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -143,6 +136,44 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
// One tracked, fully-included query per subtree depth (children of the frontier) and one per
|
||||
// ancestor level — the same entity shape as GetAllTemplatesAsync, because the reconcile
|
||||
// writes through these entities and compares script bodies (Gitea #86).
|
||||
public Task<IReadOnlyList<Template>> GetTemplateReconcileClosureAsync(int templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return TemplateReconcileClosure.CollectAsync(
|
||||
templateId,
|
||||
async (parentIds, ct) =>
|
||||
{
|
||||
var idList = parentIds.ToList();
|
||||
return await FullTemplateGraph()
|
||||
.Where(t => t.ParentTemplateId != null && idList.Contains(t.ParentTemplateId.Value))
|
||||
.ToListAsync(ct);
|
||||
},
|
||||
async (ids, ct) =>
|
||||
{
|
||||
var idList = ids.ToList();
|
||||
return await FullTemplateGraph()
|
||||
.Where(t => idList.Contains(t.Id))
|
||||
.ToListAsync(ct);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
// The tracked, fully-included template shape shared by GetAllTemplatesAsync and
|
||||
// GetTemplateReconcileClosureAsync.
|
||||
private IQueryable<Template> FullTemplateGraph()
|
||||
{
|
||||
return _context.Templates
|
||||
.Include(t => t.Attributes)
|
||||
.Include(t => t.Alarms)
|
||||
.Include(t => t.Scripts)
|
||||
.Include(t => t.Compositions)
|
||||
.Include(t => t.NativeAlarmSources)
|
||||
.AsSplitQuery();
|
||||
}
|
||||
|
||||
// The analysis projection shared by GetAllTemplatesForAnalysisAsync and
|
||||
// GetTemplateAnalysisClosureAsync (see the note on the former for why it is untracked and
|
||||
// drops TemplateScript.Code).
|
||||
|
||||
Reference in New Issue
Block a user