Skip to content

Commit 4a2140f

Browse files
authored
feat(lsp): cache definitions for goto requests (#3930)
# Description ## Problem\* Goto Definitions still take take too much time to execute leaving user with undesirable experience. On a bigger workspace (beyond simple test cases, eg. [noir-protocol-circuits](https://github.com/AztecProtocol/aztec-packages/tree/master/yarn-project/noir-protocol-circuits)) it can take as much as a second or more to perform goto request. Resolves chore(lsp): improve goto performance by caching ast #3942 ## Summary\* Improvement from `~1s` down to `~11ms` on example project [noir-protocol-circuits](https://github.com/AztecProtocol/aztec-packages/tree/master/yarn-project/noir-protocol-circuits) is achieved by caching definitions resolved during file save or file open. The cost for this optimisation is when user opens noir source file, at which point parsing and resolving is done. This however is not noticeable for user as source code can be seen and operated on. ## Additional Context ## Documentation\* Check one: - [x] No documentation needed. - [ ] Documentation included in this PR. - [ ] **[Exceptional Case]** Documentation to be submitted in a separate PR. # PR Checklist\* - [x] I have tested the changes locally. - [x] I have formatted the changes with [Prettier](https://prettier.io/) and/or `cargo fmt` on default settings.
1 parent afe9c7a commit 4a2140f

File tree

5 files changed

+65
-38
lines changed

5 files changed

+65
-38
lines changed

compiler/noirc_frontend/src/hir/mod.rs

-8
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,6 @@ impl Context<'_> {
194194
.collect()
195195
}
196196

197-
/// Returns the [Location] of the definition of the given Ident found at [Span] of the given [FileId].
198-
/// Returns [None] when definition is not found.
199-
pub fn get_definition_location_from(&self, location: Location) -> Option<Location> {
200-
let interner = &self.def_interner;
201-
202-
interner.find_location_index(location).and_then(|index| interner.resolve_location(index))
203-
}
204-
205197
/// Return a Vec of all `contract` declarations in the source code and the functions they contain
206198
pub fn get_all_contracts(&self, crate_id: &CrateId) -> Vec<Contract> {
207199
self.def_map(crate_id)

compiler/noirc_frontend/src/node_interner.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -1271,10 +1271,16 @@ impl NodeInterner {
12711271
self.selected_trait_implementations.get(&ident_id).cloned()
12721272
}
12731273

1274+
/// Returns the [Location] of the definition of the given Ident found at [Span] of the given [FileId].
1275+
/// Returns [None] when definition is not found.
1276+
pub fn get_definition_location_from(&self, location: Location) -> Option<Location> {
1277+
self.find_location_index(location).and_then(|index| self.resolve_location(index))
1278+
}
1279+
12741280
/// For a given [Index] we return [Location] to which we resolved to
12751281
/// We currently return None for features not yet implemented
12761282
/// TODO(#3659): LSP goto def should error when Ident at Location could not resolve
1277-
pub(crate) fn resolve_location(&self, index: impl Into<Index>) -> Option<Location> {
1283+
fn resolve_location(&self, index: impl Into<Index>) -> Option<Location> {
12781284
let node = self.nodes.get(index.into())?;
12791285

12801286
match node {

tooling/lsp/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use noirc_driver::{file_manager_with_stdlib, prepare_crate, NOIR_ARTIFACT_VERSIO
2525
use noirc_frontend::{
2626
graph::{CrateId, CrateName},
2727
hir::{Context, FunctionNameMatch},
28+
node_interner::NodeInterner,
2829
};
2930

3031
use notifications::{
@@ -59,8 +60,10 @@ pub struct LspState {
5960
root_path: Option<PathBuf>,
6061
client: ClientSocket,
6162
solver: WrapperSolver,
63+
open_documents_count: usize,
6264
input_files: HashMap<String, String>,
6365
cached_lenses: HashMap<String, Vec<CodeLens>>,
66+
cached_definitions: HashMap<String, NodeInterner>,
6467
}
6568

6669
impl LspState {
@@ -71,6 +74,8 @@ impl LspState {
7174
solver: WrapperSolver(Box::new(solver)),
7275
input_files: HashMap::new(),
7376
cached_lenses: HashMap::new(),
77+
cached_definitions: HashMap::new(),
78+
open_documents_count: 0,
7479
}
7580
}
7681
}

tooling/lsp/src/notifications/mod.rs

+42-25
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,16 @@ pub(super) fn on_did_open_text_document(
3636
params: DidOpenTextDocumentParams,
3737
) -> ControlFlow<Result<(), async_lsp::Error>> {
3838
state.input_files.insert(params.text_document.uri.to_string(), params.text_document.text);
39-
ControlFlow::Continue(())
39+
40+
let document_uri = params.text_document.uri;
41+
42+
match process_noir_document(document_uri, state) {
43+
Ok(_) => {
44+
state.open_documents_count += 1;
45+
ControlFlow::Continue(())
46+
}
47+
Err(err) => ControlFlow::Break(Err(err)),
48+
}
4049
}
4150

4251
pub(super) fn on_did_change_text_document(
@@ -85,34 +94,39 @@ pub(super) fn on_did_close_text_document(
8594
) -> ControlFlow<Result<(), async_lsp::Error>> {
8695
state.input_files.remove(&params.text_document.uri.to_string());
8796
state.cached_lenses.remove(&params.text_document.uri.to_string());
97+
98+
state.open_documents_count -= 1;
99+
100+
if state.open_documents_count == 0 {
101+
state.cached_definitions.clear();
102+
}
103+
88104
ControlFlow::Continue(())
89105
}
90106

91107
pub(super) fn on_did_save_text_document(
92108
state: &mut LspState,
93109
params: DidSaveTextDocumentParams,
94110
) -> ControlFlow<Result<(), async_lsp::Error>> {
95-
let file_path = match params.text_document.uri.to_file_path() {
96-
Ok(file_path) => file_path,
97-
Err(()) => {
98-
return ControlFlow::Break(Err(ResponseError::new(
99-
ErrorCode::REQUEST_FAILED,
100-
"URI is not a valid file path",
101-
)
102-
.into()))
103-
}
104-
};
111+
let document_uri = params.text_document.uri;
105112

106-
let workspace = match resolve_workspace_for_source_path(&file_path) {
107-
Ok(value) => value,
108-
Err(lsp_error) => {
109-
return ControlFlow::Break(Err(ResponseError::new(
110-
ErrorCode::REQUEST_FAILED,
111-
lsp_error.to_string(),
112-
)
113-
.into()))
114-
}
115-
};
113+
match process_noir_document(document_uri, state) {
114+
Ok(_) => ControlFlow::Continue(()),
115+
Err(err) => ControlFlow::Break(Err(err)),
116+
}
117+
}
118+
119+
fn process_noir_document(
120+
document_uri: lsp_types::Url,
121+
state: &mut LspState,
122+
) -> Result<(), async_lsp::Error> {
123+
let file_path = document_uri.to_file_path().map_err(|_| {
124+
ResponseError::new(ErrorCode::REQUEST_FAILED, "URI is not a valid file path")
125+
})?;
126+
127+
let workspace = resolve_workspace_for_source_path(&file_path).map_err(|lsp_error| {
128+
ResponseError::new(ErrorCode::REQUEST_FAILED, lsp_error.to_string())
129+
})?;
116130

117131
let mut workspace_file_manager = file_manager_with_stdlib(&workspace.root_dir);
118132
insert_all_files_for_workspace_into_file_manager(&workspace, &mut workspace_file_manager);
@@ -127,6 +141,8 @@ pub(super) fn on_did_save_text_document(
127141
Err(errors_and_warnings) => errors_and_warnings,
128142
};
129143

144+
let package_root_dir: String = package.root_dir.as_os_str().to_string_lossy().into();
145+
130146
// We don't add test headings for a package if it contains no `#[test]` functions
131147
if let Some(tests) = get_package_tests_in_crate(&context, &crate_id, &package.name) {
132148
let _ = state.client.notify::<notification::NargoUpdateTests>(NargoPackageTests {
@@ -142,7 +158,9 @@ pub(super) fn on_did_save_text_document(
142158
package,
143159
Some(&file_path),
144160
);
145-
state.cached_lenses.insert(params.text_document.uri.to_string(), collected_lenses);
161+
state.cached_lenses.insert(document_uri.to_string(), collected_lenses);
162+
163+
state.cached_definitions.insert(package_root_dir, context.def_interner);
146164

147165
let fm = &context.file_manager;
148166
let files = fm.as_file_map();
@@ -178,14 +196,13 @@ pub(super) fn on_did_save_text_document(
178196
.collect()
179197
})
180198
.collect();
181-
182199
let _ = state.client.publish_diagnostics(PublishDiagnosticsParams {
183-
uri: params.text_document.uri,
200+
uri: document_uri,
184201
version: None,
185202
diagnostics,
186203
});
187204

188-
ControlFlow::Continue(())
205+
Ok(())
189206
}
190207

191208
pub(super) fn on_exit(

tooling/lsp/src/requests/goto_definition.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,27 @@ fn on_goto_definition_inner(
2929
let workspace = resolve_workspace_for_source_path(file_path.as_path()).unwrap();
3030
let package = workspace.members.first().unwrap();
3131

32+
let package_root_path: String = package.root_dir.as_os_str().to_string_lossy().into();
33+
3234
let mut workspace_file_manager = file_manager_with_stdlib(&workspace.root_dir);
3335
insert_all_files_for_workspace_into_file_manager(&workspace, &mut workspace_file_manager);
3436

3537
let (mut context, crate_id) = nargo::prepare_package(&workspace_file_manager, package);
3638

37-
// We ignore the warnings and errors produced by compilation while resolving the definition
38-
let _ = noirc_driver::check_crate(&mut context, crate_id, false, false);
39+
let interner;
40+
if let Some(def_interner) = _state.cached_definitions.get(&package_root_path) {
41+
interner = def_interner;
42+
} else {
43+
// We ignore the warnings and errors produced by compilation while resolving the definition
44+
let _ = noirc_driver::check_crate(&mut context, crate_id, false, false);
45+
interner = &context.def_interner;
46+
}
3947

4048
let files = context.file_manager.as_file_map();
4149
let file_id = context.file_manager.name_to_id(file_path.clone()).ok_or(ResponseError::new(
4250
ErrorCode::REQUEST_FAILED,
4351
format!("Could not find file in file manager. File path: {:?}", file_path),
4452
))?;
45-
4653
let byte_index =
4754
position_to_byte_index(files, file_id, &params.text_document_position_params.position)
4855
.map_err(|err| {
@@ -58,7 +65,7 @@ fn on_goto_definition_inner(
5865
};
5966

6067
let goto_definition_response =
61-
context.get_definition_location_from(search_for_location).and_then(|found_location| {
68+
interner.get_definition_location_from(search_for_location).and_then(|found_location| {
6269
let file_id = found_location.file;
6370
let definition_position = to_lsp_location(files, file_id, found_location.span)?;
6471
let response: GotoDefinitionResponse =

0 commit comments

Comments
 (0)