Skip to content

perf: replace O(n²) splice with O(n) filter in filterHeartbeatMessages#16

Open
yuanrengu wants to merge 1 commit into
Tencent:mainfrom
yuanrengu:perf/optimize-heartbeat-filter-on
Open

perf: replace O(n²) splice with O(n) filter in filterHeartbeatMessages#16
yuanrengu wants to merge 1 commit into
Tencent:mainfrom
yuanrengu:perf/optimize-heartbeat-filter-on

Conversation

@yuanrengu
Copy link
Copy Markdown

Summary

Replace two levels of splice() calls in filterHeartbeatMessages with a filter-then-rebuild approach, reducing time complexity from O(n²) to O(n).

The function filters heartbeat tool calls from the messages array before LLM input. Previously it used messages.splice(i, 1) in the outer loop and content.splice(j, 1) in the inner loop — both O(n) per call due to element shifting.

Changes

  • Add setMessageContentLocal(msg, newContent) helper for consistent content reassignment across msg.type === "message" and normal message shapes
  • Outer loop: replace reverse iteration + splice(i, 1) → forward iteration into kept[] array, skipping removed messages via continue
  • Inner loop: replace reverse iteration + splice(j, 1)content.filter(block => !isHeartbeatToolUseBlock(block))
  • Rebuild messages array in-place from kept list at the end

Behavior

No functional change — same heartbeat detection, same count semantics, same in-place mutation contract:

Aspect Before After
Heartbeat ID collection O(n) scan O(n) scan (same)
Message removal splice(i, 1), O(n) each skip via continue, O(1)
Content block removal splice(j, 1), O(n) each filter, O(n) total
Overall time O(n²) worst-case O(n)
Removal count 1 per message 1 per message (same)

Test plan

  • npx tsdown builds successfully
  • Heartbeat ID collection unchanged
  • Same messages are removed (tool/toolResult by tcId, assistant by content blocks)
  • Content block filtering preserves non-heartbeat blocks
  • In-place mutation contract preserved (callers receive mutated array)

splice(i, 1) in the outer loop and splice(j, 1) in the inner content loop
each shift all subsequent elements, making the function O(n²) in the number
of messages and content blocks. For sessions with many heartbeat tool calls
this becomes a measurable hotspot.

Replace with a filter-then-rebuild approach:
- Tool/toolResult messages: skip heartbeats by continue (omit from kept array)
- Assistant content blocks: filter out heartbeat tool_use blocks in O(n)
- Rebuild messages array in-place from kept list

Behavior is identical: same heartbeat detection logic, same removal count
semantics (one removed increment per message regardless of block count),
same in-place mutation contract.

Signed-off-by: yuanrengu <heyonggang0811@126.com>
@Maxwell-Code07
Copy link
Copy Markdown
Collaborator

Hi @yuanrengu, thanks for the PR! 👍

The splice → filter optimization in filterHeartbeatMessages is an interesting direction. We'll review the implementation details and get back to you shortly.

Thanks for the contribution! 🙏

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.

2 participants