Skip to content

nix search: Reduce evaluator worker starvation - #564

Merged
edolstra merged 1 commit into
mainfrom
work-starvation
Jul 15, 2026
Merged

nix search: Reduce evaluator worker starvation#564
edolstra merged 1 commit into
mainfrom
work-starvation

Conversation

@edolstra

@edolstra edolstra commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Small improvements from #534: spawn work items faster so that idle workers can start earlier, and use a faster PRNG.

Context

Summary by CodeRabbit

  • Performance Improvements
    • Improved parallel processing reliability and efficiency during search operations.
    • Search evaluations now begin while results are still being enumerated, helping maintain better worker activity and responsiveness.

Off-CPU profiling showed that ~2 of 12 worker threads were idle on
average during the parallel phase of 'nix search', because work items
were only spawned after fully enumerating each attrset: while one
thread enumerated legacyPackages.x86_64-linux (~120k attributes), and
later each large package subset (pythonPackages, perlPackages, ...),
the other workers had nothing new to pick up.

Two changes:

* Spawn work items incrementally (every 256 attributes) during
  enumeration instead of in one batch at the end, so idle workers can
  start on the first attributes while enumeration continues.

* Executor::spawn(): Generate queue keys with a thread-local
  mt19937_64 instead of calling std::random_device per work item,
  which costs hundreds of cycles per call (RDRAND / /dev/urandom).
  The key only needs to spread same-priority items around the queue,
  not be cryptographically random.

At 12 eval cores (with GC disabled via GC_INITIAL_HEAP_SIZE=40G),
this reduces elapsed time from ~4.6s to ~4.3s, user CPU from ~28s to
~26s, and kernel time from ~5s to ~3.9s for 'nix search nixpkgs
--no-eval-cache fizzbuzz'.

Assisted-by: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes update executor work-item key generation to use a thread-local PRNG and make search evaluation submit work incrementally when batches reach 256 items.

Changes

Parallel evaluation

Layer / File(s) Summary
Thread-local work-item key generation
src/libexpr/parallel-eval.cc
Executor::spawn uses a thread-local std::mt19937_64 and uniform distribution for per-work-item key sampling.
Incremental search work scheduling
src/nix/search.cc
Search traversal submits accumulated work at 256 items and keeps the final post-enumeration submission.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: cole-h

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: reducing evaluator worker starvation in nix search.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch work-starvation

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 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/libexpr/parallel-eval.cc`:
- Around line 137-145: Update the thread-local
std::uniform_int_distribution<uint64_t> dist in the key-generation code to use
(1ULL << 48) - 1 as its inclusive upper bound, keeping the random suffix
confined to bits 0–47 and preventing overlap with the priority field shifted by
48.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bad6783-defa-42f0-b5f1-bcaee8c0b88b

📥 Commits

Reviewing files that changed from the base of the PR and between 045d1ab and f2d45bb.

📒 Files selected for processing (2)
  • src/libexpr/parallel-eval.cc
  • src/nix/search.cc

Comment on lines +137 to +145
/* Note: this uses a cheap PRNG rather than std::random_device,
since the latter costs hundreds of cycles per call (RDRAND or
/dev/urandom), which adds up when spawning many work items. The
key only needs to spread items of the same priority around the
queue, not be cryptographically random. */
[[gnu::tls_model("initial-exec")]] static thread_local std::mt19937_64 rng{std::random_device{}()};
[[gnu::tls_model("initial-exec")]] static thread_local std::uniform_int_distribution<uint64_t> dist(
0, 1ULL << 48);
auto key = (uint64_t(item.second) << 48) | dist(rd);
auto key = (uint64_t(item.second) << 48) | dist(rng);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

! rg -n '0,\s*1ULL << 48\)' src/libexpr/parallel-eval.cc

Repository: DeterminateSystems/nix-src

Length of output: 200


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Show the relevant section with line numbers.
sed -n '120,160p' src/libexpr/parallel-eval.cc

printf '\n---\n'

# Find the type/usage of the queued item and key comparisons.
rg -n 'item\.second|key =|priority|uniform_int_distribution|mt19937_64|queue' src/libexpr/parallel-eval.cc

Repository: DeterminateSystems/nix-src

Length of output: 2531


🌐 Web query:

std::uniform_int_distribution inclusive upper bound cppreference

💡 Result:

In C++, the std::uniform_int_distribution template produces random integers that are uniformly distributed on the closed interval [a, b] [1][2]. This means that both the minimum value (a) and the maximum value (b) are inclusive [1][3]. When constructing the distribution, the second parameter b defaults to the maximum representable value of the integer type (std::numeric_limits::max), which is also included in the potential output range [4][2].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Locate the relevant type definitions and queue ordering.
rg -n 'WorkItems|Priority|queue|std::priority_queue|std::map|emplace\(key|item\.second' src/libexpr/parallel-eval.cc

printf '\n---\n'

# Show the top of the file where types and state are declared.
sed -n '1,120p' src/libexpr/parallel-eval.cc

Repository: DeterminateSystems/nix-src

Length of output: 4664


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Find the declarations used by Executor::spawn and the queue type.
rg -n 'using WorkItems|struct Item|queue|priority|enum class|enum ' src/libexpr/parallel-eval.hh src/libexpr/parallel-eval.cc

printf '\n--- parallel-eval.hh ---\n'
sed -n '1,220p' src/libexpr/parallel-eval.hh

Repository: DeterminateSystems/nix-src

Length of output: 1103


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Locate the parallel-eval headers and the WorkItems / queue declarations.
fd -a 'parallel-eval.hh' src
printf '\n---\n'
fd -a 'parallel-eval.cc' src
printf '\n---\n'
rg -n 'using WorkItems|struct Item|priority|queue|std::map|std::priority_queue|enum class' src/libexpr src -g '!**/build/**' || true

Repository: DeterminateSystems/nix-src

Length of output: 50382


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Find the actual location of the parallel-eval header and inspect declarations.
fd -a 'parallel-eval.hh' .
printf '\n---\n'
fd -a 'parallel-eval.cc' .
printf '\n---\n'
rg -n 'using WorkItems|struct Item|priority|queue|std::map|std::priority_queue|enum class' src lib -g '!**/build/**' || true

Repository: DeterminateSystems/nix-src

Length of output: 50382


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Locate the Executor/State declaration and the queue container type.
rg -n 'struct State|class Executor|queue' src -g '*parallel*' -g '*eval*' -g '!**/build/**'

printf '\n---\n'
sed -n '1,220p' $(rg -l 'struct State|class Executor' src -g '*parallel*' -g '*eval*' | head -n 1)

Repository: DeterminateSystems/nix-src

Length of output: 7519


Keep the random suffix within bits 0–47. std::uniform_int_distribution is inclusive, so 1ULL << 48 can be returned and set bit 48, overlapping the priority field. Use (1ULL << 48) - 1 instead.

🤖 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/libexpr/parallel-eval.cc` around lines 137 - 145, Update the thread-local
std::uniform_int_distribution<uint64_t> dist in the key-generation code to use
(1ULL << 48) - 1 as its inclusive upper bound, keeping the random suffix
confined to bits 0–47 and preventing overlap with the priority field shifted by
48.

@github-actions

Copy link
Copy Markdown

@github-actions
github-actions Bot temporarily deployed to pull request July 14, 2026 19:37 Inactive
@edolstra
edolstra added this pull request to the merge queue Jul 15, 2026
Merged via the queue into main with commit ac45634 Jul 15, 2026
33 checks passed
@edolstra
edolstra deleted the work-starvation branch July 15, 2026 10:37
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