diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index b322438fab..b892ba8d3c 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -31,6 +31,7 @@ use super::diagnostics; use super::diagnostics::DiagnosticCollection; use super::diagnostics::DiagnosticSource; use super::documents::DocumentCache; +use super::performance::Performance; use super::sources; use super::sources::Sources; use super::text; @@ -54,14 +55,15 @@ pub struct StateSnapshot { struct Inner { assets: HashMap>, client: Client, - ts_server: TsServer, config: Config, - documents: DocumentCache, - sources: Sources, diagnostics: DiagnosticCollection, + documents: DocumentCache, maybe_config_uri: Option, maybe_import_map: Option, maybe_import_map_uri: Option, + performance: Performance, + sources: Sources, + ts_server: TsServer, } impl LanguageServer { @@ -81,14 +83,15 @@ impl Inner { Self { assets: Default::default(), client, - ts_server: TsServer::new(), config: Default::default(), - documents: Default::default(), - sources, diagnostics: Default::default(), + documents: Default::default(), maybe_config_uri: Default::default(), maybe_import_map: Default::default(), maybe_import_map_uri: Default::default(), + performance: Default::default(), + sources, + ts_server: TsServer::new(), } } @@ -103,7 +106,8 @@ impl Inner { &self, specifier: ModuleSpecifier, ) -> Result { - if specifier.as_url().scheme() == "asset" { + let mark = self.performance.mark("get_line_index"); + let result = if specifier.as_url().scheme() == "asset" { let maybe_asset = self.assets.get(&specifier).cloned(); if let Some(maybe_asset) = maybe_asset { if let Some(asset) = maybe_asset { @@ -128,7 +132,9 @@ impl Inner { Ok(line_index) } else { Err(anyhow!("Unable to find line index for: {}", specifier)) - } + }; + self.performance.measure(mark); + result } /// Only searches already cached assets and documents for a line index. If @@ -137,7 +143,8 @@ impl Inner { &self, specifier: &ModuleSpecifier, ) -> Option { - if specifier.as_url().scheme() == "asset" { + let mark = self.performance.mark("get_line_index_sync"); + let maybe_line_index = if specifier.as_url().scheme() == "asset" { if let Some(Some(asset)) = self.assets.get(specifier) { Some(asset.line_index.clone()) } else { @@ -150,7 +157,9 @@ impl Inner { } else { self.sources.get_line_index(specifier) } - } + }; + self.performance.measure(mark); + maybe_line_index } async fn prepare_diagnostics(&mut self) -> Result<(), AnyError> { @@ -162,6 +171,7 @@ impl Inner { let lint = async { let mut diagnostics = None; if lint_enabled { + let mark = self.performance.mark("prepare_diagnostics_lint"); diagnostics = Some( diagnostics::generate_lint_diagnostics( self.snapshot(), @@ -169,6 +179,7 @@ impl Inner { ) .await, ); + self.performance.measure(mark); }; Ok::<_, AnyError>(diagnostics) }; @@ -176,6 +187,7 @@ impl Inner { let ts = async { let mut diagnostics = None; if enabled { + let mark = self.performance.mark("prepare_diagnostics_ts"); diagnostics = Some( diagnostics::generate_ts_diagnostics( self.snapshot(), @@ -184,6 +196,7 @@ impl Inner { ) .await?, ); + self.performance.measure(mark); }; Ok::<_, AnyError>(diagnostics) }; @@ -191,6 +204,7 @@ impl Inner { let deps = async { let mut diagnostics = None; if enabled { + let mark = self.performance.mark("prepare_diagnostics_deps"); diagnostics = Some( diagnostics::generate_dependency_diagnostics( self.snapshot(), @@ -198,6 +212,7 @@ impl Inner { ) .await?, ); + self.performance.measure(mark); }; Ok::<_, AnyError>(diagnostics) }; @@ -249,6 +264,7 @@ impl Inner { } async fn publish_diagnostics(&mut self) -> Result<(), AnyError> { + let mark = self.performance.mark("publish_diagnostics"); let (maybe_changes, diagnostics_collection) = { let diagnostics_collection = &mut self.diagnostics; let maybe_changes = diagnostics_collection.take_changes(); @@ -291,6 +307,7 @@ impl Inner { } } + self.performance.measure(mark); Ok(()) } @@ -302,7 +319,8 @@ impl Inner { } } - async fn update_import_map(&mut self) -> Result<(), AnyError> { + pub async fn update_import_map(&mut self) -> Result<(), AnyError> { + let mark = self.performance.mark("update_import_map"); let (maybe_import_map, maybe_root_uri) = { let config = &self.config; (config.settings.import_map.clone(), config.root_uri.clone()) @@ -344,10 +362,12 @@ impl Inner { } else { self.maybe_import_map = None; } + self.performance.measure(mark); Ok(()) } async fn update_tsconfig(&mut self) -> Result<(), AnyError> { + let mark = self.performance.mark("update_tsconfig"); let mut tsconfig = TsConfig::new(json!({ "allowJs": true, "experimentalDecorators": true, @@ -413,6 +433,7 @@ impl Inner { .ts_server .request(self.snapshot(), tsc::RequestMethod::Configure(tsconfig)) .await?; + self.performance.measure(mark); Ok(()) } } @@ -513,6 +534,7 @@ impl Inner { } async fn did_open(&mut self, params: DidOpenTextDocumentParams) { + let mark = self.performance.mark("did_open"); if params.text_document.uri.scheme() == "deno" { // we can ignore virtual text documents opening, as they don't need to // be tracked in memory, as they are static assets that won't change @@ -532,6 +554,7 @@ impl Inner { error!("{}", err); } + self.performance.measure(mark); // TODO(@kitsonk): how to better lazily do this? if let Err(err) = self.prepare_diagnostics().await { error!("{}", err); @@ -539,6 +562,7 @@ impl Inner { } async fn did_change(&mut self, params: DidChangeTextDocumentParams) { + let mark = self.performance.mark("did_change"); let specifier = utils::normalize_url(params.text_document.uri); if let Err(err) = self.documents.change( &specifier, @@ -554,6 +578,7 @@ impl Inner { error!("{}", err); } + self.performance.measure(mark); // TODO(@kitsonk): how to better lazily do this? if let Err(err) = self.prepare_diagnostics().await { error!("{}", err); @@ -561,6 +586,7 @@ impl Inner { } async fn did_close(&mut self, params: DidCloseTextDocumentParams) { + let mark = self.performance.mark("did_close"); if params.text_document.uri.scheme() == "deno" { // we can ignore virtual text documents opening, as they don't need to // be tracked in memory, as they are static assets that won't change @@ -574,6 +600,7 @@ impl Inner { if let Err(err) = self.prepare_diagnostics().await { error!("{}", err); } + self.performance.measure(mark); } async fn did_save(&self, _params: DidSaveTextDocumentParams) { @@ -584,6 +611,7 @@ impl Inner { &mut self, params: DidChangeConfigurationParams, ) { + let mark = self.performance.mark("did_change_configuration"); let config = if self.config.client_capabilities.workspace_configuration { self .client @@ -625,12 +653,14 @@ impl Inner { } else { error!("received empty extension settings from the client"); } + self.performance.measure(mark); } async fn did_change_watched_files( &mut self, params: DidChangeWatchedFilesParams, ) { + let mark = self.performance.mark("did_change_watched_files"); // if the current import map has changed, we need to reload it if let Some(import_map_uri) = &self.maybe_import_map_uri { if params.changes.iter().any(|fe| *import_map_uri == fe.uri) { @@ -653,12 +683,14 @@ impl Inner { } } } + self.performance.measure(mark); } async fn formatting( &self, params: DocumentFormattingParams, ) -> LspResult>> { + let mark = self.performance.mark("formatting"); let specifier = utils::normalize_url(params.text_document.uri.clone()); let file_text = self .documents @@ -697,6 +729,7 @@ impl Inner { .await .unwrap(); + self.performance.measure(mark); if let Some(text_edits) = text_edits { if text_edits.is_empty() { Ok(None) @@ -713,6 +746,7 @@ impl Inner { if !self.enabled() { return Ok(None); } + let mark = self.performance.mark("hover"); let specifier = utils::normalize_url( params.text_document_position_params.text_document.uri, ); @@ -736,8 +770,10 @@ impl Inner { serde_json::from_value(res).unwrap(); if let Some(quick_info) = maybe_quick_info { let hover = quick_info.to_hover(&line_index); + self.performance.measure(mark); Ok(Some(hover)) } else { + self.performance.measure(mark); Ok(None) } } @@ -749,6 +785,7 @@ impl Inner { if !self.enabled() { return Ok(None); } + let mark = self.performance.mark("document_highlight"); let specifier = utils::normalize_url( params.text_document_position_params.text_document.uri, ); @@ -774,14 +811,15 @@ impl Inner { serde_json::from_value(res).unwrap(); if let Some(document_highlights) = maybe_document_highlights { - Ok(Some( - document_highlights - .into_iter() - .map(|dh| dh.to_highlight(&line_index)) - .flatten() - .collect(), - )) + let result = document_highlights + .into_iter() + .map(|dh| dh.to_highlight(&line_index)) + .flatten() + .collect(); + self.performance.measure(mark); + Ok(Some(result)) } else { + self.performance.measure(mark); Ok(None) } } @@ -793,6 +831,7 @@ impl Inner { if !self.enabled() { return Ok(None); } + let mark = self.performance.mark("references"); let specifier = utils::normalize_url(params.text_document_position.text_document.uri); let line_index = @@ -829,8 +868,10 @@ impl Inner { results.push(reference.to_location(&line_index)); } + self.performance.measure(mark); Ok(Some(results)) } else { + self.performance.measure(mark); Ok(None) } } @@ -842,6 +883,7 @@ impl Inner { if !self.enabled() { return Ok(None); } + let mark = self.performance.mark("goto_definition"); let specifier = utils::normalize_url( params.text_document_position_params.text_document.uri, ); @@ -865,12 +907,13 @@ impl Inner { serde_json::from_value(res).unwrap(); if let Some(definition) = maybe_definition { - Ok( - definition - .to_definition(&line_index, |s| self.get_line_index(s)) - .await, - ) + let results = definition + .to_definition(&line_index, |s| self.get_line_index(s)) + .await; + self.performance.measure(mark); + Ok(results) } else { + self.performance.measure(mark); Ok(None) } } @@ -882,6 +925,7 @@ impl Inner { if !self.enabled() { return Ok(None); } + let mark = self.performance.mark("completion"); let specifier = utils::normalize_url(params.text_document_position.text_document.uri); // TODO(lucacasonato): handle error correctly @@ -910,8 +954,11 @@ impl Inner { serde_json::from_value(res).unwrap(); if let Some(completions) = maybe_completion_info { - Ok(Some(completions.into_completion_response(&line_index))) + let results = completions.into_completion_response(&line_index); + self.performance.measure(mark); + Ok(Some(results)) } else { + self.performance.measure(mark); Ok(None) } } @@ -923,6 +970,7 @@ impl Inner { if !self.enabled() { return Ok(None); } + let mark = self.performance.mark("goto_implementation"); let specifier = utils::normalize_url( params.text_document_position_params.text_document.uri, ); @@ -971,8 +1019,10 @@ impl Inner { results.push(link); } } + self.performance.measure(mark); Ok(Some(GotoDefinitionResponse::Link(results))) } else { + self.performance.measure(mark); Ok(None) } } @@ -984,6 +1034,7 @@ impl Inner { if !self.enabled() { return Ok(None); } + let mark = self.performance.mark("goto_implementation"); let specifier = utils::normalize_url(params.text_document_position.text_document.uri); @@ -1039,8 +1090,10 @@ impl Inner { error!("Failed to get workspace edits: {:#?}", err); LspError::internal_error() })?; + self.performance.measure(mark); Ok(Some(workspace_edits)) } else { + self.performance.measure(mark); Ok(None) } } @@ -1061,6 +1114,7 @@ impl Inner { Some(Err(err)) => Err(LspError::invalid_params(err.to_string())), None => Err(LspError::invalid_params("Missing parameters")), }, + "deno/performance" => self.get_performance(), "deno/virtualTextDocument" => match params.map(serde_json::from_value) { Some(Ok(params)) => Ok(Some( serde_json::to_value(self.virtual_text_document(params).await?) @@ -1206,6 +1260,7 @@ struct VirtualTextDocumentParams { impl Inner { async fn cache(&mut self, params: CacheParams) -> LspResult { + let mark = self.performance.mark("cache"); let specifier = utils::normalize_url(params.text_document.uri); let maybe_import_map = self.maybe_import_map.clone(); sources::cache(specifier.clone(), maybe_import_map) @@ -1221,24 +1276,40 @@ impl Inner { error!("{}", err); LspError::internal_error() })?; + self.performance.measure(mark); Ok(true) } + fn get_performance(&self) -> LspResult> { + let averages = self.performance.averages(); + Ok(Some(json!({ "averages": averages }))) + } + async fn virtual_text_document( &self, params: VirtualTextDocumentParams, ) -> LspResult> { + let mark = self.performance.mark("virtual_text_document"); let specifier = utils::normalize_url(params.text_document.uri); let url = specifier.as_url(); let contents = if url.as_str() == "deno:/status.md" { - Some(format!( + let mut contents = String::new(); + + contents.push_str(&format!( r#"# Deno Language Server Status - Documents in memory: {} - - "#, +"#, self.documents.len() - )) + )); + contents.push_str("\n## Performance\n\n"); + for average in self.performance.averages() { + contents.push_str(&format!( + " - {}: {}ms ({})\n", + average.name, average.average_duration, average.count + )); + } + Some(contents) } else { match url.scheme() { "asset" => { @@ -1273,6 +1344,7 @@ impl Inner { } } }; + self.performance.measure(mark); Ok(contents) } } @@ -1280,6 +1352,7 @@ impl Inner { #[cfg(test)] mod tests { use super::*; + use crate::lsp::performance::PerformanceAverage; use lspower::jsonrpc; use lspower::ExitedError; use lspower::LspService; @@ -1288,19 +1361,25 @@ mod tests { use std::time::Instant; use tower_test::mock::Spawn; - enum LspResponse { + enum LspResponse + where + V: FnOnce(Value), + { None, RequestAny, Request(u64, Value), + RequestAssert(V), } + type LspTestHarnessRequest = (&'static str, LspResponse); + struct LspTestHarness { - requests: Vec<(&'static str, LspResponse)>, + requests: Vec, service: Spawn, } impl LspTestHarness { - pub fn new(requests: Vec<(&'static str, LspResponse)>) -> Self { + pub fn new(requests: Vec) -> Self { let (service, _) = LspService::new(LanguageServer::new); let service = Spawn::new(service); Self { requests, service } @@ -1330,6 +1409,10 @@ mod tests { ), _ => panic!("unexpected result: {:?}", result), }, + LspResponse::RequestAssert(assert) => match result { + Some(jsonrpc::Outgoing::Response(resp)) => assert(json!(resp)), + _ => panic!("unexpected result: {:?}", result), + }, }, Err(err) => panic!("Error result: {}", err), } @@ -1609,6 +1692,11 @@ mod tests { ("initialized_notification.json", LspResponse::None), ("did_open_notification_large.json", LspResponse::None), ("did_change_notification_large.json", LspResponse::None), + ("did_change_notification_large_02.json", LspResponse::None), + ("did_change_notification_large_03.json", LspResponse::None), + ("hover_request_large_01.json", LspResponse::RequestAny), + ("hover_request_large_02.json", LspResponse::RequestAny), + ("hover_request_large_03.json", LspResponse::RequestAny), ( "shutdown_request.json", LspResponse::Request(3, json!(null)), @@ -1676,4 +1764,61 @@ mod tests { ]); harness.run().await; } + + #[derive(Deserialize)] + struct PerformanceAverages { + averages: Vec, + } + #[derive(Deserialize)] + struct PerformanceResponse { + result: PerformanceAverages, + } + + #[tokio::test] + async fn test_deno_performance_request() { + let mut harness = LspTestHarness::new(vec![ + ("initialize_request.json", LspResponse::RequestAny), + ("initialized_notification.json", LspResponse::None), + ("did_open_notification.json", LspResponse::None), + ( + "hover_request.json", + LspResponse::Request( + 2, + json!({ + "contents": [ + { + "language": "typescript", + "value": "const Deno.args: string[]" + }, + "Returns the script arguments to the program. If for example we run a\nprogram:\n\ndeno run --allow-read https://deno.land/std/examples/cat.ts /etc/passwd\n\nThen `Deno.args` will contain:\n\n[ \"/etc/passwd\" ]" + ], + "range": { + "start": { + "line": 0, + "character": 17 + }, + "end": { + "line": 0, + "character": 21 + } + } + }), + ), + ), + ( + "performance_request.json", + LspResponse::RequestAssert(|value| { + let resp: PerformanceResponse = + serde_json::from_value(value).unwrap(); + assert_eq!(resp.result.averages.len(), 9); + }), + ), + ( + "shutdown_request.json", + LspResponse::Request(3, json!(null)), + ), + ("exit_notification.json", LspResponse::None), + ]); + harness.run().await; + } } diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index 91880fc85f..110bb4db2d 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -9,6 +9,7 @@ mod config; mod diagnostics; mod documents; mod language_server; +mod performance; mod sources; mod text; mod tsc; diff --git a/cli/lsp/performance.rs b/cli/lsp/performance.rs new file mode 100644 index 0000000000..8668519c8d --- /dev/null +++ b/cli/lsp/performance.rs @@ -0,0 +1,171 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use deno_core::serde::Deserialize; +use deno_core::serde::Serialize; +use std::collections::HashMap; +use std::collections::VecDeque; +use std::sync::Arc; +use std::sync::Mutex; +use std::time::Duration; +use std::time::Instant; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PerformanceAverage { + pub name: String, + pub count: u32, + pub average_duration: u32, +} + +/// A structure which serves as a start of a measurement span. +#[derive(Debug)] +pub struct PerformanceMark { + name: String, + count: u32, + start: Instant, +} + +/// A structure which holds the information about the measured span. +#[derive(Debug, Clone)] +pub struct PerformanceMeasure { + pub name: String, + pub count: u32, + pub duration: Duration, +} + +impl From for PerformanceMeasure { + fn from(value: PerformanceMark) -> Self { + Self { + name: value.name, + count: value.count, + duration: value.start.elapsed(), + } + } +} + +/// A simple structure for marking a start of something to measure the duration +/// of and measuring that duration. Each measurement is identified by a string +/// name and a counter is incremented each time a new measurement is marked. +/// +/// The structure will limit the size of measurements to the most recent 1000, +/// and will roll off when that limit is reached. +#[derive(Debug)] +pub struct Performance { + counts: Arc>>, + max_size: usize, + measures: Arc>>, +} + +impl Default for Performance { + fn default() -> Self { + Self { + counts: Default::default(), + max_size: 1_000, + measures: Default::default(), + } + } +} + +impl Performance { + /// Return the count and average duration of a measurement identified by name. + #[cfg(test)] + pub fn average(&self, name: &str) -> Option<(usize, Duration)> { + let mut items = Vec::new(); + for measure in self.measures.lock().unwrap().iter() { + if measure.name == name { + items.push(measure.duration); + } + } + let len = items.len(); + + if len > 0 { + let average = items.into_iter().sum::() / len as u32; + Some((len, average)) + } else { + None + } + } + + /// Return an iterator which provides the names, count, and average duration + /// of each measurement. + pub fn averages(&self) -> Vec { + let mut averages: HashMap> = HashMap::new(); + for measure in self.measures.lock().unwrap().iter() { + averages + .entry(measure.name.clone()) + .or_default() + .push(measure.duration); + } + averages + .into_iter() + .map(|(k, d)| { + let a = d.clone().into_iter().sum::() / d.len() as u32; + PerformanceAverage { + name: k, + count: d.len() as u32, + average_duration: a.as_millis() as u32, + } + }) + .collect() + } + + /// Marks the start of a measurement which returns a performance mark + /// structure, which is then passed to `.measure()` to finalize the duration + /// and add it to the internal buffer. + pub fn mark>(&self, name: S) -> PerformanceMark { + let name = name.as_ref(); + let mut counts = self.counts.lock().unwrap(); + let count = counts.entry(name.to_string()).or_insert(0); + *count += 1; + PerformanceMark { + name: name.to_string(), + count: *count, + start: Instant::now(), + } + } + + /// A function which accepts a previously created performance mark which will + /// be used to finalize the duration of the span being measured, and add the + /// measurement to the internal buffer. + pub fn measure(&self, mark: PerformanceMark) { + let measure = PerformanceMeasure::from(mark); + let mut measures = self.measures.lock().unwrap(); + measures.push_back(measure); + while measures.len() > self.max_size { + measures.pop_front(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_average() { + let performance = Performance::default(); + let mark1 = performance.mark("a"); + let mark2 = performance.mark("a"); + let mark3 = performance.mark("b"); + performance.measure(mark2); + performance.measure(mark1); + performance.measure(mark3); + let (count, _) = performance.average("a").expect("should have had value"); + assert_eq!(count, 2); + let (count, _) = performance.average("b").expect("should have had value"); + assert_eq!(count, 1); + assert!(performance.average("c").is_none()); + } + + #[test] + fn test_averages() { + let performance = Performance::default(); + let mark1 = performance.mark("a"); + let mark2 = performance.mark("a"); + performance.measure(mark2); + performance.measure(mark1); + let averages = performance.averages(); + assert_eq!(averages.len(), 1); + assert_eq!(averages[0].count, 2); + } +} diff --git a/cli/tests/lsp/did_change_notification_large_02.json b/cli/tests/lsp/did_change_notification_large_02.json new file mode 100644 index 0000000000..e8744d620e --- /dev/null +++ b/cli/tests/lsp/did_change_notification_large_02.json @@ -0,0 +1,25 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/didChange", + "params": { + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 445, + "character": 4 + }, + "end": { + "line": 445, + "character": 4 + } + }, + "text": "// " + } + ] + } +} diff --git a/cli/tests/lsp/did_change_notification_large_03.json b/cli/tests/lsp/did_change_notification_large_03.json new file mode 100644 index 0000000000..f39234510f --- /dev/null +++ b/cli/tests/lsp/did_change_notification_large_03.json @@ -0,0 +1,25 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/didChange", + "params": { + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 477, + "character": 4 + }, + "end": { + "line": 477, + "character": 9 + } + }, + "text": "error" + } + ] + } +} diff --git a/cli/tests/lsp/hover_request_large_01.json b/cli/tests/lsp/hover_request_large_01.json new file mode 100644 index 0000000000..78ddee5a33 --- /dev/null +++ b/cli/tests/lsp/hover_request_large_01.json @@ -0,0 +1,14 @@ +{ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/hover", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 421, + "character": 30 + } + } +} diff --git a/cli/tests/lsp/hover_request_large_02.json b/cli/tests/lsp/hover_request_large_02.json new file mode 100644 index 0000000000..4d7eae1509 --- /dev/null +++ b/cli/tests/lsp/hover_request_large_02.json @@ -0,0 +1,14 @@ +{ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/hover", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 444, + "character": 6 + } + } +} diff --git a/cli/tests/lsp/hover_request_large_03.json b/cli/tests/lsp/hover_request_large_03.json new file mode 100644 index 0000000000..5309450dd8 --- /dev/null +++ b/cli/tests/lsp/hover_request_large_03.json @@ -0,0 +1,14 @@ +{ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/hover", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + }, + "position": { + "line": 461, + "character": 34 + } + } +} diff --git a/cli/tests/lsp/performance_request.json b/cli/tests/lsp/performance_request.json new file mode 100644 index 0000000000..cb389cb9d3 --- /dev/null +++ b/cli/tests/lsp/performance_request.json @@ -0,0 +1,6 @@ +{ + "jsonrpc": "2.0", + "id": 99, + "method": "deno/performance", + "params": {} +}