diff --git a/MainWindow.IoTesting.cs b/MainWindow.IoTesting.cs index 142c521a9..7ca7eb2cc 100644 --- a/MainWindow.IoTesting.cs +++ b/MainWindow.IoTesting.cs @@ -21,6 +21,7 @@ public partial class MainWindow private IoTestSessionController? _activeIoTestSessionController; private long _ioTestObservationSequence; private int? _pollingIntervalBeforeIoFat; + private int _ioFatEvidenceDrainDispatchActive; protected override void OnInitialized(EventArgs e) { @@ -401,9 +402,37 @@ private IoTestSessionController CreateIoTestSession(IoTestProject project, strin => new( project, ResolveIoTestDevice, - action => Dispatcher.BeginInvoke(action, DispatcherPriority.Background), + DispatchIoFatEvidence, evidenceRoot); + private void DispatchIoFatEvidence(Action action) + { + ArgumentNullException.ThrowIfNull(action); + + // A newly observed edge must reach the FAT evidence evaluator before normal UI + // background work, preserving the immediate TRUE-edge behavior from #150. When + // the bounded drain reschedules itself from inside the current callback, lower the + // continuation to Background so Render/Input are not starved by a large snapshot burst. + var priority = Volatile.Read(ref _ioFatEvidenceDrainDispatchActive) == 0 + ? DispatcherPriority.DataBind + : DispatcherPriority.Background; + + Dispatcher.BeginInvoke( + new Action(() => + { + Interlocked.Increment(ref _ioFatEvidenceDrainDispatchActive); + try + { + action(); + } + finally + { + Interlocked.Decrement(ref _ioFatEvidenceDrainDispatchActive); + } + }), + priority); + } + private static string IoTestingProjectsRoot() => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ARSAS", diff --git a/tests/ARSAS.Tests/IoTestFatP0RegressionTests.cs b/tests/ARSAS.Tests/IoTestFatP0RegressionTests.cs index 791f498fe..06c24ffbb 100644 --- a/tests/ARSAS.Tests/IoTestFatP0RegressionTests.cs +++ b/tests/ARSAS.Tests/IoTestFatP0RegressionTests.cs @@ -29,7 +29,7 @@ public void FatConnect_ReadinessDoesNotBlockOnLegacyEightToEighteenSecondReportS } [Fact] - public void FatEvidenceDrain_YieldsUiAndStartupJournalUsesOneDurableBatch() + public void FatEvidenceDrain_PrioritizesFirstEdgeButYieldsContinuationAndBatchesStartupJournal() { var controller = Read("Services/IoTesting/IoTestSessionController.cs"); var journal = Read("Services/IoTesting/IoTestEvidenceJournal.cs"); @@ -41,8 +41,16 @@ public void FatEvidenceDrain_YieldsUiAndStartupJournalUsesOneDurableBatch() Assert.Contains("AppendBatchRequired(startupEntries)", controller, StringComparison.Ordinal); Assert.Contains("AppendBatch(IEnumerable entries)", journal, StringComparison.Ordinal); Assert.Contains("FlushDurable();", journal, StringComparison.Ordinal); + + // Preserve #150's immediate TRUE-edge scheduling while retaining P0's bounded + // drain/yield behavior for bursts. The first dispatch is DataBind; a reschedule + // requested while that drain is active is intentionally demoted to Background. + Assert.Contains("DispatchIoFatEvidence", main, StringComparison.Ordinal); + Assert.Contains("Volatile.Read(ref _ioFatEvidenceDrainDispatchActive) == 0", main, StringComparison.Ordinal); + Assert.Contains("DispatcherPriority.DataBind", main, StringComparison.Ordinal); Assert.Contains("DispatcherPriority.Background", main, StringComparison.Ordinal); - Assert.DoesNotContain("DispatcherPriority.DataBind", main, StringComparison.Ordinal); + Assert.Contains("Interlocked.Increment(ref _ioFatEvidenceDrainDispatchActive)", main, StringComparison.Ordinal); + Assert.Contains("Interlocked.Decrement(ref _ioFatEvidenceDrainDispatchActive)", main, StringComparison.Ordinal); } [Fact]