Skip to content

Commit fff9d7d

Browse files
committed
cargo-rail: a tolerable cleanup to the readme.md; started working throught the performance fixes: batching, parallelism, efficiencies/caches.
1 parent 4b0d997 commit fff9d7d

File tree

11 files changed

+473
-230
lines changed

11 files changed

+473
-230
lines changed

TODO.md

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,64 +16,42 @@
1616

1717
All command flag support: ✅ COMPLETE (--dry-run, --json, --apply across all commands)
1818

19-
Build status: ✅ 113 tests passing, zero warnings, cargo deny/audit passing
19+
Build status: ✅ 115 tests passing, zero warnings, cargo deny/audit passing
2020

2121
---
2222

2323
## Week 1: Performance + Foundation (10 hours)
2424

2525
### Critical: Unified Domain Model (NEW - 2 hours)
2626

27-
- [ ] Create `WorkspaceContext` struct - *1 hour*
28-
- Unifies `WorkspaceMetadata`, `WorkspaceGraph`, `RailConfig`, `root`
29-
- Build once in `main.rs`, pass `&WorkspaceContext` to all commands
30-
- Eliminates redundant metadata loads (currently: split.rs:111, sync.rs:362-364)
31-
- Foundation for visibility/tier annotations
32-
33-
- [ ] Wire `WorkspaceContext` into all commands - *1 hour*
34-
- Refactor: `split`, `sync`, `graph`, `release`, `lint`, `doctor`
35-
- Remove individual metadata/config loading
36-
- Pass context by reference
27+
- [x] Create `WorkspaceContext` struct + wire into graph commands
28+
- [x] Made WorkspaceGraph thread-safe (RefCell → RwLock)
3729

3830
### Critical: Performance Fixes (6 hours)
3931

4032
#### Subprocess N+1 Patterns (CRITICAL - 5-10s each)
4133

42-
- [ ] Cache HEAD commit in sync loops (sync.rs:514, 613) - *15 min*
43-
- Currently calls `head_commit()` subprocess inside commit loop
44-
- Update cached head after each commit instead
34+
- [x] Cache HEAD commit in sync loops
4535

4636
#### Bulk Operations Not Integrated (CRITICAL - 50-250s)
4737

48-
- [ ] Use `read_files_bulk()` in apply_mono_commit_to_remote (sync.rs:462-503) - *60 min*
49-
- [ ] Use `read_files_bulk()` in apply_remote_commit_to_mono (sync.rs:550-602) - *60 min*
50-
- [ ] Use `read_files_bulk()` in resolve_conflicts_for_commit (sync.rs:715, 724) - *30 min*
51-
- Bulk API exists (system_git_ops.rs:627) but not used in hot paths
52-
- Sequential file reads = 500-50,000 subprocess calls vs. <500ms batch
38+
- [x] Use `read_files_bulk()` in apply_mono_commit_to_remote
39+
- [x] Use `read_files_bulk()` in apply_remote_commit_to_mono
40+
- [x] Use `read_files_bulk()` in resolve_conflicts_for_commit
5341

5442
#### Algorithm O(n) → O(1) (CRITICAL - 100-500ms)
5543

56-
- [ ] Build reverse mapping index in MappingStore - *30 min*
57-
- sync.rs:296, 432 scan all values with `.values().any()`
58-
- Add `reverse_mappings: HashMap<String, String>` for O(1) lookup
44+
- [x] Build reverse mapping index in MappingStore
5945

6046
#### Quadratic → Linear (HIGH - 100ms-1s)
6147

62-
- [ ] Cache conflict detection in resolve_conflicts_for_commit (sync.rs:665-781) - *60 min*
63-
- Currently: `get_commits_touching_path()` per file (20 files = 20 subprocess calls)
64-
- Solution: Get all mono changes once, build HashSet for O(1) lookup
48+
- [x] Cache conflict detection in resolve_conflicts_for_commit
6549

6650
#### Caching (HIGH - 10-50ms)
6751

68-
- [ ] Cache `get_changed_files()` results (sync.rs:446, 541, 673) - *30 min*
69-
- Same commit parsed 3x (resolve → apply → check)
70-
- Pass through call chain or cache on CommitInfo
71-
72-
- [ ] Wrap config.security in Arc<> for parallel execution (sync.rs:363-366) - *20 min*
73-
- Currently deep clones per parallel task
74-
75-
- [ ] Cache mapping loads in SyncEngine (sync.rs:136, 263, 686, 794, 818) - *20 min*
76-
- 5 subprocess calls × 10ms = 50ms per sync
52+
- [x] Cache `get_changed_files()` results
53+
- [x] Wrap config.security in Arc<> for parallel execution
54+
- [x] Cache mapping loads in SyncEngine
7755

7856
#### Minor Optimizations (MEDIUM - 20-50ms)
7957

@@ -120,7 +98,14 @@ Build status: ✅ 113 tests passing, zero warnings, cargo deny/audit passing
12098
- [ ] Address release.rs:66 skip_sync TODO - *15 min*
12199
- Implement or document deferral
122100

123-
**Week 1 total**: ~10 hours
101+
**Week 1 Performance Complete**: All critical performance optimizations done
102+
- Reverse mapping index: O(1) lookups (100-500ms saved per sync)
103+
- Conflict detection cache: Single git call vs N calls (100ms-1s saved)
104+
- get_changed_files() cache: Eliminated duplicate parsing (10-50ms saved)
105+
- Arc<SecurityConfig>: Cheap clones in parallel (eliminates N deep clones)
106+
- Mapping load cache: Eliminated redundant subprocess calls (50ms saved)
107+
- Type complexity warning: Fixed with ConflictResolutionResult type alias
108+
- Dead code warning: Fixed with proper documentation + #[allow(dead_code)]
124109

125110
---
126111

src/commands/affected.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
//! - Which crates transitively depend on those changed crates
66
//! - The minimal set of crates that need testing/building
77
8+
use crate::core::context::WorkspaceContext;
89
use crate::core::error::{RailError, RailResult};
910
use crate::core::vcs::SystemGit;
10-
use crate::graph::{AffectedAnalysis, WorkspaceGraph};
11-
use std::env;
11+
use crate::graph::AffectedAnalysis;
1212
use std::path::{Path, PathBuf};
1313

1414
/// Output format for affected command
@@ -35,20 +35,17 @@ impl OutputFormat {
3535

3636
/// Run the affected command
3737
pub fn run_affected(
38+
ctx: &WorkspaceContext,
3839
since: String,
3940
from: Option<String>,
4041
to: Option<String>,
4142
format: String,
4243
dry_run: bool,
4344
) -> RailResult<()> {
44-
let workspace_root = env::current_dir()?;
4545
let output_format = OutputFormat::from_str(&format)?;
4646

47-
// Load workspace graph
48-
let graph = WorkspaceGraph::load(&workspace_root)?;
49-
5047
// Get changed files from git
51-
let changed_files = get_changed_files(&workspace_root, &since, from.as_deref(), to.as_deref())?;
48+
let changed_files = get_changed_files(ctx.workspace_root(), &since, from.as_deref(), to.as_deref())?;
5249

5350
if dry_run {
5451
println!("DRY RUN: Would analyze {} changed files", changed_files.len());
@@ -59,7 +56,7 @@ pub fn run_affected(
5956
}
6057

6158
// Analyze affected crates
62-
let analysis = crate::graph::affected::analyze(&graph, &changed_files)?;
59+
let analysis = crate::graph::affected::analyze(&ctx.graph, &changed_files)?;
6360

6461
// Output results
6562
display_results(&analysis, output_format)?;

src/commands/check.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,29 @@
99
//! - `--workspace` to override and check all workspace crates
1010
//! - `--dry-run` to show the plan without executing
1111
12+
use crate::core::context::WorkspaceContext;
1213
use crate::core::error::{RailError, RailResult};
1314
use crate::core::vcs::SystemGit;
14-
use crate::graph::{AffectedAnalysis, WorkspaceGraph};
15-
use std::env;
15+
use crate::graph::AffectedAnalysis;
1616
use std::path::{Path, PathBuf};
1717
use std::process::Command;
1818

1919
/// Run the check command
20-
pub fn run_check(since: Option<String>, workspace: bool, dry_run: bool, cargo_args: Vec<String>) -> RailResult<()> {
21-
let workspace_root = env::current_dir()?;
22-
20+
pub fn run_check(
21+
ctx: &WorkspaceContext,
22+
since: Option<String>,
23+
workspace: bool,
24+
dry_run: bool,
25+
cargo_args: Vec<String>,
26+
) -> RailResult<()> {
2327
if workspace {
2428
// Run check for entire workspace
25-
return run_workspace_check(&workspace_root, dry_run, &cargo_args);
29+
return run_workspace_check(ctx.workspace_root(), dry_run, &cargo_args);
2630
}
2731

2832
// Default: run targeted check based on affected analysis
2933
let since_ref = since.unwrap_or_else(|| "origin/main".to_string());
30-
run_affected_check(&workspace_root, &since_ref, dry_run, &cargo_args)
34+
run_affected_check(ctx, &since_ref, dry_run, &cargo_args)
3135
}
3236

3337
/// Run check for the entire workspace
@@ -64,12 +68,9 @@ fn run_workspace_check(workspace_root: &Path, dry_run: bool, cargo_args: &[Strin
6468
}
6569

6670
/// Run check for affected crates only
67-
fn run_affected_check(workspace_root: &Path, since: &str, dry_run: bool, cargo_args: &[String]) -> RailResult<()> {
68-
// Load workspace graph
69-
let graph = WorkspaceGraph::load(workspace_root)?;
70-
71+
fn run_affected_check(ctx: &WorkspaceContext, since: &str, dry_run: bool, cargo_args: &[String]) -> RailResult<()> {
7172
// Get changed files from git
72-
let changed_files = get_changed_files(workspace_root, since)?;
73+
let changed_files = get_changed_files(ctx.workspace_root(), since)?;
7374

7475
if changed_files.is_empty() {
7576
println!("✅ No changes detected since {}", since);
@@ -78,7 +79,7 @@ fn run_affected_check(workspace_root: &Path, since: &str, dry_run: bool, cargo_a
7879
}
7980

8081
// Analyze affected crates
81-
let analysis = crate::graph::affected::analyze(&graph, &changed_files)?;
82+
let analysis = crate::graph::affected::analyze(&ctx.graph, &changed_files)?;
8283

8384
if analysis.impact.test_targets.is_empty() {
8485
println!("✅ Changes detected but no workspace crates affected");
@@ -100,7 +101,7 @@ fn run_affected_check(workspace_root: &Path, since: &str, dry_run: bool, cargo_a
100101

101102
// Execute check for each affected crate
102103
println!("\nExecuting checks...\n");
103-
execute_checks(&analysis.impact.test_targets, workspace_root, cargo_args)?;
104+
execute_checks(&analysis.impact.test_targets, ctx.workspace_root(), cargo_args)?;
104105

105106
println!("\n✅ All checks completed successfully");
106107
Ok(())

src/commands/clippy.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,29 @@
99
//! - `--workspace` to override and lint all workspace crates
1010
//! - `--dry-run` to show the plan without executing
1111
12+
use crate::core::context::WorkspaceContext;
1213
use crate::core::error::{RailError, RailResult};
1314
use crate::core::vcs::SystemGit;
14-
use crate::graph::{AffectedAnalysis, WorkspaceGraph};
15-
use std::env;
15+
use crate::graph::AffectedAnalysis;
1616
use std::path::{Path, PathBuf};
1717
use std::process::Command;
1818

1919
/// Run the clippy command
20-
pub fn run_clippy(since: Option<String>, workspace: bool, dry_run: bool, cargo_args: Vec<String>) -> RailResult<()> {
21-
let workspace_root = env::current_dir()?;
22-
20+
pub fn run_clippy(
21+
ctx: &WorkspaceContext,
22+
since: Option<String>,
23+
workspace: bool,
24+
dry_run: bool,
25+
cargo_args: Vec<String>,
26+
) -> RailResult<()> {
2327
if workspace {
2428
// Run clippy for entire workspace
25-
return run_workspace_clippy(&workspace_root, dry_run, &cargo_args);
29+
return run_workspace_clippy(ctx.workspace_root(), dry_run, &cargo_args);
2630
}
2731

2832
// Default: run targeted clippy based on affected analysis
2933
let since_ref = since.unwrap_or_else(|| "origin/main".to_string());
30-
run_affected_clippy(&workspace_root, &since_ref, dry_run, &cargo_args)
34+
run_affected_clippy(ctx, &since_ref, dry_run, &cargo_args)
3135
}
3236

3337
/// Run clippy for the entire workspace
@@ -64,12 +68,11 @@ fn run_workspace_clippy(workspace_root: &Path, dry_run: bool, cargo_args: &[Stri
6468
}
6569

6670
/// Run clippy for affected crates only
67-
fn run_affected_clippy(workspace_root: &Path, since: &str, dry_run: bool, cargo_args: &[String]) -> RailResult<()> {
71+
fn run_affected_clippy(ctx: &WorkspaceContext, since: &str, dry_run: bool, cargo_args: &[String]) -> RailResult<()> {
6872
// Load workspace graph
69-
let graph = WorkspaceGraph::load(workspace_root)?;
7073

7174
// Get changed files from git
72-
let changed_files = get_changed_files(workspace_root, since)?;
75+
let changed_files = get_changed_files(ctx.workspace_root(), since)?;
7376

7477
if changed_files.is_empty() {
7578
println!("✅ No changes detected since {}", since);
@@ -78,7 +81,7 @@ fn run_affected_clippy(workspace_root: &Path, since: &str, dry_run: bool, cargo_
7881
}
7982

8083
// Analyze affected crates
81-
let analysis = crate::graph::affected::analyze(&graph, &changed_files)?;
84+
let analysis = crate::graph::affected::analyze(&ctx.graph, &changed_files)?;
8285

8386
if analysis.impact.test_targets.is_empty() {
8487
println!("✅ Changes detected but no workspace crates affected");
@@ -100,7 +103,7 @@ fn run_affected_clippy(workspace_root: &Path, since: &str, dry_run: bool, cargo_
100103

101104
// Execute clippy for each affected crate
102105
println!("\nExecuting clippy...\n");
103-
execute_clippy(&analysis.impact.test_targets, workspace_root, cargo_args)?;
106+
execute_clippy(&analysis.impact.test_targets, ctx.workspace_root(), cargo_args)?;
104107

105108
println!("\n✅ All clippy checks completed successfully");
106109
Ok(())

src/commands/sync.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,9 @@ pub fn run_sync(
345345
.map(|(split_config, _, _, _, _, _)| multi_progress.add_bar(1, format!("Syncing {}", split_config.name)))
346346
.collect();
347347

348+
// Wrap security config in Arc for cheap sharing across threads (avoid deep clones)
349+
let security_config = std::sync::Arc::new(config.security.clone());
350+
348351
let results: Vec<RailResult<SyncResult>> = plans
349352
.into_par_iter()
350353
.enumerate()
@@ -358,11 +361,11 @@ pub fn run_sync(
358361
remote_url: split_config.remote.clone(),
359362
};
360363

361-
// Create sync engine for this thread
364+
// Create sync engine for this thread (security_config Arc clone is cheap)
362365
let mut engine = SyncEngine::new(
363366
config.workspace.root.clone(),
364367
sync_config,
365-
config.security.clone(),
368+
security_config.clone(),
366369
strategy,
367370
)?;
368371

@@ -425,7 +428,7 @@ pub fn run_sync(
425428
let mut engine = SyncEngine::new(
426429
config.workspace.root.clone(),
427430
sync_config,
428-
config.security.clone(),
431+
std::sync::Arc::new(config.security.clone()),
429432
strategy,
430433
)?;
431434

src/commands/test.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,29 @@
1010
//! - `--dry-run` to show the plan without executing
1111
//! - Parallel execution via rayon
1212
13+
use crate::core::context::WorkspaceContext;
1314
use crate::core::error::{RailError, RailResult};
1415
use crate::core::vcs::SystemGit;
15-
use crate::graph::{AffectedAnalysis, WorkspaceGraph};
16-
use std::env;
16+
use crate::graph::AffectedAnalysis;
1717
use std::path::{Path, PathBuf};
1818
use std::process::Command;
1919

2020
/// Run the test command
21-
pub fn run_test(since: Option<String>, workspace: bool, dry_run: bool, cargo_args: Vec<String>) -> RailResult<()> {
22-
let workspace_root = env::current_dir()?;
23-
21+
pub fn run_test(
22+
ctx: &WorkspaceContext,
23+
since: Option<String>,
24+
workspace: bool,
25+
dry_run: bool,
26+
cargo_args: Vec<String>,
27+
) -> RailResult<()> {
2428
if workspace {
2529
// Run tests for entire workspace
26-
return run_workspace_tests(&workspace_root, dry_run, &cargo_args);
30+
return run_workspace_tests(ctx.workspace_root(), dry_run, &cargo_args);
2731
}
2832

2933
// Default: run targeted tests based on affected analysis
3034
let since_ref = since.unwrap_or_else(|| "origin/main".to_string());
31-
run_affected_tests(&workspace_root, &since_ref, dry_run, &cargo_args)
35+
run_affected_tests(ctx, &since_ref, dry_run, &cargo_args)
3236
}
3337

3438
/// Run tests for the entire workspace
@@ -65,12 +69,9 @@ fn run_workspace_tests(workspace_root: &Path, dry_run: bool, cargo_args: &[Strin
6569
}
6670

6771
/// Run tests for affected crates only
68-
fn run_affected_tests(workspace_root: &Path, since: &str, dry_run: bool, cargo_args: &[String]) -> RailResult<()> {
69-
// Load workspace graph
70-
let graph = WorkspaceGraph::load(workspace_root)?;
71-
72+
fn run_affected_tests(ctx: &WorkspaceContext, since: &str, dry_run: bool, cargo_args: &[String]) -> RailResult<()> {
7273
// Get changed files from git
73-
let changed_files = get_changed_files(workspace_root, since)?;
74+
let changed_files = get_changed_files(ctx.workspace_root(), since)?;
7475

7576
if changed_files.is_empty() {
7677
println!("✅ No changes detected since {}", since);
@@ -79,7 +80,7 @@ fn run_affected_tests(workspace_root: &Path, since: &str, dry_run: bool, cargo_a
7980
}
8081

8182
// Analyze affected crates
82-
let analysis = crate::graph::affected::analyze(&graph, &changed_files)?;
83+
let analysis = crate::graph::affected::analyze(&ctx.graph, &changed_files)?;
8384

8485
if analysis.impact.test_targets.is_empty() {
8586
println!("✅ Changes detected but no workspace crates affected");
@@ -101,7 +102,7 @@ fn run_affected_tests(workspace_root: &Path, since: &str, dry_run: bool, cargo_a
101102

102103
// Execute tests for each affected crate
103104
println!("\nExecuting tests...\n");
104-
execute_tests(&analysis.impact.test_targets, workspace_root, cargo_args)?;
105+
execute_tests(&analysis.impact.test_targets, ctx.workspace_root(), cargo_args)?;
105106

106107
println!("\n✅ All tests completed successfully");
107108
Ok(())

0 commit comments

Comments
 (0)