Skip to content

Commit de32935

Browse files
authored
feat: profile CLI command (ribru17#136)
1 parent 051e22e commit de32935

4 files changed

Lines changed: 153 additions & 2 deletions

File tree

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,24 @@ ts_query_ls lint ./queries
245245
ts_query_ls lint --help
246246
```
247247

248+
### Profiler
249+
250+
The server can be used to profile individual query patterns to check for
251+
patterns which are very slow to compile (often because they are too complex).
252+
This can be done via the `profile` subcommand, which prints each pattern's file
253+
path, start line, and the time (in milliseconds) that it took to compile.
254+
Alternatively, it can also time the entire query file itself (rather than each
255+
pattern inside of it).
256+
257+
```sh
258+
ts_query_ls profile ./queries
259+
# Use this command for the full documentation
260+
ts_query_ls profile --help
261+
```
262+
263+
> **NOTE:** This command will not warm up the cache for you, so it may be best
264+
> to run more than once.
265+
248266
## Checklist
249267

250268
- [x] References for captures
@@ -266,7 +284,7 @@ ts_query_ls lint --help
266284
query file. This should either be implemented as a user command, or core
267285
methods should be exposed to gather pattern information more efficiently~~
268286
- For now, this has been made possible due to caching and spawning query scans
269-
on a separate, blocking thread. Ideally in the future the kinks of query
287+
on a separate, blocking thread. Ideally, in the future, the kinks of query
270288
creation will be ironed out so query creation will be quicker, and this
271289
logic can be simplified
272290
- [x] Recognize parsers built for `WASM`

src/cli/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use walkdir::WalkDir;
55
pub(super) mod check;
66
pub(super) mod format;
77
pub(super) mod lint;
8+
pub(super) mod profile;
89

910
pub(in crate::cli) fn get_scm_files(directories: &[PathBuf]) -> Vec<PathBuf> {
1011
directories

src/cli/profile.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use std::{
2+
env, fs,
3+
path::PathBuf,
4+
sync::{Arc, LazyLock},
5+
time::Instant,
6+
};
7+
8+
use dashmap::DashMap;
9+
use futures::future::join_all;
10+
use tower_lsp::lsp_types::Url;
11+
use tree_sitter::{Parser, Query, QueryCursor, StreamingIterator as _};
12+
use ts_query_ls::Options;
13+
14+
use crate::{LanguageData, QUERY_LANGUAGE, handlers::did_open::init_language_data, util};
15+
16+
use super::get_scm_files;
17+
18+
static LANGUAGE_CACHE: LazyLock<DashMap<String, Arc<LanguageData>>> = LazyLock::new(DashMap::new);
19+
20+
static PATTERN_DEFINITION_QUERY: LazyLock<Query> =
21+
LazyLock::new(|| Query::new(&QUERY_LANGUAGE, "(program (definition) @def)").unwrap());
22+
23+
pub async fn profile_directories(directories: &[PathBuf], config: String, broad: bool) {
24+
let Ok(options) = serde_json::from_str::<Options>(&config) else {
25+
eprintln!("Could not parse the provided configuration");
26+
return;
27+
};
28+
let scm_files = if directories.is_empty() {
29+
get_scm_files(&[env::current_dir().expect("Failed to get current directory")])
30+
} else {
31+
get_scm_files(directories)
32+
};
33+
let tasks = scm_files.into_iter().filter_map(|path| {
34+
let uri = Url::from_file_path(path.canonicalize().unwrap()).unwrap();
35+
let path_str = path.to_string_lossy().to_string();
36+
let language_name = util::get_language_name(&uri, &options);
37+
let language_data = language_name.and_then(|name| {
38+
LANGUAGE_CACHE.get(&name).as_deref().cloned().or_else(|| {
39+
util::get_language(&name, &options).map(|lang| Arc::new(init_language_data(lang)))
40+
})
41+
});
42+
if let Some(lang_data) = language_data {
43+
let lang = lang_data.language.clone().unwrap();
44+
if let Ok(source) = fs::read_to_string(&path) {
45+
Some(tokio::spawn(async move {
46+
let mut results = Vec::new();
47+
if broad {
48+
let now = Instant::now();
49+
let _ = Query::new(&lang, &source);
50+
results.push((path_str.clone(), 1, now.elapsed().as_millis()));
51+
} else {
52+
let mut parser = Parser::new();
53+
parser.set_language(&QUERY_LANGUAGE).unwrap();
54+
let tree = parser.parse(&source, None).expect("Tree should exist");
55+
let mut cursor = QueryCursor::new();
56+
let source_bytes = source.as_bytes();
57+
let mut matches = cursor.matches(
58+
&PATTERN_DEFINITION_QUERY,
59+
tree.root_node(),
60+
source_bytes,
61+
);
62+
while let Some(match_) = matches.next() {
63+
for capture in match_.captures {
64+
let now = Instant::now();
65+
let _ = Query::new(
66+
&lang,
67+
capture
68+
.node
69+
.utf8_text(source_bytes)
70+
.expect("Source should be UTF-8"),
71+
);
72+
results.push((
73+
path_str.clone(),
74+
capture.node.start_position().row + 1,
75+
now.elapsed().as_millis(),
76+
));
77+
}
78+
}
79+
}
80+
results
81+
}))
82+
} else {
83+
eprintln!("Failed to read {:?}", path.canonicalize().unwrap());
84+
None
85+
}
86+
} else {
87+
eprintln!(
88+
"Could not retrieve language for {:?}",
89+
path.canonicalize().unwrap()
90+
);
91+
None
92+
}
93+
});
94+
let results = join_all(tasks).await;
95+
let mut results = results
96+
.into_iter()
97+
.flat_map(|r| r.unwrap_or_default())
98+
.collect::<Vec<_>>();
99+
results.sort_unstable_by(|a, b| a.2.cmp(&b.2));
100+
for (path, row, time) in results {
101+
if broad {
102+
println!("Query at {path} took {time}ms to compile");
103+
} else {
104+
println!("Pattern in {path} at line {row} took {time}ms to compile");
105+
}
106+
}
107+
}

src/main.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use clap::{Parser, Subcommand};
2-
use cli::{check::check_directories, format::format_directories, lint::lint_directories};
2+
use cli::{
3+
check::check_directories, format::format_directories, lint::lint_directories,
4+
profile::profile_directories,
5+
};
36
use core::fmt;
47
use std::{
58
collections::{BTreeSet, HashMap, HashSet},
@@ -267,6 +270,19 @@ enum Commands {
267270
#[arg(long, short)]
268271
fix: bool,
269272
},
273+
/// Profile each pattern in the given queries, outputting the time it takes them to compile.
274+
Profile {
275+
/// List of directories to profile
276+
directories: Vec<PathBuf>,
277+
278+
/// String representing server's JSON configuration
279+
#[arg(long, short)]
280+
config: Option<String>,
281+
282+
/// Whether to broadly profile the entire query, rather than each pattern within the query.
283+
#[arg(long, short)]
284+
broad: bool,
285+
},
270286
}
271287

272288
/// Return the given config string, or read it from a config file if not given. This function can
@@ -308,6 +324,15 @@ async fn main() {
308324
let config_str = get_config_str(config);
309325
std::process::exit(lint_directories(&directories, config_str, fix).await)
310326
}
327+
Some(Commands::Profile {
328+
directories,
329+
broad,
330+
config,
331+
}) => {
332+
let config_str = get_config_str(config);
333+
profile_directories(&directories, config_str, broad).await;
334+
std::process::exit(0);
335+
}
311336
None => {}
312337
}
313338

0 commit comments

Comments
 (0)