rtk-ai / rtk-ai/rtk

`run_streaming`'s `CaptureOnly` arm clones the entire child stdout into a field no caller reads

Open
#3,397 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area:performance bug help wanted priority:medium
Dominant language
Rust
Stars
81.1k
Forks
5.1k
Avg merge
4d 21h
Merged PRs (30d)
35

Description

Two full copies of the child's output are made on the buffered path, one of which appears to be unused entirely.

Provenance: AI-assisted source audit, human-directed, at v0.44.2 (700bdde). Reachability was traced by reading every call site; not measured with a profiler.

1. stream.rs:475 — a clone nobody reads

In the FilterMode::CaptureOnly arm:

filtered = raw_stdout.clone();

Tracing the consumers of StreamResult on that path:

  • src/core/runner.rs:107 reads .raw / .raw_stdout
  • src/cmds/python/uv_cmd.rs:69 likewise

.filtered is only read on the Streaming path (runner.rs:203, search.rs:435), which is a different arm. So for every CaptureOnly invocation the process allocates a second copy of the child's stdout — up to the 10 MiB RAW_CAP — and drops it unused. 34 modules route through runner::run.

2. stream.rs:491 — a third copy, retained alongside the first two
raw = format!("{}{}", raw_stdout, raw_stderr);

StreamResult then holds raw, raw_stdout and raw_stderr simultaneously. With the 10 MiB cap applied to each stream, that is up to ~30 MiB of duplicated capture even on the capped path, against the <5 MB figure in the module docs.

3. runner.rs:14-19 — two more to_string()
let filtered = filtered.to_string();
... never_worse(raw_for_tracking, &filtered).to_string()

Both are unconditional, and never_worse already returns a &str borrow, so the second copy exists only to satisfy the return type.

Suggested directions (untested — no programmer has reviewed these)
  • Leave .filtered empty (or make it Option<String>) in the CaptureOnly arm, since nothing consumes it there.
  • Build raw lazily — a method that concatenates on demand, or have consumers take raw_stdout/raw_stderr directly, so all three are not resident at once.
  • Return Cow<str> from emit_guarded so the common case borrows.

None of these change behaviour; they are allocation removals. Related but separate: #3392 covers rtk grep, where the same copy-everything shape produces a measured 31× memory factor.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading stream.rs around lines 475-491 and runner.rs lines 14-19, then trace the CaptureOnly consumers named in runner.rs and src/cmds/python/uv_cmd.rs. Confirm which StreamResult fields are required and preserve the existing output while removing redundant allocations. Done means behavior remains unchanged and the unnecessary copies are no longer retained.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
cli, performance
Issue type
Refactor
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.