Skip to content

Pool memqueue producer response channels via sync.Pool#49337

Open
Copilot wants to merge 5 commits intomainfrom
copilot/reduce-memqueue-allocations
Open

Pool memqueue producer response channels via sync.Pool#49337
Copilot wants to merge 5 commits intomainfrom
copilot/reduce-memqueue-allocations

Conversation

Copy link

Copilot AI commented Mar 7, 2026

forgetfulProducer.makePushRequest and ackProducer.makePushRequest allocate a chan queue.EntryID per publish call. On the BenchmarkProducerThroughput path this dominated the allocation profile (~96% of alloc_space).

Pool and reuse response channels with sync.Pool, matching the existing batchPool pattern in broker.go.

  • Added respChanPool (sync.Pool) producing make(chan queue.EntryID, 1)
  • getRespChan() / putRespChan() acquire and release; putRespChan drains any stale buffered value before returning to pool
  • Both makePushRequest variants now call getRespChan() instead of make
  • openState.publish and openState.tryPublish return the channel via defer putRespChan(req.resp)
BenchmarkProducerThroughput  (before)  ~10,007 allocs/op  ~1,120,530 B/op
BenchmarkProducerThroughput  (after)        ~5 allocs/op       ~230 B/op
Original prompt

This section details on the original issue you should resolve

<issue_title>[performance-profiler] Reduce memqueue producer allocations by pooling response channels</issue_title>
<issue_description>## Hot Path
libbeat/publisher/queue/memqueue/produce.go allocates a fresh chan queue.EntryID for each publish request in both producers.

  • libbeat/publisher/queue/memqueue/produce.go:82-90 (forgetfulProducer.makePushRequest)
  • libbeat/publisher/queue/memqueue/produce.go:115-127 (ackProducer.makePushRequest)

This is on the BenchmarkProducerThroughput path and dominated allocation profiles.

Profiling Data

Before:

go test ./libbeat/publisher/queue/memqueue -run '^$' -bench '^BenchmarkProducerThroughput$' -benchmem -count=3
BenchmarkProducerThroughput-4    81   14053069 ns/op  1120662 B/op  10008 allocs/op
BenchmarkProducerThroughput-4    93   13188353 ns/op  1120528 B/op  10007 allocs/op
BenchmarkProducerThroughput-4    91   13209742 ns/op  1120530 B/op  10007 allocs/op

Allocation profile evidence (from sub-agent run):

go tool pprof -top -alloc_space /tmp/mq_mem.out
(*forgetfulProducer).makePushRequest 96.14% alloc_space (182.02MB / 189.32MB)

Proposed Change

Pool and reuse response channels (chan queue.EntryID) per producer using sync.Pool instead of allocating per event.

Key touched region:

  • libbeat/publisher/queue/memqueue/produce.go:27-39, 62-79, 82-148

Results

After:

go test ./libbeat/publisher/queue/memqueue -run '^$' -bench '^BenchmarkProducerThroughput$' -benchmem -count=3
BenchmarkProducerThroughput-4   82   13464002 ns/op   501 B/op  6 allocs/op
BenchmarkProducerThroughput-4  100   12388993 ns/op   335 B/op  5 allocs/op
BenchmarkProducerThroughput-4   94   12657108 ns/op   335 B/op  5 allocs/op

Improvement (mean):

  • Time/op: 13,483,721 ns12,836,701 ns (4.80% faster)
  • Memory/op: 1,120,573 B390 B (99.97% lower)
  • Allocs/op: 10,0075.33 (99.95% lower)

Verification

  • go test ./libbeat/publisher/queue/memqueue -run '^Test'
  • go test ./libbeat/publisher/queue/memqueue -run '^$' -bench '^BenchmarkProducerThroughput$' -benchmem -count=3
  • Behavior preserved for tested memqueue package paths (no test failures observed).

Evidence

Commands run:

  • Baseline in clean worktree at HEAD:
    • git worktree add --detach /tmp/gh-aw/agent/beats-baseline HEAD
    • go test ./libbeat/publisher/queue/memqueue -run '^$' -bench '^BenchmarkProducerThroughput$' -benchmem -count=3
  • Candidate benchmark and tests in current workspace:
    • go test ./libbeat/publisher/queue/memqueue -run '^Test'
    • go test ./libbeat/publisher/queue/memqueue -run '^$' -bench '^BenchmarkProducerThroughput$' -benchmem -count=3

Duplicate check performed against /tmp/previous-findings.json; no matching open/recent finding for this memqueue allocation hot path.


What is this? | From workflow: Performance Profiler

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

  • expires on Mar 9, 2026, 2:25 PM UTC

Comments on the Issue (you are @copilot in this section)

@strawgate /ai can you see if this improves any other benchmarks?

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@botelastic botelastic bot added the needs_team Indicates that the issue/PR needs a Team:* label label Mar 7, 2026
@botelastic
Copy link

botelastic bot commented Mar 7, 2026

This pull request doesn't have a Team:<team> label.

@mergify
Copy link
Contributor

mergify bot commented Mar 7, 2026

This pull request does not have a backport label.
If this is a bug or security fix, could you label this PR @Copilot? 🙏.
For such, you'll need to label your PR with:

  • The upcoming major version of the Elastic Stack
  • The upcoming minor version of the Elastic Stack (if you're not pushing a breaking change)

To fixup this pull request, you need to add the backport labels for the needed
branches, such as:

  • backport-8./d is the label to automatically backport to the 8./d branch. /d is the digit
  • backport-active-all is the label that automatically backports to all active branches.
  • backport-active-8 is the label that automatically backports to all active minor branches for the 8 major.
  • backport-active-9 is the label that automatically backports to all active minor branches for the 9 major.

Copilot AI and others added 2 commits March 7, 2026 00:38
Use sync.Pool to reuse chan queue.EntryID response channels in both
forgetfulProducer and ackProducer instead of allocating a new channel
per publish request.

Channels are obtained from the pool in makePushRequest and returned
via defer in openState.publish and openState.tryPublish, with a
drain step to handle any stale buffered response.

Benchmark improvement (BenchmarkProducerThroughput):
- Allocs/op: ~10,007 → ~4-5 (99.95% reduction)
- Memory/op: ~1,120,530 B → ~187-322 B (99.97% reduction)

Co-authored-by: strawgate <6384545+strawgate@users.noreply.github.com>
Co-authored-by: strawgate <6384545+strawgate@users.noreply.github.com>
Copilot AI changed the title [WIP] Reduce memqueue producer allocations by pooling response channels Pool memqueue producer response channels via sync.Pool Mar 7, 2026
@strawgate strawgate marked this pull request as ready for review March 7, 2026 00:50
@strawgate strawgate requested a review from a team as a code owner March 7, 2026 00:50
@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0e1dc29-8d42-407d-8a5a-10f3a2ab5c44

📥 Commits

Reviewing files that changed from the base of the PR and between e98267e and 72877fa.

📒 Files selected for processing (1)
  • libbeat/publisher/queue/memqueue/produce.go

📝 Walkthrough

Walkthrough

Adds a global sync.Pool of chan queue.EntryID in libbeat/publisher/queue/memqueue/produce.go with helpers getRespChan() and putRespChan() that drain a single value on return. Replaces per-request channel allocations in forgetfulProducer.makePushRequest and ackProducer.makePushRequest with pooled channels. Ensures pooled channels are returned (and drained) on all publish control paths (including done, queueClosing, dropped) via deferred returns and conditional recycling. No public APIs changed.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR successfully implements all objectives from issue #49192: pools response channels via sync.Pool, adds acquire/release helpers with proper draining, integrates pooling into producer paths via defer, and achieves target performance metrics (5 allocs/op, 230 B/op vs. 10,007/1,120,530 baseline).
Out of Scope Changes check ✅ Passed All changes in produce.go directly address the hot allocation issue: sync.Pool implementation, getRespChan/putRespChan helpers, and integration into makePushRequest and publish paths. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/reduce-memqueue-allocations
  • 🛠️ Update Documentation: Commit on current branch
  • 🛠️ Update Documentation: Create PR

Comment @coderabbitai help to get the list of available commands and usage tips.

Handle sync.Pool type assertion with an ok-check and fallback channel allocation to satisfy golangci-lint errcheck without changing behavior.

Made-with: Cursor
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@libbeat/publisher/queue/memqueue/produce.go`:
- Around line 151-152: The publish function currently defers
putRespChan(req.resp) immediately, which can return the response channel to the
pool while the queue goroutine may still send to it (race between st.events <-
req and handlePendingResponse), causing a pooled channel to receive a stale
EntryID; remove the early defer in openState.publish and instead call
putRespChan(req.resp) only on the error/early-return paths where the request is
not enqueued (i.e., when you know the queue goroutine will not send), and let
the consumer of the request (the queue goroutine/handler) be responsible for
returning req.resp to the pool after it has sent or closed the response; update
paths that call handlePendingResponse and any return branches to avoid returning
the channel to the pool when st.events <- req succeeded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8403320f-eba1-45f2-99e6-674c31af592e

📥 Commits

Reviewing files that changed from the base of the PR and between 13108e8 and e98267e.

📒 Files selected for processing (1)
  • libbeat/publisher/queue/memqueue/produce.go

Only return response channels to the pool when publish ownership is unambiguous; avoid recycling on ambiguous queue-closing paths to prevent stale sends into reused channels.

Made-with: Cursor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs_team Indicates that the issue/PR needs a Team:* label skip-changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[performance-profiler] Reduce memqueue producer allocations by pooling response channels

2 participants