Skip to content

Commit be5dbeb

Browse files
authored
perf(lsp): cache ValidatorRegistry across validations (#182)
* perf(lsp): cache ValidatorRegistry across validations Reuse the ValidatorRegistry instance in the LSP Backend struct instead of creating a new registry for every validation call. The registry is now wrapped in Arc and shared across all validation operations. Changes: - Add `registry: Arc<agnix_core::ValidatorRegistry>` field to Backend - Initialize registry once in Backend::new() with with_defaults() - Use validate_file_with_registry() instead of validate_file() - Update doc comments to reflect the optimization - Add test_cached_registry_used_for_multiple_validations test This eliminates redundant HashMap allocations and validator factory lookups on each file validation, improving LSP server performance for workspaces with many files. Closes #171 * fix: clean up AI slop in test comments * docs: clarify registry thread-safety and test purpose - Add comment explaining Arc is for spawn_blocking task sharing - Update test docstring to clarify it's a regression test for thread-safety * docs: add CHANGELOG entry for ValidatorRegistry caching
1 parent 922073d commit be5dbeb

2 files changed

Lines changed: 73 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8888
- Comprehensive test coverage with 8 fixtures
8989

9090
### Performance
91+
- LSP server now caches ValidatorRegistry in Backend struct (#171)
92+
- Registry wrapped in Arc and shared across spawn_blocking validation tasks
93+
- Eliminates redundant HashMap allocations and validator factory lookups per validation
9194
- AS-015 directory size validation now short-circuits when limit exceeded, improving performance on large skill directories (#84)
9295
- Stream file walk to reduce memory usage on large repositories (#172)
9396
- Replaced collect-then-validate pattern with streaming par_bridge()

crates/agnix-lsp/src/backend.rs

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,8 @@ fn create_error_diagnostic(code: &str, message: String) -> Diagnostic {
4747
///
4848
/// # Performance Notes
4949
///
50-
/// The `LintConfig` is cached and reused across validations to avoid
51-
/// repeated allocations. The `ValidatorRegistry` is currently created
52-
/// per-validation inside `agnix_core::validate_file` - this is a known
53-
/// limitation of the agnix-core API that could be optimized in the future
54-
/// by using `validate_file_with_registry` with a shared registry.
50+
/// Both `LintConfig` and `ValidatorRegistry` are cached and reused across
51+
/// validations to avoid repeated allocations.
5552
pub struct Backend {
5653
client: Client,
5754
/// Cached lint configuration reused across validations.
@@ -61,6 +58,9 @@ pub struct Backend {
6158
/// Set during initialize() from the client's root_uri.
6259
workspace_root: RwLock<Option<PathBuf>>,
6360
documents: RwLock<HashMap<Url, String>>,
61+
/// Cached validator registry reused across validations.
62+
/// Immutable after construction; Arc enables sharing across spawn_blocking tasks.
63+
registry: Arc<agnix_core::ValidatorRegistry>,
6464
}
6565

6666
impl Backend {
@@ -71,6 +71,7 @@ impl Backend {
7171
config: RwLock::new(Arc::new(agnix_core::LintConfig::default())),
7272
workspace_root: RwLock::new(None),
7373
documents: RwLock::new(HashMap::new()),
74+
registry: Arc::new(agnix_core::ValidatorRegistry::with_defaults()),
7475
}
7576
}
7677

@@ -79,12 +80,15 @@ impl Backend {
7980
/// agnix-core validation is CPU-bound and synchronous, so we run it
8081
/// in a blocking task to avoid blocking the async runtime.
8182
///
82-
/// The `LintConfig` is cloned from the cached instance to avoid
83-
/// repeated allocations on each validation.
83+
/// Both `LintConfig` and `ValidatorRegistry` are cloned from cached
84+
/// instances to avoid repeated allocations on each validation.
8485
async fn validate_file(&self, path: PathBuf) -> Vec<Diagnostic> {
8586
let config = Arc::clone(&*self.config.read().await);
86-
let result =
87-
tokio::task::spawn_blocking(move || agnix_core::validate_file(&path, &config)).await;
87+
let registry = Arc::clone(&self.registry);
88+
let result = tokio::task::spawn_blocking(move || {
89+
agnix_core::validate_file_with_registry(&path, &config, &registry)
90+
})
91+
.await;
8892

8993
match result {
9094
Ok(Ok(diagnostics)) => to_lsp_diagnostics(diagnostics),
@@ -981,4 +985,61 @@ model: sonnet
981985

982986
// All validations should complete (config is reused internally)
983987
}
988+
989+
/// Regression test: validates multiple files using the cached registry.
990+
/// Verifies the Arc<ValidatorRegistry> is thread-safe across spawn_blocking tasks.
991+
#[tokio::test]
992+
async fn test_cached_registry_used_for_multiple_validations() {
993+
let (service, _socket) = LspService::new(Backend::new);
994+
995+
// Initialize
996+
service
997+
.inner()
998+
.initialize(InitializeParams::default())
999+
.await
1000+
.unwrap();
1001+
1002+
let temp_dir = tempfile::tempdir().unwrap();
1003+
1004+
// Skill file
1005+
let skill_path = temp_dir.path().join("SKILL.md");
1006+
std::fs::write(
1007+
&skill_path,
1008+
r#"---
1009+
name: test-skill
1010+
version: 1.0.0
1011+
model: sonnet
1012+
---
1013+
1014+
# Test Skill
1015+
"#,
1016+
)
1017+
.unwrap();
1018+
1019+
// CLAUDE.md file
1020+
let claude_path = temp_dir.path().join("CLAUDE.md");
1021+
std::fs::write(
1022+
&claude_path,
1023+
r#"# Project Memory
1024+
1025+
This is a test project.
1026+
"#,
1027+
)
1028+
.unwrap();
1029+
1030+
for path in [&skill_path, &claude_path] {
1031+
let uri = Url::from_file_path(path).unwrap();
1032+
service
1033+
.inner()
1034+
.did_open(DidOpenTextDocumentParams {
1035+
text_document: TextDocumentItem {
1036+
uri,
1037+
language_id: "markdown".to_string(),
1038+
version: 1,
1039+
text: String::new(),
1040+
},
1041+
})
1042+
.await;
1043+
}
1044+
}
9841045
}

0 commit comments

Comments
 (0)