Skip to content

Commit 0d189cc

Browse files
authored
Merge pull request #11 from OxfordAbstracts/lsp-2
Lsp 2
2 parents e92b840 + c03651d commit 0d189cc

34 files changed

Lines changed: 3096 additions & 445 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Thumbs.db
2424

2525

2626
# lsp vscode client
27-
2827
/editors/code/node_modules
2928
/editors/code/out
29+
/editors/code/.vscode-test
30+
/editors/code/package-lock.json

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ edition = "2021"
77
name = "pfc"
88
path = "src/main.rs"
99

10+
[[bin]]
11+
name = "profile-load-sources"
12+
path = "src/bin/profile_load_sources.rs"
13+
1014
[dependencies]
1115
clap = { version = "4", features = ["derive"] }
1216
log = "0.4"
@@ -26,7 +30,10 @@ rayon = "1.10"
2630
mimalloc = { version = "0.1", default-features = false }
2731
tower-lsp = "0.20"
2832
tokio = { version = "1", features = ["full"] }
33+
serde = { version = "1", features = ["derive"] }
2934
serde_json = "1"
35+
bincode = "1"
36+
zstd = "0.13"
3037

3138
[build-dependencies]
3239
lalrpop = "0.22"

editors/code/package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@
2727
]
2828
}
2929
],
30+
"commands": [
31+
{
32+
"command": "pfc.rebuildModule",
33+
"title": "PFC: Rebuild Current Module"
34+
},
35+
{
36+
"command": "pfc.rebuildProject",
37+
"title": "PFC: Rebuild Project"
38+
}
39+
],
3040
"configuration": {
3141
"title": "PureScript Fast Compiler",
3242
"properties": {
@@ -37,7 +47,7 @@
3747
},
3848
"pfc.sourcesCommand": {
3949
"type": "string",
40-
"default": "spago sources",
50+
"default": "ragu sources",
4151
"description": "Shell command that outputs PureScript source file paths (one per line). Example: find src .spago/p -name '*.purs'"
4252
}
4353
}

editors/code/src/extension.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,30 @@ export function activate(context: vscode.ExtensionContext) {
3333
clientOptions
3434
);
3535

36+
context.subscriptions.push(
37+
vscode.commands.registerCommand("pfc.rebuildModule", async () => {
38+
const editor = vscode.window.activeTextEditor;
39+
if (!editor) {
40+
vscode.window.showWarningMessage("No active editor");
41+
return;
42+
}
43+
if (!client) {
44+
vscode.window.showWarningMessage("Language server not running");
45+
return;
46+
}
47+
await client.sendRequest("pfc/rebuildModule", {
48+
uri: editor.document.uri.toString(),
49+
});
50+
}),
51+
vscode.commands.registerCommand("pfc.rebuildProject", async () => {
52+
if (!client) {
53+
vscode.window.showWarningMessage("Language server not running");
54+
return;
55+
}
56+
await client.sendRequest("pfc/rebuildProject");
57+
})
58+
);
59+
3660
client.start();
3761
}
3862

src/bin/profile_load_sources.rs

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
use std::collections::HashSet;
2+
use std::path::PathBuf;
3+
use std::time::Instant;
4+
5+
use clap::Parser;
6+
use rayon::prelude::*;
7+
8+
use purescript_fast_compiler::build::{self, BuildOptions};
9+
use purescript_fast_compiler::lsp::utils::find_definition::DefinitionIndex;
10+
use purescript_fast_compiler::lsp::utils::resolve::ResolutionExports;
11+
12+
/// Profile the LSP load_sources phases with per-phase timing.
13+
#[derive(Parser)]
14+
#[command(name = "profile-load-sources")]
15+
struct Args {
16+
/// Working directory to run the sources command in
17+
#[arg(long)]
18+
path: PathBuf,
19+
20+
/// Shell command that outputs source globs/paths (e.g. "spago sources")
21+
#[arg(long)]
22+
sources_cmd: String,
23+
24+
/// Directory for disk cache (enables warm-cache profiling across runs)
25+
#[arg(long)]
26+
cache_dir: Option<PathBuf>,
27+
}
28+
29+
macro_rules! phase {
30+
($name:expr, $body:expr) => {{
31+
let start = Instant::now();
32+
let result = $body;
33+
let elapsed = start.elapsed();
34+
eprintln!(" {:.<50} {:>8.2?}", $name, elapsed);
35+
result
36+
}};
37+
}
38+
39+
fn main() {
40+
let args = Args::parse();
41+
let total_start = Instant::now();
42+
43+
eprintln!("Profiling load_sources at: {}", args.path.display());
44+
eprintln!("Sources command: {}", args.sources_cmd);
45+
eprintln!();
46+
47+
// Phase 1: Run shell command
48+
let globs: Vec<String> = phase!("Run sources command", {
49+
let output = std::process::Command::new("sh")
50+
.arg("-c")
51+
.arg(&args.sources_cmd)
52+
.current_dir(&args.path)
53+
.output()
54+
.expect("Failed to run sources command");
55+
56+
if !output.status.success() {
57+
let stderr = String::from_utf8_lossy(&output.stderr);
58+
eprintln!("Command failed: {stderr}");
59+
std::process::exit(1);
60+
}
61+
62+
String::from_utf8_lossy(&output.stdout)
63+
.lines()
64+
.filter(|l| !l.is_empty())
65+
.map(|l| l.to_string())
66+
.collect()
67+
});
68+
eprintln!(" {} glob patterns", globs.len());
69+
70+
// Phase 2: Resolve globs
71+
let file_paths: Vec<PathBuf> = phase!("Resolve globs", {
72+
let mut paths = Vec::new();
73+
for pattern in &globs {
74+
// Resolve relative globs against the working directory
75+
let full_pattern = if PathBuf::from(pattern).is_relative() {
76+
args.path.join(pattern).to_string_lossy().into_owned()
77+
} else {
78+
pattern.clone()
79+
};
80+
match glob::glob(&full_pattern) {
81+
Ok(entries) => {
82+
for entry in entries.flatten() {
83+
if entry.extension().map_or(false, |ext| ext == "purs") {
84+
paths.push(entry);
85+
}
86+
}
87+
}
88+
Err(e) => eprintln!(" Invalid glob {pattern}: {e}"),
89+
}
90+
}
91+
paths
92+
});
93+
eprintln!(" {} .purs files", file_paths.len());
94+
95+
// Phase 3: Read all sources in parallel
96+
let sources: Vec<(String, String)> = phase!("Read sources (parallel)", {
97+
file_paths
98+
.par_iter()
99+
.filter_map(|entry| {
100+
let source = std::fs::read_to_string(entry).ok()?;
101+
let abs = entry.canonicalize().unwrap_or_else(|_| entry.clone());
102+
Some((abs.to_string_lossy().into_owned(), source))
103+
})
104+
.collect()
105+
});
106+
eprintln!(" {} files read", sources.len());
107+
108+
// Phase 4: Build with incremental cache
109+
let source_refs: Vec<(&str, &str)> = sources
110+
.iter()
111+
.map(|(p, s)| (p.as_str(), s.as_str()))
112+
.collect();
113+
114+
let options = BuildOptions {
115+
output_dir: None,
116+
..Default::default()
117+
};
118+
119+
let cache_dir = args.cache_dir.as_ref().map(|d| {
120+
if d.is_relative() {
121+
args.path.join(d)
122+
} else {
123+
d.clone()
124+
}
125+
});
126+
127+
let mut cache = if let Some(ref dir) = cache_dir {
128+
phase!("Load cache from disk", {
129+
match build::cache::ModuleCache::load_from_disk(dir) {
130+
Ok(c) => {
131+
eprintln!(" loaded cache from {}", dir.display());
132+
c
133+
}
134+
Err(_) => {
135+
eprintln!(" no existing cache, starting fresh");
136+
build::cache::ModuleCache::new()
137+
}
138+
}
139+
})
140+
} else {
141+
build::cache::ModuleCache::new()
142+
};
143+
144+
let (result, _registry, build_parsed_modules) = phase!("Build (incremental)", {
145+
build::build_from_sources_incremental(&source_refs, &None, None, &options, &mut cache)
146+
});
147+
148+
phase!("Build reverse deps", {
149+
cache.build_reverse_deps();
150+
});
151+
152+
if let Some(ref dir) = cache_dir {
153+
phase!("Save cache to disk", {
154+
if let Err(e) = cache.save_to_disk(dir) {
155+
eprintln!(" failed to save cache: {e}");
156+
}
157+
});
158+
}
159+
160+
let error_count: usize = result.modules.iter().map(|m| m.type_errors.len()).sum();
161+
let module_count = result.modules.len();
162+
let error_module_count = result.modules.iter().filter(|m| !m.type_errors.is_empty()).count();
163+
let cached_count = result.modules.iter().filter(|m| m.cached).count();
164+
eprintln!(
165+
" {} modules ({} cached, {} errors in {} modules)",
166+
module_count, cached_count, error_count, error_module_count
167+
);
168+
169+
// Phase 5: Parse cache-hit sources
170+
let already_parsed: HashSet<String> = build_parsed_modules
171+
.iter()
172+
.map(|(p, _)| p.to_string_lossy().into_owned())
173+
.collect();
174+
175+
let cache_hit_sources: Vec<_> = sources
176+
.iter()
177+
.filter(|(path, _)| !already_parsed.contains(path.as_str()))
178+
.collect();
179+
180+
let extra_count = cache_hit_sources.len();
181+
182+
let mut all_modules: Vec<(PathBuf, purescript_fast_compiler::CstModule)> = build_parsed_modules;
183+
184+
phase!(format!("Parse cache-hits ({extra_count} modules)"), {
185+
let extra: Vec<_> = cache_hit_sources
186+
.par_iter()
187+
.filter_map(|(path, source)| {
188+
purescript_fast_compiler::parse(source)
189+
.ok()
190+
.map(|m| (PathBuf::from(path.as_str()), m))
191+
})
192+
.collect();
193+
all_modules.extend(extra);
194+
});
195+
196+
// Phase 6: Build definition index
197+
let index = phase!(format!("Build definition index ({} modules)", all_modules.len()), {
198+
let mut index = DefinitionIndex::new();
199+
for (path, module) in &all_modules {
200+
index.add_module(module, &path.to_string_lossy());
201+
}
202+
index
203+
});
204+
205+
// Phase 7: Build ResolutionExports
206+
let exports = phase!("Build ResolutionExports", {
207+
let just_modules: Vec<purescript_fast_compiler::CstModule> =
208+
all_modules.into_iter().map(|(_, m)| m).collect();
209+
ResolutionExports::new(&just_modules)
210+
});
211+
212+
// Phase 8: Save LSP snapshots
213+
if let Some(ref dir) = cache_dir {
214+
let lsp_dir = dir.join("lsp");
215+
phase!("Save registry snapshot", {
216+
if let Err(e) = build::cache::save_registry_snapshot(&_registry, &lsp_dir.join("registry.bin")) {
217+
eprintln!(" failed: {e}");
218+
}
219+
});
220+
phase!("Save def_index snapshot", {
221+
if let Err(e) = index.save_to_disk(&lsp_dir.join("def_index.bin")) {
222+
eprintln!(" failed: {e}");
223+
}
224+
});
225+
phase!("Save resolution_exports snapshot", {
226+
if let Err(e) = exports.save_to_disk(&lsp_dir.join("resolution_exports.bin")) {
227+
eprintln!(" failed: {e}");
228+
}
229+
});
230+
231+
// Phase 9: Load LSP snapshots (benchmark restore time)
232+
eprintln!();
233+
eprintln!(" --- Restore from cache (simulated warm startup) ---");
234+
phase!("Load registry snapshot", {
235+
match build::cache::load_registry_snapshot(&lsp_dir.join("registry.bin")) {
236+
Ok(_) => {},
237+
Err(e) => eprintln!(" failed: {e}"),
238+
}
239+
});
240+
phase!("Load def_index snapshot", {
241+
match DefinitionIndex::load_from_disk(&lsp_dir.join("def_index.bin")) {
242+
Ok(_) => {},
243+
Err(e) => eprintln!(" failed: {e}"),
244+
}
245+
});
246+
phase!("Load resolution_exports snapshot", {
247+
match ResolutionExports::load_from_disk(&lsp_dir.join("resolution_exports.bin")) {
248+
Ok(_) => {},
249+
Err(e) => eprintln!(" failed: {e}"),
250+
}
251+
});
252+
phase!("Load cache index", {
253+
match build::cache::ModuleCache::load_from_disk(dir) {
254+
Ok(_) => {},
255+
Err(e) => eprintln!(" failed: {e}"),
256+
}
257+
});
258+
}
259+
260+
eprintln!();
261+
eprintln!(" {:.<50} {:>8.2?}", "TOTAL", total_start.elapsed());
262+
}

0 commit comments

Comments
 (0)