Skip to content

Rust performance and maintainability wins#234

Merged
timopruesse merged 15 commits intomainfrom
worktree-glimmering-kindling-snowglobe
Apr 24, 2026
Merged

Rust performance and maintainability wins#234
timopruesse merged 15 commits intomainfrom
worktree-glimmering-kindling-snowglobe

Conversation

@timopruesse
Copy link
Copy Markdown
Owner

Summary

14 commits — one per item — applying the Apollo Rust best practices to tighten performance hot paths and reduce duplicated code. Each commit is self-contained and passes cargo test + cargo clippy -- -D warnings.

Performance (P1–P8)

  • P1 — stop cloning the full AppConfig when constructing TaskRunner; move it in after the requires_sudo check
  • P2std::mem::take instead of cloning selected task names in select_tasks
  • P3 — iterate filtered_indices directly in the TUI task-list render (removes one HashSet<usize> alloc per frame at ~20 Hz)
  • P4 — for bash/zsh, pipe the in-memory script straight to stdin instead of writing-then-reading a temp file. PowerShell (which needs -File) is the only remaining temp-file path
  • P5RunArgs::all_command_strings now returns impl Iterator<Item = &str>; no Vec<&str> alloc on every description render or requires_sudo scan
  • P6HashSet lookup for selected task names in requires_sudo
  • P7topological_sort returns Cow<'a, [String]>; the no-deps fast path borrows instead of cloning every name
  • P8 — tighter to_string_lossy usage: into_owned instead of .to_string(), and reuse the resolved PathBuf in setup.rs so canonicalize doesn't re-parse a string

Maintainability (M1–M6)

  • M1config::resolve_config_dir helper replaces three copies of the canonicalize-parent-or-fallback idiom
  • M2utils::process::stream_and_wait replaces the duplicated child-stdout/stderr plumbing in run.rs and clone.rs; parameterized on whether stderr should carry the [stderr] tag (shell) or pass through (git progress)
  • M3utils::path::walk_relative replaces the WalkDir + strip_prefix + ignore loop duplicated across copy.rs and symlink.rs (install + uninstall)
  • M4Display impls on each args struct as single source of truth; CommandEntry::Display and each executor's description() now delegate. Side effect: list command output now shows the (sudo) marker, which is more informative
  • M5 — rewrote validate_env_key with let-else so the empty-string check and .next().unwrap() collapse into one branch
  • M6create_executor takes CommandEntry by value; ownership transfer is explicit at the single caller (iter().cloned()) and the match arms move args into the executor without an internal clone

One trailing style: apply rustfmt commit covers line-length reflows from the new helpers.

Test plan

  • cargo test — all 31 tests pass
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo fmt --check — clean
  • Manual smoke test of install / update / uninstall on a real config
  • Verify TUI still renders correctly and is responsive during heavy output
  • Verify git clone output is readable without [stderr] noise

🤖 Generated with Claude Code

timopruesse and others added 15 commits April 24, 2026 19:37
Moved the requires_sudo check above runner construction so we can move
app_config into TaskRunner::new instead of cloning the whole task map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use std::mem::take to move each name out of the underlying vec instead
of cloning; selections is sorted and unique so no slot is taken twice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TUI redraws at ~20Hz. Previously each frame allocated a HashSet<usize>
from filtered_indices just to answer contains() for every task. Since
filtered_indices is already sorted and unique, iterate it directly and
look up each task by index.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
For bash/zsh, build_shell_command produces a script string that we already
hold in memory; previously we wrote it to a temp file only to read it back
and pipe to stdin. Now pipe the string directly.

PowerShell still needs a temp file because we invoke it with -File, but
that path is isolated. Split execute_script into stdin/file variants
sharing a common wait_with_output helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every caller was asking len/is_empty/first — none actually needed the
intermediate Vec<&str>. Returning impl Iterator<Item = &str> lets
callers drive what they need and removes the allocation on every
description render and every requires_sudo scan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous O(tasks * selected) scan becomes O(tasks + selected) by
building a HashSet once up front. Not meaningful for small configs but
free to fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When no task has a depends_on entry, the sort is a no-op but the old
return type forced a Vec::to_vec() clone. Switch to Cow<'a, [String]>
so the common no-deps path hands back a borrow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small wins:
- setup.rs: keep the resolved PathBuf around so canonicalize can use it
  directly instead of re-parsing a stringified form; URLs skip the
  allocation entirely via Cow::Borrowed.
- shell.rs: use Cow::into_owned instead of to_string_lossy().to_string();
  same cost, clearer intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The canonicalize-parent-or-cwd idiom was duplicated in main.rs (twice)
and setup.rs. Centralize it in config::resolve_config_dir, parameterized
on a fallback path so setup.rs can pass the parent config_dir while
main.rs passes cwd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
run.rs and clone.rs both spawned two tokio tasks to read child stdout
and stderr line-by-line. Extract the shared machinery into a utility,
parameterized on whether stderr should be labelled (shell commands) or
passed through (git progress).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
copy.rs and symlink.rs both duplicated a WalkDir + strip_prefix +
should_ignore + target.join loop across their install and uninstall
paths. Centralize that pattern in utils::path::walk_relative and let
callers focus on the per-entry action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Display impl on CommandEntry and each executor's description()
expressed the same idea twice — and in subtly different ways (sudo
prefix shown by executors but not Display). Impl Display on each args
struct as the single source of truth, have CommandEntry::Display
delegate, and have each executor's description() return args.to_string().

The `list` command output now shows the (sudo) marker on copy/symlink
entries, which is incidental but actually more informative.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use let-else to handle the empty-string case, eliminating both the
explicit is_empty check and the chars.next().unwrap() that followed it.
Same behavior, fewer footguns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously create_executor took &CommandEntry and each arm did
args.clone(). Move the clone up to the single caller (iter().cloned())
so ownership transfer is explicit and the match arms can move args
directly into the executor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@timopruesse timopruesse merged commit 4afe255 into main Apr 24, 2026
3 checks passed
@timopruesse timopruesse deleted the worktree-glimmering-kindling-snowglobe branch April 24, 2026 18:23
@timopruesse timopruesse mentioned this pull request Apr 24, 2026
2 tasks
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.

1 participant