summaryrefslogtreecommitdiff
path: root/cli/bench/lsp.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/bench/lsp.rs')
-rw-r--r--cli/bench/lsp.rs379
1 files changed, 379 insertions, 0 deletions
diff --git a/cli/bench/lsp.rs b/cli/bench/lsp.rs
new file mode 100644
index 000000000..63e1821d4
--- /dev/null
+++ b/cli/bench/lsp.rs
@@ -0,0 +1,379 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+use deno_core::error::generic_error;
+use deno_core::error::AnyError;
+use deno_core::serde::de;
+use deno_core::serde::Deserialize;
+use deno_core::serde::Serialize;
+use deno_core::serde_json;
+use deno_core::serde_json::json;
+use deno_core::serde_json::Value;
+use lazy_static::lazy_static;
+use regex::Regex;
+use std::collections::HashMap;
+use std::io::BufRead;
+use std::io::Read;
+use std::io::Write;
+use std::path::PathBuf;
+use std::process::ChildStdin;
+use std::process::ChildStdout;
+use std::process::Command;
+use std::process::Stdio;
+use std::time::Duration;
+use std::time::Instant;
+
+static FIXTURE_DB_TS: &str = include_str!("fixtures/db.ts");
+static FIXTURE_DB_MESSAGES: &[u8] = include_bytes!("fixtures/db_messages.json");
+static FIXTURE_INIT_JSON: &[u8] =
+ include_bytes!("fixtures/initialize_params.json");
+
+lazy_static! {
+ static ref CONTENT_TYPE_REG: Regex =
+ Regex::new(r"(?i)^content-length:\s+(\d+)").unwrap();
+}
+
+#[derive(Debug, Deserialize)]
+enum FixtureType {
+ #[serde(rename = "action")]
+ Action,
+ #[serde(rename = "change")]
+ Change,
+ #[serde(rename = "completion")]
+ Completion,
+ #[serde(rename = "highlight")]
+ Highlight,
+ #[serde(rename = "hover")]
+ Hover,
+}
+
+#[derive(Debug, Deserialize)]
+struct FixtureMessage {
+ #[serde(rename = "type")]
+ fixture_type: FixtureType,
+ params: Value,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+struct LspResponseError {
+ code: i32,
+ message: String,
+ data: Option<Value>,
+}
+
+#[derive(Debug)]
+enum LspMessage {
+ Notification(String, Option<Value>),
+ Request(u64, String, Option<Value>),
+ Response(u64, Option<Value>, Option<LspResponseError>),
+}
+
+impl<'a> From<&'a [u8]> for LspMessage {
+ fn from(s: &'a [u8]) -> Self {
+ let value: Value = serde_json::from_slice(s).unwrap();
+ let obj = value.as_object().unwrap();
+ if obj.contains_key("id") && obj.contains_key("method") {
+ let id = obj.get("id").unwrap().as_u64().unwrap();
+ let method = obj.get("method").unwrap().as_str().unwrap().to_string();
+ Self::Request(id, method, obj.get("params").cloned())
+ } else if obj.contains_key("id") {
+ let id = obj.get("id").unwrap().as_u64().unwrap();
+ let maybe_error: Option<LspResponseError> = obj
+ .get("error")
+ .map(|v| serde_json::from_value(v.clone()).unwrap());
+ Self::Response(id, obj.get("result").cloned(), maybe_error)
+ } else {
+ assert!(obj.contains_key("method"));
+ let method = obj.get("method").unwrap().as_str().unwrap().to_string();
+ Self::Notification(method, obj.get("params").cloned())
+ }
+ }
+}
+
+struct LspClient {
+ reader: std::io::BufReader<ChildStdout>,
+ child: std::process::Child,
+ request_id: u64,
+ start: Instant,
+ writer: std::io::BufWriter<ChildStdin>,
+}
+
+fn read_message<R>(reader: &mut R) -> Result<Vec<u8>, AnyError>
+where
+ R: Read + BufRead,
+{
+ let mut content_length = 0_usize;
+ loop {
+ let mut buf = String::new();
+ reader.read_line(&mut buf)?;
+ if let Some(captures) = CONTENT_TYPE_REG.captures(&buf) {
+ let content_length_match = captures
+ .get(1)
+ .ok_or_else(|| generic_error("missing capture"))?;
+ content_length = content_length_match.as_str().parse::<usize>()?;
+ }
+ if &buf == "\r\n" {
+ break;
+ }
+ }
+
+ let mut msg_buf = vec![0_u8; content_length];
+ reader.read_exact(&mut msg_buf)?;
+ Ok(msg_buf)
+}
+
+impl Drop for LspClient {
+ fn drop(&mut self) {
+ match self.child.try_wait() {
+ Ok(None) => {
+ self.child.kill().unwrap();
+ let _ = self.child.wait();
+ }
+ Ok(Some(status)) => panic!("deno lsp exited unexpectedly {}", status),
+ Err(e) => panic!("pebble error: {}", e),
+ }
+ }
+}
+
+impl LspClient {
+ fn new(deno_exe: &PathBuf) -> Result<Self, AnyError> {
+ let mut child = Command::new(deno_exe)
+ .arg("lsp")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::null())
+ .spawn()?;
+
+ let stdout = child.stdout.take().unwrap();
+ let reader = std::io::BufReader::new(stdout);
+
+ let stdin = child.stdin.take().unwrap();
+ let writer = std::io::BufWriter::new(stdin);
+
+ Ok(Self {
+ child,
+ reader,
+ request_id: 1,
+ start: Instant::now(),
+ writer,
+ })
+ }
+
+ fn duration(&self) -> Duration {
+ self.start.elapsed()
+ }
+
+ fn read(&mut self) -> Result<LspMessage, AnyError> {
+ let msg_buf = read_message(&mut self.reader)?;
+ let msg = LspMessage::from(msg_buf.as_slice());
+ Ok(msg)
+ }
+
+ fn read_notification<R>(&mut self) -> Result<(String, Option<R>), AnyError>
+ where
+ R: de::DeserializeOwned,
+ {
+ loop {
+ if let LspMessage::Notification(method, maybe_params) = self.read()? {
+ if let Some(p) = maybe_params {
+ let params = serde_json::from_value(p)?;
+ return Ok((method, Some(params)));
+ } else {
+ return Ok((method, None));
+ }
+ }
+ }
+ }
+
+ fn write(&mut self, value: Value) -> Result<(), AnyError> {
+ let value_str = value.to_string();
+ let msg = format!(
+ "Content-Length: {}\r\n\r\n{}",
+ value_str.as_bytes().len(),
+ value_str
+ );
+ self.writer.write_all(msg.as_bytes())?;
+ self.writer.flush()?;
+ Ok(())
+ }
+
+ fn write_request<S, V>(
+ &mut self,
+ method: S,
+ params: V,
+ ) -> Result<(Option<Value>, Option<LspResponseError>), AnyError>
+ where
+ S: AsRef<str>,
+ V: Serialize,
+ {
+ let value = json!({
+ "jsonrpc": "2.0",
+ "id": self.request_id,
+ "method": method.as_ref(),
+ "params": params,
+ });
+ self.write(value)?;
+
+ loop {
+ if let LspMessage::Response(id, result, error) = self.read()? {
+ assert_eq!(id, self.request_id);
+ self.request_id += 1;
+ return Ok((result, error));
+ }
+ }
+ }
+
+ fn write_notification<S, V>(
+ &mut self,
+ method: S,
+ params: V,
+ ) -> Result<(), AnyError>
+ where
+ S: AsRef<str>,
+ V: Serialize,
+ {
+ let value = json!({
+ "jsonrpc": "2.0",
+ "method": method.as_ref(),
+ "params": params,
+ });
+ self.write(value)?;
+ Ok(())
+ }
+}
+
+/// A benchmark that opens a 8000+ line TypeScript document, adds a function to
+/// the end of the document and does a level of hovering and gets quick fix
+/// code actions.
+fn bench_big_file_edits(deno_exe: &PathBuf) -> Result<Duration, AnyError> {
+ let mut client = LspClient::new(deno_exe)?;
+
+ let params: Value = serde_json::from_slice(FIXTURE_INIT_JSON)?;
+ let (_, response_error): (Option<Value>, Option<LspResponseError>) =
+ client.write_request("initialize", params)?;
+ assert!(response_error.is_none());
+
+ client.write_notification("initialized", json!({}))?;
+
+ client.write_notification(
+ "textDocument/didOpen",
+ json!({
+ "textDocument": {
+ "uri": "file:///fixtures/db.ts",
+ "languageId": "typescript",
+ "version": 1,
+ "text": FIXTURE_DB_TS
+ }
+ }),
+ )?;
+
+ let (method, _): (String, Option<Value>) = client.read_notification()?;
+ assert_eq!(method, "textDocument/publishDiagnostics");
+
+ let messages: Vec<FixtureMessage> =
+ serde_json::from_slice(FIXTURE_DB_MESSAGES)?;
+
+ for msg in messages {
+ match msg.fixture_type {
+ FixtureType::Action => {
+ client.write_request("textDocument/codeAction", msg.params)?;
+ }
+ FixtureType::Change => {
+ client.write_notification("textDocument/didChange", msg.params)?;
+ }
+ FixtureType::Completion => {
+ client.write_request("textDocument/completion", msg.params)?;
+ }
+ FixtureType::Highlight => {
+ client.write_request("textDocument/documentHighlight", msg.params)?;
+ }
+ FixtureType::Hover => {
+ client.write_request("textDocument/hover", msg.params)?;
+ }
+ }
+ }
+
+ let (_, response_error): (Option<Value>, Option<LspResponseError>) =
+ client.write_request("shutdown", json!(null))?;
+ assert!(response_error.is_none());
+
+ client.write_notification("exit", json!(null))?;
+
+ Ok(client.duration())
+}
+
+/// A test that starts up the LSP, opens a single line document, and exits.
+fn bench_startup_shutdown(deno_exe: &PathBuf) -> Result<Duration, AnyError> {
+ let mut client = LspClient::new(deno_exe)?;
+
+ let params: Value = serde_json::from_slice(FIXTURE_INIT_JSON)?;
+ let (_, response_error): (Option<Value>, Option<LspResponseError>) =
+ client.write_request("initialize", params)?;
+ assert!(response_error.is_none());
+
+ client.write_notification("initialized", json!({}))?;
+
+ client.write_notification(
+ "textDocument/didOpen",
+ json!({
+ "textDocument": {
+ "uri": "file:///a/file.ts",
+ "languageId": "typescript",
+ "version": 1,
+ "text": "console.log(Deno.args);\n"
+ }
+ }),
+ )?;
+
+ let (method, _): (String, Option<Value>) = client.read_notification()?;
+ assert_eq!(method, "textDocument/publishDiagnostics");
+
+ let (_, response_error): (Option<Value>, Option<LspResponseError>) =
+ client.write_request("shutdown", json!(null))?;
+ assert!(response_error.is_none());
+
+ client.write_notification("exit", json!(null))?;
+
+ Ok(client.duration())
+}
+
+/// Generate benchmarks for the LSP server.
+pub(crate) fn benchmarks(
+ deno_exe: &PathBuf,
+) -> Result<HashMap<String, u64>, AnyError> {
+ println!("-> Start benchmarking lsp");
+ let mut exec_times = HashMap::new();
+
+ println!(" - Simple Startup/Shutdown ");
+ let mut times = Vec::new();
+ for _ in 0..10 {
+ times.push(bench_startup_shutdown(deno_exe)?);
+ }
+ let mean =
+ (times.iter().sum::<Duration>() / times.len() as u32).as_millis() as u64;
+ println!(" ({} runs, mean: {}ms)", times.len(), mean);
+ exec_times.insert("startup_shutdown".to_string(), mean);
+
+ println!(" - Big Document/Several Edits ");
+ let mut times = Vec::new();
+ for _ in 0..5 {
+ times.push(bench_big_file_edits(deno_exe)?);
+ }
+ let mean =
+ (times.iter().sum::<Duration>() / times.len() as u32).as_millis() as u64;
+ println!(" ({} runs, mean: {}ms)", times.len(), mean);
+ exec_times.insert("big_file_edits".to_string(), mean);
+
+ println!("<- End benchmarking lsp");
+
+ Ok(exec_times)
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn test_read_message() {
+ let msg = b"content-length: 11\r\n\r\nhello world";
+ let reader = std::io::Cursor::new(msg);
+ assert_eq!(read_message(reader).unwrap(), b"hello world");
+ }
+}