Skip to content

Release query-engine memory back to the OS#1171

Merged
ccgsnet merged 10 commits into
masterfrom
malloc_trim
Jul 7, 2026
Merged

Release query-engine memory back to the OS#1171
ccgsnet merged 10 commits into
masterfrom
malloc_trim

Conversation

@ccgsnet

@ccgsnet ccgsnet commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

The query-engine bus node kept growing in memory during run_toy.sh and stayed
around ~15GB even after the workload finished. The node is a long-lived service, so
whatever it accumulated was never given back.

Root causes:

  1. glibc never returned freed heap to the OS. Each query makes large transient
    allocations (HandleSets of tens of thousands of atoms + QueryAnswers) and frees
    them, but with up to 8 * nproc malloc arenas and no malloc_trim, glibc parked
    those pages in per-arena free lists. RSS stayed pinned at the peak.
  2. Finished query threads were never reaped. PatternMatchingQueryProcessor and
    QueryEvolutionProcessor both left a // TODO add a call to remove_query_thread,
    so every completed query thread stayed joinable and held its stack/arena.

Changes

  • bus_node.cc: cap glibc arenas (M_ARENA_MAX=4) and lower the trim threshold
    (M_TRIM_THRESHOLD) so freed pages are returned to the OS. Both are only applied
    when the operator has not set the corresponding MALLOC_ARENA_MAX /
    MALLOC_TRIM_THRESHOLD_ env var, so explicit allocator tuning is respected.
  • PatternMatchingQueryProcessor / QueryEvolutionProcessor: call malloc_trim(0)
    after a query finishes (once its tree and answers are freed), and have each worker
    self-reap: a thread cannot join itself, so when it finishes it detaches and
    removes its own entry from query_threads. This returns to baseline immediately,
    including the burst-then-idle case (no dependency on a later command).
  • StoppableThread: add detach() and separate the stop state from thread
    cleanup. stop_flag now only means "stop requested" (what stopped() reports);
    reaping claims thread_object exactly once (claim-and-null under the lock, join/detach
    outside it). So stop()/detach() are idempotent and null-safe, stop(false) can
    request a stop and still have the thread reaped by a later stop()/detach()/destructor,
    and a thread can release itself without a later join.

All allocator calls are guarded with #if defined(__GLIBC__).

Tests

  • stoppable_thread_test.cc: covers the self-reap mechanism — a burst of workers
    that each detach + erase themselves drains to empty without any external reaping
    trigger, stop() after detach() is a safe no-op, and stop(false) (request-only)
    followed by a later stop() still reaps the thread.

Result (query-engine container during run_toy.sh)

Before After
Idle RSS after run ~15GB, never released ~90MB
Peak RSS during run climbing 8.5GB → 15GB ~4-6GB sawtooth
Thread count accumulating returns to baseline

Query behavior is unchanged; only memory lifecycle is affected.

@ccgsnet ccgsnet self-assigned this Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds self-reaping for finished query threads, revises StoppableThread stop/detach handling, applies glibc heap trimming/tuning, and locks ThreadSafeHeap::snapshot copying.

Changes

Thread lifecycle and memory tuning

Layer / File(s) Summary
StoppableThread detach and stop behavior
src/commons/StoppableThread.h, src/commons/StoppableThread.cc, src/tests/cpp/BUILD, src/tests/cpp/stoppable_thread_test.cc
Declares detach(), rewrites stop() and detach() to claim thread handles under the mutex, and adds tests for self-reap, detach, and stop(false) behavior.
Query processors self-reap
src/agents/evolution/QueryEvolutionProcessor.h, src/agents/evolution/QueryEvolutionProcessor.cc, src/agents/query_engine/PatternMatchingQueryProcessor.h, src/agents/query_engine/PatternMatchingQueryProcessor.cc
Adds glibc malloc support, trims heap on worker exit, and detaches and erases finished workers under query_threads_mutex in both query processors.
bus_node glibc malloc tuning
src/main/bus_node.cc
Conditionally includes <malloc.h> and applies mallopt arena and trim-threshold settings at startup when the corresponding environment variables are unset.
ThreadSafeHeap snapshot locking
src/commons/ThreadSafeHeap.h
Copies the backing priority_queue under api_mutex before draining the local snapshot output.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: returning query-engine memory to the OS.
Description check ✅ Passed The description is directly related to the changeset and matches the memory-reclamation work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Tests For Behavior Changes ✅ Passed PASS: The PR adds src/tests/cpp/stoppable_thread_test.cc and a BUILD target covering self-reap, detach(), and stop(false)/stop() semantics for the new behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch malloc_trim

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/agents/evolution/QueryEvolutionProcessor.cc`:
- Line 41: The worker cleanup in QueryEvolutionProcessor only happens during
run_command() and after workers enqueue their finished ids, so finished
StoppableThread entries can stay joinable in query_threads while idle. Update
QueryEvolutionProcessor to trigger reap_finished_threads() from a non-worker
path as well, and make sure the finished-id handling around the queue in the
worker-completion flow still feeds that cleanup. Add a matching *_test.cc case
under src/tests/cpp that covers the burst-then-idle scenario and verifies the
threads are reaped without waiting for another command.

In `@src/agents/query_engine/PatternMatchingQueryProcessor.cc`:
- Line 64: Finished workers are only reaped before spawning in run_command(),
while the completion path around the finished-id queue just enqueues and leaves
joinable StoppableThread entries in query_threads until another command arrives.
Add a non-worker reaping trigger so completed threads are joined as soon as they
finish, and update the relevant cleanup path in PatternMatchingQueryProcessor to
remove them without waiting for the next command. Also add a matching *_test.cc
case that exercises the burst-then-idle scenario to verify query_threads returns
to baseline after workers finish.

In `@src/main/bus_node.cc`:
- Around line 32-45: The glibc memory-tuning block in bus_node.cc overrides the
operator’s trim setting by always calling mallopt for the trim threshold. Update
the startup logic around the existing getenv("MALLOC_ARENA_MAX") /
mallopt(M_ARENA_MAX, 4) handling so the M_TRIM_THRESHOLD setting is also skipped
when an explicit allocator trim tunable is present, or otherwise clearly
document that BusNode initialization intentionally overrides allocator
configuration. Use the existing bus-node initialization section and the
mallopt(M_TRIM_THRESHOLD, ...) call as the main place to apply the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 831c284b-ad75-494b-a7bb-66f8e7a157fc

📥 Commits

Reviewing files that changed from the base of the PR and between e5458ec and 20b48b7.

📒 Files selected for processing (5)
  • src/agents/evolution/QueryEvolutionProcessor.cc
  • src/agents/evolution/QueryEvolutionProcessor.h
  • src/agents/query_engine/PatternMatchingQueryProcessor.cc
  • src/agents/query_engine/PatternMatchingQueryProcessor.h
  • src/main/bus_node.cc

Comment thread src/agents/evolution/QueryEvolutionProcessor.cc Outdated
Comment thread src/agents/query_engine/PatternMatchingQueryProcessor.cc Outdated
Comment thread src/main/bus_node.cc

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/tests/cpp/stoppable_thread_test.cc (1)

14-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add one processor-level regression for the actual cleanup path.

This mirrors the map/self-detach pattern, but it won’t catch broken PatternMatchingQueryProcessor or QueryEvolutionProcessor wiring. Add a focused burst-then-idle test around at least one real processor path so query_threads drains without a follow-up command.

As per coding guidelines, production behavior changes require matching tests. As per path instructions, tests should cover real behavior, not trivial coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tests/cpp/stoppable_thread_test.cc` around lines 14 - 19, The current
StoppableThread test only covers the generic self-reap pattern and does not
exercise the real cleanup path in PatternMatchingQueryProcessor or
QueryEvolutionProcessor. Add a burst-then-idle regression test against at least
one of those processor classes so the actual query_threads lifecycle is covered
end-to-end, verifying that the worker map drains to empty after a burst without
requiring any extra command or external reap trigger. Use the existing processor
setup/teardown and the relevant query execution entry point to drive real work
and assert the cleanup behavior in the processor-specific test.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commons/StoppableThread.cc`:
- Around line 34-35: The early return in StoppableThread::stop should not reuse
stop_flag as a “already cleaned up” signal, because that can skip the
join/detach/delete path for the thread_object. Update the stop() and destructor
flow to separate “stop requested” from thread cleanup state, and make sure
joinable threads are always reaped exactly once even after stop(false), later
stop() calls, or self-reap detach() paths.

---

Nitpick comments:
In `@src/tests/cpp/stoppable_thread_test.cc`:
- Around line 14-19: The current StoppableThread test only covers the generic
self-reap pattern and does not exercise the real cleanup path in
PatternMatchingQueryProcessor or QueryEvolutionProcessor. Add a burst-then-idle
regression test against at least one of those processor classes so the actual
query_threads lifecycle is covered end-to-end, verifying that the worker map
drains to empty after a burst without requiring any extra command or external
reap trigger. Use the existing processor setup/teardown and the relevant query
execution entry point to drive real work and assert the cleanup behavior in the
processor-specific test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 60c544c8-f921-4594-87bd-ca2b47db013c

📥 Commits

Reviewing files that changed from the base of the PR and between 20b48b7 and 57b1b4c.

📒 Files selected for processing (9)
  • src/agents/evolution/QueryEvolutionProcessor.cc
  • src/agents/evolution/QueryEvolutionProcessor.h
  • src/agents/query_engine/PatternMatchingQueryProcessor.cc
  • src/agents/query_engine/PatternMatchingQueryProcessor.h
  • src/commons/StoppableThread.cc
  • src/commons/StoppableThread.h
  • src/main/bus_node.cc
  • src/tests/cpp/BUILD
  • src/tests/cpp/stoppable_thread_test.cc
💤 Files with no reviewable changes (2)
  • src/agents/evolution/QueryEvolutionProcessor.h
  • src/agents/query_engine/PatternMatchingQueryProcessor.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/bus_node.cc

Comment thread src/commons/StoppableThread.cc Outdated
@ccgsnet ccgsnet requested a review from andre-senna July 1, 2026 12:23

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/commons/ThreadSafeHeap.h (1)

86-94: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reserve output capacity before draining the snapshot.

copy.size() is known before the loop, so reserve the destination vector to avoid repeated reallocations when snapshotting large candidate heaps.

Proposed change
         {
             lock_guard<mutex> semaphore(this->api_mutex);
             copy = this->queue;
         }
+        output.reserve(output.size() + copy.size());
         while (!copy.empty()) {
             output.push_back(copy.top().element);
             copy.pop();
         }

As per path instructions, “MEMORY & PERFORMANCE (high priority): Flag unnecessary copies of large objects ... missing reserve() before repeated push_back/emplace_back.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commons/ThreadSafeHeap.h` around lines 86 - 94, The snapshot-draining
logic in ThreadSafeHeap::snapshot (the block that copies this->queue into the
local priority_queue copy and then appends to output) should reserve the
destination vector upfront. Use the known size of copy before the while loop to
call reserve on output, then keep the push_back/pop draining as-is to avoid
repeated reallocations for large heaps.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/commons/ThreadSafeHeap.h`:
- Around line 86-94: The snapshot-draining logic in ThreadSafeHeap::snapshot
(the block that copies this->queue into the local priority_queue copy and then
appends to output) should reserve the destination vector upfront. Use the known
size of copy before the while loop to call reserve on output, then keep the
push_back/pop draining as-is to avoid repeated reallocations for large heaps.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 57e4dc8d-dc8c-467a-8bbb-f2bd7034580d

📥 Commits

Reviewing files that changed from the base of the PR and between 667c39d and c9b8ac4.

📒 Files selected for processing (1)
  • src/commons/ThreadSafeHeap.h

@ccgsnet ccgsnet merged commit d7e60f8 into master Jul 7, 2026
3 checks passed
@ccgsnet ccgsnet deleted the malloc_trim branch July 7, 2026 20:51
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