use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::mpsc::{Receiver, Sender};
use std::time::{Duration, Instant};
use serde_json::{Value, json};
const UPDATE_BUDGET: Duration = Duration::from_millis(1000);
const COMPLETION_BUDGET: Duration = Duration::from_millis(2500);
const STEADY_BUDGET: Duration = Duration::from_millis(50);
const STEADY_FROM: usize = 9;
const MAIN_CPP: &str = r#"#include <vector>
#include <string>
#include <format>
#include <iostream>
class Sux {
public:
Sux() : data(0) {}
Sux(int value) : data(value) {}
void print(int i) {
printf("Sux class\n %d\n", this->data);
}
private:
int data;
};
int main() {
Sux sux;
std::string ixx;
sux.print(ixx.size());
std::cout << std::format("{}", 0) << std::endl;
std::vector<int> v;
v.push_back(42);
std::string s = std::format("{}", 1);
auto x = std::vector<int>();
return 0;
}
"#;
#[test]
fn a_completion_asked_while_the_index_fills_comes_back_within_the_budget() {
if not_the_product() {
return;
}
let project = Project::new("latency");
project.write("main.cpp", MAIN_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "window": { "workDoneProgress": true } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": MAIN_CPP }
}),
);
let (line, character) = position_of(MAIN_CPP, "std::cout");
let mut slowest = Duration::ZERO;
let mut over: Vec<String> = Vec::new();
let mut every: Vec<String> = Vec::new();
let mut steady: Vec<Duration> = Vec::new();
for attempt in 0..12u32 {
let started = Instant::now();
let response = server.request(
100 + i64::from(attempt),
"textDocument/completion",
json!({
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character },
}),
);
let took = started.elapsed();
slowest = slowest.max(took);
let items = response["result"]["items"]
.as_array()
.or_else(|| response["result"].as_array())
.map(|items| items.len())
.unwrap_or(0);
eprintln!(
" completion #{attempt}: {} ms, {items} item(s)",
took.as_millis()
);
every.push(format!(
"#{attempt}: {} ms, {items} item(s)",
took.as_millis()
));
if attempt as usize >= STEADY_FROM {
steady.push(took);
}
if took > COMPLETION_BUDGET {
over.push(format!(
" #{attempt}: {} ms, {items} item(s)",
took.as_millis()
));
}
}
assert!(
over.is_empty(),
"a completion asked while the index fills must come back inside {budget} ms — every request waits for \
whatever an update holds the session for, and the numbers below are the wait:\n{list}\n\
(slowest {slowest} ms; every attempt was: {every})",
budget = COMPLETION_BUDGET.as_millis(),
list = over.join("\n"),
slowest = slowest.as_millis(),
every = every.join(", "),
);
const A_RENDER_MAY_TAKE: Duration = Duration::from_millis(300);
const RENDERS_IN_THE_WINDOW: usize = 2;
let over: Vec<(usize, Duration)> = steady
.iter()
.enumerate()
.filter(|(_, took)| **took > STEADY_BUDGET)
.map(|(at, took)| (STEADY_FROM + at, *took))
.collect();
let slow_steady: Vec<String> = over
.iter()
.map(|(at, took)| format!(" #{}: {} ms", at, took.as_millis()))
.collect();
assert!(
over.len() <= RENDERS_IN_THE_WINDOW,
"at most {RENDERS_IN_THE_WINDOW} steady-state attempts may pay for a render; this run had {}:\n{}",
over.len(),
slow_steady.join("\n"),
);
assert!(
over.iter().all(|(_, took)| *took <= A_RENDER_MAY_TAKE),
"the attempt that paid for a render must still be inside a render's budget ({} ms):\n{}",
A_RENDER_MAY_TAKE.as_millis(),
slow_steady.join("\n"),
);
let slow_after_the_render: Vec<String> = over
.iter()
.skip(RENDERS_IN_THE_WINDOW)
.map(|(at, took)| format!(" #{}: {} ms", at, took.as_millis()))
.collect();
assert!(
slow_after_the_render.is_empty(),
"more than the {RENDERS_IN_THE_WINDOW} render-paying attempts are over budget — that is a query \
regression, not a cook:\n{}",
slow_after_the_render.join("\n"),
);
}
#[test]
fn no_update_holds_the_analysis_for_longer_than_its_budget() {
if not_the_product() {
return;
}
let project = Project::new("latency-hold");
project.write("main.cpp", MAIN_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "window": { "workDoneProgress": true } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": MAIN_CPP }
}),
);
let (line, character) = position_of(MAIN_CPP, "return 0;");
let deadline = Instant::now() + Duration::from_secs(180);
loop {
let _ = server.request(
200,
"textDocument/completion",
json!({
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character },
}),
);
if let Some(held) = server.worst_write_hold() {
assert!(
held <= UPDATE_BUDGET.as_millis() as u64,
"an update held the analysis for {held} ms, over the {} ms budget — every request in flight waits \
for exactly this, so no amount of query tuning can make a completion faster than this number",
UPDATE_BUDGET.as_millis(),
);
return;
}
assert!(
Instant::now() < deadline,
"the server never finished indexing, so it never reported how long an update held the analysis"
);
}
}
fn uri_of(path: &Path) -> String {
let text = path.to_string_lossy().replace('\\', "/");
format!("file:///{}", text.trim_start_matches('/'))
}
fn position_of(source: &str, needle: &str) -> (u32, u32) {
let at = source.find(needle).expect("the needle is in the fixture");
let before = &source[..at];
let line = before.matches('\n').count() as u32;
let character = before
.rsplit('\n')
.next()
.map(|last| last.len())
.unwrap_or(0) as u32;
(line, character)
}
struct Project {
root: PathBuf,
}
impl Project {
fn new(name: &str) -> Project {
let root = std::env::temp_dir().join(format!("cppls-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("the test project");
Project { root }
}
fn root(&self) -> &Path {
&self.root
}
fn write(&self, name: &str, text: &str) {
std::fs::write(self.root.join(name), text).expect("the fixture file");
}
}
struct Server {
child: Child,
stdin: ChildStdin,
messages: Receiver<Value>,
log_dir: PathBuf,
}
impl Server {
fn start(root: &Path) -> Server {
let log_dir = root.join("log");
let _ = std::fs::create_dir_all(&log_dir);
let mut child = Command::new(env!("CARGO_BIN_EXE_cpp_ls"))
.arg("--log-path")
.arg(&log_dir)
.arg("--log-level")
.arg("debug")
.current_dir(root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("the server starts");
let stdin = child.stdin.take().expect("stdin");
let stdout = child.stdout.take().expect("stdout");
let (tx, messages): (Sender<Value>, Receiver<Value>) = std::sync::mpsc::channel();
std::thread::spawn(move || read_messages(stdout, tx));
Server {
child,
stdin,
messages,
log_dir,
}
}
fn worst_write_hold(&self) -> Option<u64> {
let entry = std::fs::read_dir(&self.log_dir).ok()?.flatten().next()?;
let text = std::fs::read_to_string(entry.path()).ok()?;
text.lines()
.filter_map(|line| line.split_once("held the analysis was "))
.filter_map(|(_, rest)| rest.split_whitespace().next())
.filter_map(|value| value.parse::<u64>().ok())
.max()
}
fn send(&mut self, message: Value) {
let body = serde_json::to_string(&message).expect("a message serializes");
write!(self.stdin, "Content-Length: {}\r\n\r\n{body}", body.len())
.expect("the message is written");
self.stdin.flush().expect("the message is flushed");
}
fn notify(&mut self, method: &str, params: Value) {
self.send(json!({ "jsonrpc": "2.0", "method": method, "params": params }));
}
fn request(&mut self, id: i64, method: &str, params: Value) -> Value {
self.send(json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params }));
self.wait_for(|message| message["id"] == json!(id))
}
fn wait_for(&mut self, accept: impl Fn(&Value) -> bool) -> Value {
let deadline = Instant::now() + Duration::from_secs(120);
loop {
let left = deadline.saturating_duration_since(Instant::now());
match self.messages.recv_timeout(left) {
Ok(message) => {
if accept(&message) {
return message;
}
}
Err(err) => panic!("the server did not answer: {err}"),
}
}
}
}
impl Drop for Server {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
fn read_messages(stdout: ChildStdout, tx: Sender<Value>) {
let mut reader = BufReader::new(stdout);
loop {
let mut length = None;
loop {
let mut header = String::new();
if reader.read_line(&mut header).unwrap_or(0) == 0 {
return;
}
let header = header.trim_end();
if header.is_empty() {
break;
}
if let Some(value) = header.strip_prefix("Content-Length: ") {
length = value.parse::<usize>().ok();
}
}
let Some(length) = length else { return };
let mut body = vec![0u8; length];
if reader.read_exact(&mut body).is_err() {
return;
}
let Ok(message) = serde_json::from_slice::<Value>(&body) else {
return;
};
if tx.send(message).is_err() {
return;
}
}
}
#[allow(unused_imports)]
use std::io::Read as _;
fn not_the_product() -> bool {
if cfg!(debug_assertions) {
eprintln!(
"skipped: this measures a debug build, which says nothing about the product — run it with --release"
);
return true;
}
false
}