Skip to content

perf: eliminate TimedBatch deadline-wait allocations (AsTask + WhenAny) — issue-204 - #205

Open
SunSi12138 wants to merge 3 commits into
devfrom
feature/issue-204-timedbatch-alloc
Open

perf: eliminate TimedBatch deadline-wait allocations (AsTask + WhenAny) — issue-204#205
SunSi12138 wants to merge 3 commits into
devfrom
feature/issue-204-timedbatch-alloc

Conversation

@SunSi12138

Copy link
Copy Markdown
Owner

Closes #204.

问题

SendPumpTimedBatchDeadlineCycle 在 deadline 等待周期分配 566 B/cycle(issue #204 数据 547-576 区间),远高于正常 idle→wake 周期的 496 B。开销来自 WaitForMoreUntilDeadlineAsync 里的:

  • WaitToReadAsync(_sessionCancellation):可取消 token 使 unbounded channel 无法复用池化 waiter,每次分配 WaitingReadAsyncOperation + CancellationTokenRegistration(.NET 10 UnboundedChannel.WaitToReadAsync 源码确认:CanBeCanceled == false 才走 _waiterSingleton.TryOwnAndReset() 零分配路径)
  • waitToRead.AsTask():分配 ValueTaskSourceAsTask(channel 的 AsyncOperation 不实现 ITaskCompletionActionValueTask<bool> 无公开 OnCompleted,这一步无法省)
  • Task.Delay(...)DelayPromise + 每次一个 timer
  • Task.WhenAny(...)WhenAnyPromise
  • 周期外的 CancellationTokenSource 复用/取消逻辑

方案

新增 DeadlineReadRaceIValueTaskSource<bool>,内嵌 ManualResetValueTaskSourceCore<bool>):

  • 读侧改为 WaitToReadAsync(CancellationToken.None)(池化 waiter,与 perf: non-cancellable SendPump channel waits (issue 157) #203 主等待路径一致)
  • 每周期仅剩:AsTask() 一次 + 每次等待一个 TimeProvider.CreateTimer(tick 精确,超时即 Dispose)+ 一个闭包
  • 彻底移除 Task.WhenAnyTask.Delay_delayCancellation CTS

保留的语义(#157 约束)

  • timer 赢后 pending read 不消费、由 _pendingReadWait 保留并在下一轮 WaitToReadAsync() 复用
  • read 赢 → 继续合批(batchDeadline 不重置);writer 关闭 → ReadClosed 清保留退出;超长 deadline 按 MaximumTimerDelay 分块重挂
  • 不改 batching policy、不动 force-flush 语义

并发正确性(关键点)

  • timer 先于 read continuation 注册创建(UnsafeOnCompleted 对已完成任务会内联同步调用)
  • 每次 arm 用闭包捕获自身 read + ReferenceEquals 身份校验:上一轮的迟到 continuation 不可能作用到当前轮状态
  • read/timer 互斥由 Interlocked abandon 标志裁决;RunContinuationsAsynchronously 防重入

验证(192.168.31.242,SDK 10.0.110,taskset -c 2,3)

TimedBatchDeadlineCycle baseline (dev) 本 PR Δ
Allocated 566 B 363 B -36%
Mean 5.855 μs 2.710 μs -54%
Gen0 /1000op 0.0305 0.0153 -50%
Op/s 170,788 369,025 +116%
  • deadline 周期分配现低于 idle→wake 的 496 B;idle-wake 4 场景两树均 496 B,无回退
  • 隔离 probe(泵形态周期,20 万轮):deadline-wait 机制 456 → 240 B/cycle(-47%),Gen0 6 → 3

测试

新增 4 例 bounded-completion 测试(SendPumpTests):

  • TimedBatchShouldDeliverFrameSentAfterDeadlineFlushThroughRetainedRead —— 保留语义回归(timer 赢后读复用;丢弃读会使后续帧永远无法唤醒泵)
  • TimedBatchShouldExtendBatchForFrameArrivingBeforeDeadline —— read 赢合批(两帧一次 flush,记录型 provider 同步)
  • TimedBatchShouldRearmAcrossMaximumTimerDelayChunks —— 超长 deadline 分块重挂且不提前 flush
  • TimedBatchDeadlineWaitShouldExitWhenSessionIsDisposed —— 等待期停止(ReadClosed)

远程 943 总/942 过:唯一失败为 PumpBlockedInFlushExitsWhenTransportOutputFaults——dev 既有问题(隔离复跑 5/5 确定性 5s 超时,本改动不触及 Balanced flush 故障路径;本地 macOS 全套 943/943 全绿)。CI 上 ManifestSourceIsolationTests 的 poison flake 为已知既有竞态。

sunsi added 2 commits August 15, 2026 13:25
@SunSi12138

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b1f7f582d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


private void OnTimerFired()
{
if (Interlocked.Exchange(ref _readAbandoned, 1) != 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Isolate timer callbacks by arm generation

When a read wins near its deadline, disposing its ITimer does not guarantee that an already queued callback has finished. If the pump starts another wait first, that stale callback observes the newly reset _readAbandoned, disposes the new arm's _timer, and completes the new _core as timed out, causing a premature batch flush. Capture and validate an arm generation (or use callback-specific state) before a timer callback touches the reusable race state.

Useful? React with 👍 / 👎.

Comment on lines +536 to +538
lock (_gate)
_armedDueTimes.Add(dueTime);
return inner.CreateTimer(callback, state, dueTime, period);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record the timer only after it is armed

The new tests use WasArmed as a synchronization point before advancing the manual clock, but this publishes the due time before inner.CreateTimer has actually installed the timer. If the test thread observes the list entry in that window and advances the clock, the timer is subsequently scheduled relative to the advanced timestamp, so the final expected advance does not fire it and TimedBatchShouldExtendBatchForFrameArrivingBeforeDeadline can time out nondeterministically. Publish the entry only after inner.CreateTimer returns, as the existing observing provider above does.

Useful? React with 👍 / 👎.

…st timer arms after install (issue-204 codex review)
@SunSi12138

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 443f4f731a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +99 to +100
_timer = _timeProvider.CreateTimer(
state => OnTimerFired(generation), this, timeout, Timeout.InfiniteTimeSpan);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Store the timer before allowing its callback to run

With a valid short MaxLatency (for example, one tick), TimeProvider.System can execute the callback before CreateTimer returns and this assignment completes. OnTimerFired then evaluates _timer!.Dispose() while _timer is still null on the first arm, producing an unhandled timer-callback exception that can terminate the process; on later arms it may instead dispose the previous timer and leak the new one. Create and store a disabled timer first, then arm it with Change, or otherwise give the callback an already-published per-arm timer reference.

Useful? React with 👍 / 👎.

Comment on lines +111 to +114
if (!ReferenceEquals(read, _read))
return; // Stale completion from a previous arm: never touch the current cycle's state.

if (Interlocked.Exchange(ref _readAbandoned, 1) != 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Claim the same arm validated by the read callback

When an old read continuation passes this identity check and is suspended before the exchange, its timer can win and the pump can re-arm this object, replacing _read and resetting _readAbandoned to zero. The old continuation then resumes, claims the new arm, disposes its timer, and completes its _core with the old read's result; that false read win clears the new _pendingReadWait, abandoning the actual channel waiter so a subsequently queued frame may never wake the pump. Use immutable per-arm state, or atomically validate the arm generation as part of claiming it, rather than separating validation from the shared abandon flag.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant