From 4f16d1d00055534dca4257d72c536a62429a2831 Mon Sep 17 00:00:00 2001 From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:25:27 +0800 Subject: [PATCH 1/3] chore(issue-801) stage dispatcher-hooks cleanup --- .../issue-801-stage-dispatcher-hooks.yml | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/issue-801-stage-dispatcher-hooks.yml diff --git a/.github/workflows/issue-801-stage-dispatcher-hooks.yml b/.github/workflows/issue-801-stage-dispatcher-hooks.yml new file mode 100644 index 000000000..a4424ebd6 --- /dev/null +++ b/.github/workflows/issue-801-stage-dispatcher-hooks.yml @@ -0,0 +1,49 @@ +name: Issue 801 - Stage verified dispatcher-hooks +on: + push: + branches: + - cleanup/issue-801-dispatcher-test-hooks +permissions: + contents: write +jobs: + apply: + if: startsWith(github.event.head_commit.message, 'chore(issue-801)') + runs-on: ubuntu-latest + timeout-minutes: 35 + env: + DOTNET_CLI_TELEMETRY_OPTOUT: '1' + TESTINGPLATFORM_TELEMETRY_OPTOUT: '1' + TUNIT_MAX_PARALLEL_TESTS: '8' + steps: + - name: Checkout exact dev-based cleanup branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: true + - name: Configure .NET 10 + uses: ./.github/actions/setup-dotnet + - name: Apply previously validated audited source edit + run: | + git fetch --no-tags --depth=1 origin refs/heads/audit/issue-801-dev-3x-deadcode + mkdir -p eng/issue801 artifacts/issue-801-dispatcher-hooks + git show FETCH_HEAD:eng/issue801/compact-test-hooks.py > eng/issue801/compact-test-hooks.py + python3 eng/issue801/compact-test-hooks.py artifacts/issue-801-dispatcher-hooks + git diff --check + git diff --stat -- "src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs" + - name: Restore and build full Release + run: | + dotnet restore Sharplink.slnx -v minimal + dotnet build Sharplink.slnx -c Release --no-restore -v minimal + - name: Unit Tests + run: dotnet test --project test/SharpLink.UnitTests/SharpLink.UnitTests.csproj -c Release --no-build + - name: Generator Tests + run: dotnet test --project test/SharpLink.Generator.Tests/SharpLink.Generator.Tests.csproj -c Release --no-build + - name: Commit only validated cleanup and remove staging workflow + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -- "src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs" + git rm -f .github/workflows/issue-801-stage-dispatcher-hooks.yml + git diff --cached --check + git diff --cached --stat + git commit -m "refactor(runtime): lazily compact pooled stream dispatcher test callbacks (#801)" + git push origin HEAD:refs/heads/cleanup/issue-801-dispatcher-test-hooks From 536d69c871cd9b72913fb2f14bde1ea8e50215c7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Oct 2026 05:26:56 +0000 Subject: [PATCH 2/3] refactor(runtime): lazily compact pooled stream dispatcher test callbacks (#801) --- .../issue-801-stage-dispatcher-hooks.yml | 49 -------- .../PooledAsyncStreamDispatcher.cs | 112 +++++++++--------- 2 files changed, 58 insertions(+), 103 deletions(-) delete mode 100644 .github/workflows/issue-801-stage-dispatcher-hooks.yml diff --git a/.github/workflows/issue-801-stage-dispatcher-hooks.yml b/.github/workflows/issue-801-stage-dispatcher-hooks.yml deleted file mode 100644 index a4424ebd6..000000000 --- a/.github/workflows/issue-801-stage-dispatcher-hooks.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Issue 801 - Stage verified dispatcher-hooks -on: - push: - branches: - - cleanup/issue-801-dispatcher-test-hooks -permissions: - contents: write -jobs: - apply: - if: startsWith(github.event.head_commit.message, 'chore(issue-801)') - runs-on: ubuntu-latest - timeout-minutes: 35 - env: - DOTNET_CLI_TELEMETRY_OPTOUT: '1' - TESTINGPLATFORM_TELEMETRY_OPTOUT: '1' - TUNIT_MAX_PARALLEL_TESTS: '8' - steps: - - name: Checkout exact dev-based cleanup branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: true - - name: Configure .NET 10 - uses: ./.github/actions/setup-dotnet - - name: Apply previously validated audited source edit - run: | - git fetch --no-tags --depth=1 origin refs/heads/audit/issue-801-dev-3x-deadcode - mkdir -p eng/issue801 artifacts/issue-801-dispatcher-hooks - git show FETCH_HEAD:eng/issue801/compact-test-hooks.py > eng/issue801/compact-test-hooks.py - python3 eng/issue801/compact-test-hooks.py artifacts/issue-801-dispatcher-hooks - git diff --check - git diff --stat -- "src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs" - - name: Restore and build full Release - run: | - dotnet restore Sharplink.slnx -v minimal - dotnet build Sharplink.slnx -c Release --no-restore -v minimal - - name: Unit Tests - run: dotnet test --project test/SharpLink.UnitTests/SharpLink.UnitTests.csproj -c Release --no-build - - name: Generator Tests - run: dotnet test --project test/SharpLink.Generator.Tests/SharpLink.Generator.Tests.csproj -c Release --no-build - - name: Commit only validated cleanup and remove staging workflow - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -- "src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs" - git rm -f .github/workflows/issue-801-stage-dispatcher-hooks.yml - git diff --cached --check - git diff --cached --stat - git commit -m "refactor(runtime): lazily compact pooled stream dispatcher test callbacks (#801)" - git push origin HEAD:refs/heads/cleanup/issue-801-dispatcher-test-hooks diff --git a/src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs b/src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs index 887a1e97a..3946320f5 100644 --- a/src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs +++ b/src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs @@ -50,15 +50,9 @@ internal sealed class PooledAsyncStreamDispatcher : private long _disposeFinalizedLeaseState; private DisposeCompletion? _disposeCompletion; private TaskCompletionSource? _remoteTerminalPublication; - private Action? _beforeConcurrentDisposeCompletionInstallForTests; - private Action? _beforeRemoteTerminalPublicationPublishForTests; - private Action? _beforeRemoteTerminalPublicationCompletionInstallForTests; - private Action? _afterRemoteTerminalPublicationCompletionInstallForTests; - private Action? _beforeProducerOperationAcquireForTests; - private Action? _beforeConsumerWaitOwnerAcquireForTests; - private Action? _afterConsumerWaitResultForTests; - private Action? _beforeReturnTransitionForTests; - private Action? _afterReturnTransitionForTests; + // Per-instance test instrumentation is dormant by default. + // The array is allocated only when a unit test installs an active callback. + private Action?[]? _testHooks; private int _terminalDispatchStateClosed; // 0 = no remote terminal, 1 = terminal publication still owns dispatch-state close, // 2 = remote terminal publication is complete. @@ -241,15 +235,7 @@ private void Reset( Volatile.Write(ref _consumerTerminal, 0); Volatile.Write(ref _terminalDispatchStateClosed, 0); Volatile.Write(ref _remoteTerminalPublication, null); - Volatile.Write(ref _beforeConcurrentDisposeCompletionInstallForTests, null); - Volatile.Write(ref _beforeRemoteTerminalPublicationPublishForTests, null); - Volatile.Write(ref _beforeRemoteTerminalPublicationCompletionInstallForTests, null); - Volatile.Write(ref _afterRemoteTerminalPublicationCompletionInstallForTests, null); - Volatile.Write(ref _beforeProducerOperationAcquireForTests, null); - Volatile.Write(ref _beforeConsumerWaitOwnerAcquireForTests, null); - Volatile.Write(ref _afterConsumerWaitResultForTests, null); - Volatile.Write(ref _beforeReturnTransitionForTests, null); - Volatile.Write(ref _afterReturnTransitionForTests, null); + Volatile.Write(ref _testHooks, null); Volatile.Write(ref _remoteTerminalPublicationState, 0); } @@ -343,7 +329,7 @@ private bool TryAcquireDispatch(out long leaseState) if (GetLeaseStatus(leaseState) != LeaseActive) return false; - Volatile.Read(ref _beforeProducerOperationAcquireForTests)?.Invoke(); + InvokeTestHook(4); Interlocked.Increment(ref _producerOperations); var currentLeaseState = Volatile.Read(ref _leaseState); if (IsSameLeaseGeneration(currentLeaseState, leaseState) && @@ -413,33 +399,67 @@ internal long RetainForRegistration() // boundary. It gates a rare second-dispose CAS so the old-generation/re-rent handoff can be // tested deterministically without reflection; it is never read by a first disposal. internal void SetBeforeConcurrentDisposeCompletionInstallForTests(Action? callback) - => Volatile.Write(ref _beforeConcurrentDisposeCompletionInstallForTests, callback); + => SetTestHook(0, callback); // Gates the remote-terminal publication in Complete's finally so a racing DisposeAsync can // deterministically install its completion while the publication state is still 1 (issue #206). internal void SetBeforeRemoteTerminalPublicationPublishForTests(Action? callback) - => Volatile.Write(ref _beforeRemoteTerminalPublicationPublishForTests, callback); + => SetTestHook(1, callback); internal void SetBeforeRemoteTerminalPublicationCompletionInstallForTests(Action? callback) - => Volatile.Write(ref _beforeRemoteTerminalPublicationCompletionInstallForTests, callback); + => SetTestHook(2, callback); internal void SetAfterRemoteTerminalPublicationCompletionInstallForTests(Action? callback) - => Volatile.Write(ref _afterRemoteTerminalPublicationCompletionInstallForTests, callback); + => SetTestHook(3, callback); internal void SetBeforeProducerOperationAcquireForTests(Action? callback) - => Volatile.Write(ref _beforeProducerOperationAcquireForTests, callback); + => SetTestHook(4, callback); internal void SetBeforeConsumerWaitOwnerAcquireForTests(Action? callback) - => Volatile.Write(ref _beforeConsumerWaitOwnerAcquireForTests, callback); + => SetTestHook(5, callback); internal void SetAfterConsumerWaitResultForTests(Action? callback) - => Volatile.Write(ref _afterConsumerWaitResultForTests, callback); + => SetTestHook(6, callback); internal void SetBeforeReturnTransitionForTests(Action? callback) - => Volatile.Write(ref _beforeReturnTransitionForTests, callback); + => SetTestHook(7, callback); internal void SetAfterReturnTransitionForTests(Action? callback) - => Volatile.Write(ref _afterReturnTransitionForTests, callback); + => SetTestHook(8, callback); + + // A single pointer on every dispatcher instead of nine test-only pointers. + // No array is allocated for ordinary production streams. + private void SetTestHook(int index, Action? callback) + { + var hooks = Volatile.Read(ref _testHooks); + if (hooks is null) + { + if (callback is null) + return; + var candidate = new Action?[9]; + hooks = Interlocked.CompareExchange(ref _testHooks, candidate, null) ?? candidate; + } + Volatile.Write(ref hooks[index], callback); + } + + private void InvokeTestHook(int index) + { + var hooks = Volatile.Read(ref _testHooks); + if (hooks is not null) + Volatile.Read(ref hooks[index])?.Invoke(); + } + + private bool HasTestHooks() + { + var hooks = Volatile.Read(ref _testHooks); + if (hooks is null) + return false; + foreach (var callback in hooks) + if (callback is not null) + return true; + return false; + } + private static ValueTask RejectedDispatch() { @@ -723,7 +743,7 @@ public void Complete(Exception? exception) } finally { - Volatile.Read(ref _beforeRemoteTerminalPublicationPublishForTests)?.Invoke(); + InvokeTestHook(1); // Publish only after the first remote Close and Signal are complete. A racing // consumer DisposeAsync then performs its own second Close in sequence. @@ -873,7 +893,7 @@ private async ValueTask SlowMoveNextAsync(long consumerLeaseState) ThrowIfEnumerationCanceled(); if (!ownsConsumerOperation) { - Volatile.Read(ref _beforeConsumerWaitOwnerAcquireForTests)?.Invoke(); + InvokeTestHook(5); Interlocked.Increment(ref _consumerOperations); var currentLeaseState = Volatile.Read(ref _leaseState); if (!IsSameLeaseGeneration(currentLeaseState, consumerLeaseState) || @@ -889,7 +909,7 @@ private async ValueTask SlowMoveNextAsync(long consumerLeaseState) ownsConsumerOperation = true; } await WaitForDataAsync().ConfigureAwait(false); - Volatile.Read(ref _afterConsumerWaitResultForTests)?.Invoke(); + InvokeTestHook(6); } } finally @@ -991,7 +1011,7 @@ private ValueTask AwaitConcurrentDisposeAsync(long disposingLeaseState) } var created = new DisposeCompletion(disposingLeaseState); - Volatile.Read(ref _beforeConcurrentDisposeCompletionInstallForTests)?.Invoke(); + InvokeTestHook(0); if (Interlocked.CompareExchange(ref _disposeCompletion, created, null) is not null) continue; @@ -1020,7 +1040,7 @@ private ValueTask WaitForRemoteTerminalPublicationAsync() if (Volatile.Read(ref _remoteTerminalPublicationState) == 2) return ValueTask.CompletedTask; - Volatile.Read(ref _beforeRemoteTerminalPublicationCompletionInstallForTests)?.Invoke(); + InvokeTestHook(2); var completion = Volatile.Read(ref _remoteTerminalPublication); if (completion is null) @@ -1029,7 +1049,7 @@ private ValueTask WaitForRemoteTerminalPublicationAsync() completion = Interlocked.CompareExchange(ref _remoteTerminalPublication, created, null) ?? created; } - Volatile.Read(ref _afterRemoteTerminalPublicationCompletionInstallForTests)?.Invoke(); + InvokeTestHook(3); // Fenced re-read (issue #206): mirrors the publisher-side fence in Complete. The // install CAS above is already a full barrier, but a fenced read keeps the two sides @@ -1423,13 +1443,13 @@ private void TryReturnToPool(long disposingLeaseState) return; var returnedState = unchecked(disposingLeaseState + 2); - Volatile.Read(ref _beforeReturnTransitionForTests)?.Invoke(); + InvokeTestHook(7); if (Interlocked.CompareExchange( ref _leaseState, returnedState, disposingLeaseState) != disposingLeaseState) return; - Volatile.Read(ref _afterReturnTransitionForTests)?.Invoke(); + InvokeTestHook(8); // Close the acquire-vs-return race before clearing lease state. var dispatchState = Volatile.Read(ref _dispatchState); @@ -1472,15 +1492,7 @@ private void TryReturnToPool(long disposingLeaseState) Volatile.Write(ref _dispatchState, null); Volatile.Write(ref _disposeCompletion, null); Volatile.Write(ref _remoteTerminalPublication, null); - Volatile.Write(ref _beforeConcurrentDisposeCompletionInstallForTests, null); - Volatile.Write(ref _beforeRemoteTerminalPublicationPublishForTests, null); - Volatile.Write(ref _beforeRemoteTerminalPublicationCompletionInstallForTests, null); - Volatile.Write(ref _afterRemoteTerminalPublicationCompletionInstallForTests, null); - Volatile.Write(ref _beforeProducerOperationAcquireForTests, null); - Volatile.Write(ref _beforeConsumerWaitOwnerAcquireForTests, null); - Volatile.Write(ref _afterConsumerWaitResultForTests, null); - Volatile.Write(ref _beforeReturnTransitionForTests, null); - Volatile.Write(ref _afterReturnTransitionForTests, null); + Volatile.Write(ref _testHooks, null); // 复位枚举器占用标记 Volatile.Write(ref _enumeratorTaken, 0); @@ -1557,15 +1569,7 @@ _current is not null || _enumerationToken.CanBeCanceled || !_additionalEnumerationCancellationRegistration.Equals(default) || Volatile.Read(ref _disposeCompletion) is not null || Volatile.Read(ref _remoteTerminalPublication) is not null || - Volatile.Read(ref _beforeConcurrentDisposeCompletionInstallForTests) is not null || - Volatile.Read(ref _beforeRemoteTerminalPublicationPublishForTests) is not null || - Volatile.Read(ref _beforeRemoteTerminalPublicationCompletionInstallForTests) is not null || - Volatile.Read(ref _afterRemoteTerminalPublicationCompletionInstallForTests) is not null || - Volatile.Read(ref _beforeProducerOperationAcquireForTests) is not null || - Volatile.Read(ref _beforeConsumerWaitOwnerAcquireForTests) is not null || - Volatile.Read(ref _afterConsumerWaitResultForTests) is not null || - Volatile.Read(ref _beforeReturnTransitionForTests) is not null || - Volatile.Read(ref _afterReturnTransitionForTests) is not null) + HasTestHooks()) { return true; } From e5220a76ab23120e00c462113a1ff4c3d04740ce Mon Sep 17 00:00:00 2001 From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:33:07 +0800 Subject: [PATCH 3/3] style(issue-801): keep compact dispatcher hooks within reviewed LOC budget --- src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs b/src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs index 3946320f5..542d79ac8 100644 --- a/src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs +++ b/src/SharpLink.Runtime/PooledAsyncStreamDispatcher.cs @@ -50,8 +50,7 @@ internal sealed class PooledAsyncStreamDispatcher : private long _disposeFinalizedLeaseState; private DisposeCompletion? _disposeCompletion; private TaskCompletionSource? _remoteTerminalPublication; - // Per-instance test instrumentation is dormant by default. - // The array is allocated only when a unit test installs an active callback. + // Test hooks allocate storage only when a test installs a callback. private Action?[]? _testHooks; private int _terminalDispatchStateClosed; // 0 = no remote terminal, 1 = terminal publication still owns dispatch-state close, @@ -426,9 +425,6 @@ internal void SetBeforeReturnTransitionForTests(Action? callback) internal void SetAfterReturnTransitionForTests(Action? callback) => SetTestHook(8, callback); - - // A single pointer on every dispatcher instead of nine test-only pointers. - // No array is allocated for ordinary production streams. private void SetTestHook(int index, Action? callback) { var hooks = Volatile.Read(ref _testHooks); @@ -460,7 +456,6 @@ private bool HasTestHooks() return false; } - private static ValueTask RejectedDispatch() { #if DEBUG