From b032d7fe48fa3e071d5b480e7c64aa07eb8e951c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 08:37:20 -0400 Subject: [PATCH] fix(centralui): bound FlowDebugService script cache (review 2026-10-21 07/M-2) --- .../Services/FlowDebugService.cs | 11 +++++++++++ .../Services/FlowDebugServiceTests.cs | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/FlowDebugService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/FlowDebugService.cs index 4c7c106c..f0c090c2 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/FlowDebugService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/FlowDebugService.cs @@ -107,6 +107,13 @@ public sealed class FlowDebugService : IFlowDebugService /// private readonly Dictionary _scriptCache = new(); + /// + /// Ceiling on entries (review 2026-10-21 07/M-2). Only the most + /// recently selected instance is ever read back, so a small bound loses nothing; reaching it + /// drops the whole cache before the new entry goes in. + /// + internal const int ScriptCacheCapacity = 8; + /// One instance's flattened scripts: the unique name the site keys on, the raw resolved scripts, and the picker projection. private sealed record InstanceScripts( string UniqueName, @@ -484,6 +491,10 @@ public sealed class FlowDebugService : IFlowDebugService .ToList(); var loaded = new InstanceScripts(instance.UniqueName, scripts, options); + if (_scriptCache.Count >= ScriptCacheCapacity && !_scriptCache.ContainsKey(instanceId)) + { + _scriptCache.Clear(); + } _scriptCache[instanceId] = loaded; return loaded; } diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Services/FlowDebugServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Services/FlowDebugServiceTests.cs index a194447e..4b940953 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Services/FlowDebugServiceTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Services/FlowDebugServiceTests.cs @@ -677,6 +677,24 @@ public sealed class FlowDebugServiceTests : IDisposable Assert.False(options.Single(o => o.CanonicalName == "Mod.Tick").IsWriteKind); } + /// Verifies the per-circuit script cache stays bounded however many instances are browsed (review 2026-10-21 07/M-2). + /// A task that represents the asynchronous operation. + [Fact] + public async Task GetInstanceScripts_BrowsingManyInstances_KeepsCacheBounded() + { + var svc = CreateService(reply: null); + for (var id = 100; id < 100 + 3 * FlowDebugService.ScriptCacheCapacity; id++) + { + WireFlattened(id, "Sim" + id, new ResolvedScript { CanonicalName = "Manual", Code = "x", TriggerType = "Call" }); + await svc.GetInstanceScriptsAsync(id); + } + + var cache = (System.Collections.IDictionary)typeof(FlowDebugService) + .GetField("_scriptCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .GetValue(svc)!; + Assert.True(cache.Count <= FlowDebugService.ScriptCacheCapacity); + } + /// Verifies that requesting scripts for an unknown instance yields an empty result. /// A task that represents the asynchronous operation. [Fact]