rubydex 0.2.6-aarch64-linux → 0.2.8-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/THIRD_PARTY_LICENSES.html +39 -6
- data/exe/rdx +2 -0
- data/ext/rubydex/declaration.c +115 -106
- data/ext/rubydex/definition.c +100 -80
- data/ext/rubydex/diagnostic.c +5 -0
- data/ext/rubydex/document.c +23 -31
- data/ext/rubydex/extconf.rb +1 -1
- data/ext/rubydex/graph.c +257 -70
- data/ext/rubydex/handle.h +13 -0
- data/ext/rubydex/location.c +5 -0
- data/ext/rubydex/reference.c +51 -46
- data/ext/rubydex/rubydex.c +5 -0
- data/ext/rubydex/signature.c +5 -0
- data/ext/rubydex/utils.c +13 -0
- data/ext/rubydex/utils.h +4 -0
- data/lib/rubydex/3.2/rubydex.so +0 -0
- data/lib/rubydex/3.3/rubydex.so +0 -0
- data/lib/rubydex/3.4/rubydex.so +0 -0
- data/lib/rubydex/4.0/rubydex.so +0 -0
- data/lib/rubydex/bin/rubydex_mcp +0 -0
- data/lib/rubydex/errors.rb +3 -0
- data/lib/rubydex/graph.rb +9 -28
- data/lib/rubydex/librubydex_sys.so +0 -0
- data/lib/rubydex/version.rb +1 -1
- data/rbi/rubydex.rbi +15 -7
- data/rust/Cargo.lock +119 -14
- data/rust/rubydex/Cargo.toml +19 -1
- data/rust/rubydex/benches/graph_memory.rs +46 -0
- data/rust/rubydex/src/config.rs +275 -0
- data/rust/rubydex/src/errors.rs +2 -0
- data/rust/rubydex/src/lib.rs +7 -0
- data/rust/rubydex/src/main.rs +148 -5
- data/rust/rubydex/src/model/declaration.rs +16 -55
- data/rust/rubydex/src/model/graph.rs +123 -17
- data/rust/rubydex/src/model/ids.rs +30 -0
- data/rust/rubydex/src/model/string_ref.rs +12 -4
- data/rust/rubydex/src/query.rs +83 -15
- data/rust/rubydex/src/resolution.rs +198 -151
- data/rust/rubydex/tests/cli.rs +66 -0
- data/rust/rubydex-mcp/src/server.rs +1 -1
- data/rust/rubydex-sys/src/declaration_api.rs +2 -2
- data/rust/rubydex-sys/src/graph_api.rs +127 -46
- metadata +4 -2
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#[cfg(all(feature = "jemalloc", not(target_os = "windows")))]
|
|
2
|
+
mod imp {
|
|
3
|
+
use std::collections::HashSet;
|
|
4
|
+
|
|
5
|
+
use rubydex::{
|
|
6
|
+
indexing::{self, IndexerBackend},
|
|
7
|
+
listing,
|
|
8
|
+
model::graph::Graph,
|
|
9
|
+
resolution::Resolver,
|
|
10
|
+
};
|
|
11
|
+
use tikv_jemalloc_ctl::{epoch, stats};
|
|
12
|
+
|
|
13
|
+
/// Advance the jemalloc epoch (stats are cached between epochs) and read the
|
|
14
|
+
/// number of bytes currently allocated by the application.
|
|
15
|
+
fn allocated_bytes() -> usize {
|
|
16
|
+
epoch::advance().expect("failed to advance jemalloc epoch");
|
|
17
|
+
stats::allocated::read().expect("failed to read stats.allocated (is the `stats` feature on?)")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn run() {
|
|
21
|
+
let paths: Vec<String> = std::env::args().skip(1).collect();
|
|
22
|
+
let paths = if paths.is_empty() { vec![".".to_string()] } else { paths };
|
|
23
|
+
let (file_paths, _) = listing::collect_file_paths(paths, &HashSet::new());
|
|
24
|
+
|
|
25
|
+
let mut graph = Graph::new();
|
|
26
|
+
let _ = indexing::index_files(&mut graph, file_paths, IndexerBackend::RubyIndexer);
|
|
27
|
+
Resolver::new(&mut graph).resolve();
|
|
28
|
+
|
|
29
|
+
// Compare the total memory used in the allocator before and after dropping the graph
|
|
30
|
+
let before_drop = allocated_bytes();
|
|
31
|
+
drop(graph);
|
|
32
|
+
let after_drop = allocated_bytes();
|
|
33
|
+
|
|
34
|
+
let total_graph_memory = before_drop.saturating_sub(after_drop);
|
|
35
|
+
|
|
36
|
+
#[allow(clippy::cast_precision_loss)]
|
|
37
|
+
let mega_bytes = total_graph_memory as f64 / 1024.0 / 1024.0;
|
|
38
|
+
|
|
39
|
+
println!("Total graph memory: {mega_bytes:.2} MB");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn main() {
|
|
44
|
+
#[cfg(all(feature = "jemalloc", not(target_os = "windows")))]
|
|
45
|
+
imp::run();
|
|
46
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
use crate::assert_mem_size;
|
|
2
|
+
use crate::errors::Errors;
|
|
3
|
+
use serde::Deserialize;
|
|
4
|
+
use std::collections::HashSet;
|
|
5
|
+
use std::fs;
|
|
6
|
+
use std::io::ErrorKind;
|
|
7
|
+
use std::path::{Path, PathBuf};
|
|
8
|
+
|
|
9
|
+
pub const DEFAULT_EXCLUDED_DIRECTORIES: &[&str] = &[
|
|
10
|
+
".bundle",
|
|
11
|
+
".claude",
|
|
12
|
+
".git",
|
|
13
|
+
".github",
|
|
14
|
+
".ruby-lsp",
|
|
15
|
+
".vscode",
|
|
16
|
+
"log",
|
|
17
|
+
"node_modules",
|
|
18
|
+
"tmp",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/// Configuration coming from a config file
|
|
22
|
+
#[derive(Debug, Default, Deserialize)]
|
|
23
|
+
struct ConfigFile {
|
|
24
|
+
/// Paths to exclude from file discovery during indexing.
|
|
25
|
+
#[serde(default)]
|
|
26
|
+
exclude: Vec<PathBuf>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Project configuration
|
|
30
|
+
#[derive(Debug)]
|
|
31
|
+
pub struct Config {
|
|
32
|
+
/// Path to the workspace being analyzed
|
|
33
|
+
workspace_path: Box<Path>,
|
|
34
|
+
/// Paths to exclude from file discovery during indexing.
|
|
35
|
+
excluded_paths: HashSet<PathBuf>,
|
|
36
|
+
}
|
|
37
|
+
assert_mem_size!(Config, 64);
|
|
38
|
+
|
|
39
|
+
impl Default for Config {
|
|
40
|
+
fn default() -> Self {
|
|
41
|
+
Self::new()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
impl Config {
|
|
46
|
+
/// Creates a configuration whose workspace path defaults to the current working directory.
|
|
47
|
+
#[must_use]
|
|
48
|
+
pub fn new() -> Self {
|
|
49
|
+
Self {
|
|
50
|
+
workspace_path: std::env::current_dir()
|
|
51
|
+
.unwrap_or_else(|_| PathBuf::from("."))
|
|
52
|
+
.into_boxed_path(),
|
|
53
|
+
excluded_paths: DEFAULT_EXCLUDED_DIRECTORIES.iter().map(PathBuf::from).collect(),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Returns the root directory of the workspace being analyzed.
|
|
58
|
+
#[must_use]
|
|
59
|
+
pub fn workspace_path(&self) -> &Path {
|
|
60
|
+
&self.workspace_path
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Sets the root directory of the workspace being analyzed.
|
|
64
|
+
pub fn set_workspace_path(&mut self, workspace_path: PathBuf) {
|
|
65
|
+
self.workspace_path = workspace_path.into_boxed_path();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Adds paths to exclude from file discovery during indexing. Excluded directories will be skipped entirely during
|
|
69
|
+
/// directory traversal.
|
|
70
|
+
pub fn exclude_paths(&mut self, paths: impl IntoIterator<Item = PathBuf>) {
|
|
71
|
+
self.excluded_paths.extend(paths);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Returns the set of paths excluded from file discovery
|
|
75
|
+
#[must_use]
|
|
76
|
+
pub fn excluded_paths(&self) -> HashSet<PathBuf> {
|
|
77
|
+
self.excluded_paths
|
|
78
|
+
.iter()
|
|
79
|
+
.map(|path| self.workspace_path.join(path))
|
|
80
|
+
.collect()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Merges the default `rubydex.toml` configuration file from the workspace root into this config, if present.
|
|
84
|
+
///
|
|
85
|
+
/// The default config file is optional, so a missing `rubydex.toml` is silently ignored. Any other failure (an
|
|
86
|
+
/// unreadable or malformed file) is still reported.
|
|
87
|
+
///
|
|
88
|
+
/// # Errors
|
|
89
|
+
///
|
|
90
|
+
/// Will error if the config file exists but cannot be read or has invalid syntax.
|
|
91
|
+
pub fn load_default(&mut self) -> Result<(), Errors> {
|
|
92
|
+
let config_path = self.workspace_path.join("rubydex.toml");
|
|
93
|
+
|
|
94
|
+
match self.load_file(&config_path) {
|
|
95
|
+
Err(Errors::ConfigNotFound(_)) => Ok(()),
|
|
96
|
+
other => other,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Merges the configuration at `config_path` into this config
|
|
101
|
+
///
|
|
102
|
+
/// # Errors
|
|
103
|
+
///
|
|
104
|
+
/// Returns [`Errors::ConfigNotFound`] if the file does not exist or [`Errors::ConfigError`] if it cannot otherwise
|
|
105
|
+
/// be read or has invalid syntax.
|
|
106
|
+
pub fn load_file(&mut self, config_path: &Path) -> Result<(), Errors> {
|
|
107
|
+
let content = match fs::read_to_string(config_path) {
|
|
108
|
+
Ok(content) => content,
|
|
109
|
+
Err(error) if error.kind() == ErrorKind::NotFound => {
|
|
110
|
+
return Err(Errors::ConfigNotFound(format!(
|
|
111
|
+
"Config file `{}` does not exist",
|
|
112
|
+
config_path.display()
|
|
113
|
+
)));
|
|
114
|
+
}
|
|
115
|
+
Err(error) => {
|
|
116
|
+
return Err(Errors::ConfigError(format!(
|
|
117
|
+
"Failed to read config file `{}`: {error}",
|
|
118
|
+
config_path.display()
|
|
119
|
+
)));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let parsed = Self::parse(&content).map_err(|error| {
|
|
124
|
+
Errors::ConfigError(format!("Invalid config file `{}`: {error}", config_path.display()))
|
|
125
|
+
})?;
|
|
126
|
+
|
|
127
|
+
self.excluded_paths.extend(parsed.exclude);
|
|
128
|
+
Ok(())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Parses the content into a [`ConfigFile`]
|
|
132
|
+
fn parse(content: &str) -> Result<ConfigFile, toml::de::Error> {
|
|
133
|
+
toml::from_str(content)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[cfg(test)]
|
|
138
|
+
mod tests {
|
|
139
|
+
use super::*;
|
|
140
|
+
use std::path::Path;
|
|
141
|
+
|
|
142
|
+
#[test]
|
|
143
|
+
fn excluded_paths_are_resolved_against_the_workspace_path() {
|
|
144
|
+
let mut config = Config::new();
|
|
145
|
+
config.set_workspace_path(PathBuf::from("/workspace"));
|
|
146
|
+
config.exclude_paths([PathBuf::from("vendor"), PathBuf::from("/absolute/path")]);
|
|
147
|
+
|
|
148
|
+
let excluded = config.excluded_paths();
|
|
149
|
+
|
|
150
|
+
// Relative entries (including the defaults) are joined with the workspace path.
|
|
151
|
+
assert!(excluded.contains(Path::new("/workspace/vendor")));
|
|
152
|
+
assert!(excluded.contains(Path::new("/workspace/.git")));
|
|
153
|
+
// Absolute entries pass through unchanged.
|
|
154
|
+
assert!(excluded.contains(Path::new("/absolute/path")));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[test]
|
|
158
|
+
fn new_seeds_the_default_excluded_directories() {
|
|
159
|
+
let config = Config::new();
|
|
160
|
+
|
|
161
|
+
for default in DEFAULT_EXCLUDED_DIRECTORIES {
|
|
162
|
+
assert!(
|
|
163
|
+
config.excluded_paths.contains(Path::new(default)),
|
|
164
|
+
"expected `{default}` to be excluded by default"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[test]
|
|
170
|
+
fn load_file_merges_excluded_paths_and_leaves_the_workspace_path_untouched() {
|
|
171
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
172
|
+
let config_path = dir.path().join("rubydex.toml");
|
|
173
|
+
fs::write(&config_path, "exclude = [\"vendor\", \"generated\"]\n").unwrap();
|
|
174
|
+
|
|
175
|
+
let mut config = Config::new();
|
|
176
|
+
config.set_workspace_path(PathBuf::from("/workspace"));
|
|
177
|
+
|
|
178
|
+
config
|
|
179
|
+
.load_file(&config_path)
|
|
180
|
+
.expect("expected the config file to load");
|
|
181
|
+
|
|
182
|
+
let excluded = config.excluded_paths();
|
|
183
|
+
// Entries from the file are merged in and resolved against the workspace path.
|
|
184
|
+
assert!(excluded.contains(Path::new("/workspace/vendor")));
|
|
185
|
+
assert!(excluded.contains(Path::new("/workspace/generated")));
|
|
186
|
+
// Defaults seeded at construction survive the merge.
|
|
187
|
+
assert!(excluded.contains(Path::new("/workspace/node_modules")));
|
|
188
|
+
// A config file cannot override the programmatically-set workspace path.
|
|
189
|
+
assert_eq!(config.workspace_path(), Path::new("/workspace"));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[test]
|
|
193
|
+
fn load_file_accumulates_exclusions_across_multiple_loads() {
|
|
194
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
195
|
+
fs::write(dir.path().join("a.toml"), "exclude = [\"vendor\"]\n").unwrap();
|
|
196
|
+
fs::write(dir.path().join("b.toml"), "exclude = [\"generated\"]\n").unwrap();
|
|
197
|
+
|
|
198
|
+
let mut config = Config::new();
|
|
199
|
+
config.set_workspace_path(PathBuf::from("/workspace"));
|
|
200
|
+
config
|
|
201
|
+
.load_file(&dir.path().join("a.toml"))
|
|
202
|
+
.expect("expected the first file to load");
|
|
203
|
+
config
|
|
204
|
+
.load_file(&dir.path().join("b.toml"))
|
|
205
|
+
.expect("expected the second file to load");
|
|
206
|
+
|
|
207
|
+
let excluded = config.excluded_paths();
|
|
208
|
+
assert!(excluded.contains(Path::new("/workspace/vendor")));
|
|
209
|
+
assert!(excluded.contains(Path::new("/workspace/generated")));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#[test]
|
|
213
|
+
fn load_file_errors_when_the_file_is_missing() {
|
|
214
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
215
|
+
let mut config = Config::new();
|
|
216
|
+
|
|
217
|
+
let error = config
|
|
218
|
+
.load_file(&dir.path().join("does_not_exist.toml"))
|
|
219
|
+
.expect_err("an explicitly requested missing file must be an error");
|
|
220
|
+
|
|
221
|
+
assert!(
|
|
222
|
+
matches!(error, Errors::ConfigNotFound(_)),
|
|
223
|
+
"unexpected error: {error:?}"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn load_default_ignores_a_missing_config_file() {
|
|
229
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
230
|
+
let mut config = Config::new();
|
|
231
|
+
config.set_workspace_path(dir.path().to_path_buf());
|
|
232
|
+
|
|
233
|
+
config
|
|
234
|
+
.load_default()
|
|
235
|
+
.expect("a missing rubydex.toml must not be an error");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[test]
|
|
239
|
+
fn load_default_loads_an_existing_config_file() {
|
|
240
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
241
|
+
fs::write(dir.path().join("rubydex.toml"), "exclude = [\"vendor\"]\n").unwrap();
|
|
242
|
+
|
|
243
|
+
let mut config = Config::new();
|
|
244
|
+
config.set_workspace_path(dir.path().to_path_buf());
|
|
245
|
+
config.load_default().expect("expected rubydex.toml to load");
|
|
246
|
+
|
|
247
|
+
assert!(config.excluded_paths().contains(&dir.path().join("vendor")));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[test]
|
|
251
|
+
fn load_default_propagates_malformed_config_errors() {
|
|
252
|
+
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
253
|
+
fs::write(dir.path().join("rubydex.toml"), "exclude = [\n").unwrap();
|
|
254
|
+
|
|
255
|
+
let mut config = Config::new();
|
|
256
|
+
config.set_workspace_path(dir.path().to_path_buf());
|
|
257
|
+
|
|
258
|
+
let error = config
|
|
259
|
+
.load_default()
|
|
260
|
+
.expect_err("a malformed default config must still be an error");
|
|
261
|
+
|
|
262
|
+
assert!(matches!(error, Errors::ConfigError(_)), "unexpected error: {error:?}");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn parse_defaults_the_excluded_paths_to_empty_when_the_key_is_absent() {
|
|
267
|
+
let file = Config::parse("").expect("an empty config is valid");
|
|
268
|
+
assert!(file.exclude.is_empty());
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn parse_rejects_an_exclude_value_of_the_wrong_type() {
|
|
273
|
+
Config::parse("exclude = \"vendor\"").expect_err("exclude must be an array of strings, not a string");
|
|
274
|
+
}
|
|
275
|
+
}
|
data/rust/rubydex/src/errors.rs
CHANGED
data/rust/rubydex/src/lib.rs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
// Setting the global allocator needs to happen during linking and consumers may not always want to change the global
|
|
2
|
+
// allocator. We gate the usage of jemalloc with a cargo feature, so that consumers can decide if they want to use it
|
|
3
|
+
#[cfg(all(feature = "jemalloc", not(target_os = "windows")))]
|
|
4
|
+
#[global_allocator]
|
|
5
|
+
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
|
6
|
+
|
|
1
7
|
pub mod compile_assertions;
|
|
8
|
+
pub mod config;
|
|
2
9
|
pub mod diagnostic;
|
|
3
10
|
pub mod dot;
|
|
4
11
|
pub mod errors;
|
data/rust/rubydex/src/main.rs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
use clap::{Parser, ValueEnum};
|
|
2
|
-
use std::{
|
|
2
|
+
use std::{
|
|
3
|
+
fs, mem,
|
|
4
|
+
path::{Path, PathBuf},
|
|
5
|
+
time::{Duration, Instant},
|
|
6
|
+
};
|
|
3
7
|
|
|
4
8
|
use rubydex::{
|
|
5
9
|
dot,
|
|
6
|
-
indexing::{self, IndexerBackend},
|
|
10
|
+
indexing::{self, IndexerBackend, LanguageId, build_local_graph},
|
|
7
11
|
integrity, listing,
|
|
8
12
|
model::graph::Graph,
|
|
9
13
|
resolution::Resolver,
|
|
@@ -12,12 +16,17 @@ use rubydex::{
|
|
|
12
16
|
timer::{Timer, time_it},
|
|
13
17
|
},
|
|
14
18
|
};
|
|
19
|
+
use url::Url;
|
|
15
20
|
|
|
16
21
|
#[derive(Parser, Debug)]
|
|
17
22
|
#[command(name = "rubydex_cli", about = "A Static Analysis Toolkit for Ruby", version)]
|
|
18
23
|
#[allow(clippy::struct_excessive_bools)]
|
|
19
24
|
struct Args {
|
|
20
|
-
#[arg(
|
|
25
|
+
#[arg(
|
|
26
|
+
value_name = "PATHS",
|
|
27
|
+
default_value = ".",
|
|
28
|
+
help = "Path(s) to index. If the first path is a directory, it is used as the workspace root for rubydex.toml"
|
|
29
|
+
)]
|
|
21
30
|
paths: Vec<String>,
|
|
22
31
|
|
|
23
32
|
#[arg(long = "stop-after", help = "Stop after the given stage")]
|
|
@@ -52,6 +61,21 @@ struct Args {
|
|
|
52
61
|
help = "Write orphan definitions report to specified file"
|
|
53
62
|
)]
|
|
54
63
|
report_orphans: Option<String>,
|
|
64
|
+
|
|
65
|
+
#[arg(
|
|
66
|
+
long = "incremental_cycle",
|
|
67
|
+
value_name = "N",
|
|
68
|
+
help = "After the initial build, run N incremental resolution cycles and report their timings"
|
|
69
|
+
)]
|
|
70
|
+
incremental_cycle: Option<usize>,
|
|
71
|
+
|
|
72
|
+
#[arg(
|
|
73
|
+
long = "incremental_files",
|
|
74
|
+
value_name = "N",
|
|
75
|
+
default_value_t = 1,
|
|
76
|
+
help = "Number of files to re-index per incremental cycle"
|
|
77
|
+
)]
|
|
78
|
+
incremental_files: usize,
|
|
55
79
|
}
|
|
56
80
|
|
|
57
81
|
#[derive(Debug, Clone, ValueEnum)]
|
|
@@ -85,6 +109,11 @@ fn exit(print_stats: bool) {
|
|
|
85
109
|
std::process::exit(0);
|
|
86
110
|
}
|
|
87
111
|
|
|
112
|
+
fn workspace_path_for(paths: &[String]) -> Option<PathBuf> {
|
|
113
|
+
let first_path = paths.first()?;
|
|
114
|
+
fs::canonicalize(first_path).ok().filter(|path| path.is_dir())
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
fn main() {
|
|
89
118
|
let args = Args::parse();
|
|
90
119
|
|
|
@@ -92,9 +121,21 @@ fn main() {
|
|
|
92
121
|
Timer::set_global_timer(Timer::new());
|
|
93
122
|
}
|
|
94
123
|
|
|
124
|
+
let mut graph = Graph::new();
|
|
125
|
+
|
|
126
|
+
if let Some(workspace_path) = workspace_path_for(&args.paths) {
|
|
127
|
+
graph.set_workspace_path(workspace_path);
|
|
128
|
+
if let Err(error) = graph.load_config(None) {
|
|
129
|
+
eprintln!("{error}");
|
|
130
|
+
std::process::exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
95
134
|
// Listing
|
|
96
135
|
|
|
97
|
-
let (file_paths, errors) = time_it!(listing, {
|
|
136
|
+
let (file_paths, errors) = time_it!(listing, {
|
|
137
|
+
listing::collect_file_paths(args.paths, &graph.excluded_paths())
|
|
138
|
+
});
|
|
98
139
|
|
|
99
140
|
for error in errors {
|
|
100
141
|
eprintln!("{error}");
|
|
@@ -106,8 +147,16 @@ fn main() {
|
|
|
106
147
|
|
|
107
148
|
// Indexing
|
|
108
149
|
|
|
109
|
-
let mut graph = Graph::new();
|
|
110
150
|
let backend = IndexerBackend::from(&args.indexer);
|
|
151
|
+
|
|
152
|
+
// The incremental benchmark re-indexes files after the initial build, so keep a copy of the
|
|
153
|
+
// paths before `index_files` consumes them.
|
|
154
|
+
let incremental_paths = if args.incremental_cycle.is_some() {
|
|
155
|
+
file_paths.clone()
|
|
156
|
+
} else {
|
|
157
|
+
Vec::new()
|
|
158
|
+
};
|
|
159
|
+
|
|
111
160
|
let errors = time_it!(indexing, { indexing::index_files(&mut graph, file_paths, backend) });
|
|
112
161
|
|
|
113
162
|
for error in errors {
|
|
@@ -125,6 +174,12 @@ fn main() {
|
|
|
125
174
|
resolver.resolve();
|
|
126
175
|
});
|
|
127
176
|
|
|
177
|
+
// Incremental resolution benchmark. Runs before the stop-after check so it can be combined with
|
|
178
|
+
// `--stop-after=resolution`.
|
|
179
|
+
if let Some(cycles) = args.incremental_cycle {
|
|
180
|
+
run_incremental_resolution(&mut graph, &incremental_paths, cycles, args.incremental_files, backend);
|
|
181
|
+
}
|
|
182
|
+
|
|
128
183
|
if let Some(StopAfter::Resolution) = args.stop_after {
|
|
129
184
|
return exit(args.stats);
|
|
130
185
|
}
|
|
@@ -186,3 +241,91 @@ fn main() {
|
|
|
186
241
|
// Forget the graph so we don't have to wait for deallocation and let the system reclaim the memory at exit
|
|
187
242
|
mem::forget(graph);
|
|
188
243
|
}
|
|
244
|
+
|
|
245
|
+
/// Simulates incremental editing to measure incremental resolution cost. For each cycle it
|
|
246
|
+
/// re-indexes a rotating window of `files_per_cycle` files (parsing plus the same invalidation the
|
|
247
|
+
/// LSP performs on save) and then re-runs resolution over the resulting pending work, reporting
|
|
248
|
+
/// per-cycle and aggregate `resolve()` timings.
|
|
249
|
+
///
|
|
250
|
+
/// With `--stats`, the `compute_descendants` breakdown printed in the timing summary reflects the
|
|
251
|
+
/// last incremental cycle, since it is recorded on every `resolve()`.
|
|
252
|
+
fn run_incremental_resolution(
|
|
253
|
+
graph: &mut Graph,
|
|
254
|
+
paths: &[PathBuf],
|
|
255
|
+
cycles: usize,
|
|
256
|
+
files_per_cycle: usize,
|
|
257
|
+
backend: IndexerBackend,
|
|
258
|
+
) {
|
|
259
|
+
if paths.is_empty() || cycles == 0 || files_per_cycle == 0 {
|
|
260
|
+
eprintln!("Skipping incremental resolution: nothing to re-index");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let files_per_cycle = files_per_cycle.min(paths.len());
|
|
265
|
+
|
|
266
|
+
let mut reindex_total = Duration::ZERO;
|
|
267
|
+
let mut resolve_total = Duration::ZERO;
|
|
268
|
+
let mut resolve_min = Duration::MAX;
|
|
269
|
+
let mut resolve_max = Duration::ZERO;
|
|
270
|
+
|
|
271
|
+
println!();
|
|
272
|
+
println!("Incremental resolution ({cycles} cycle(s), {files_per_cycle} file(s)/cycle)");
|
|
273
|
+
println!(" Scenario: no-op reindex; files are indexed again without changing their contents.");
|
|
274
|
+
println!(" This should be the fastest incremental resolution path.");
|
|
275
|
+
println!(" Add scenario-based benchmarks before optimizing incremental resolution.");
|
|
276
|
+
|
|
277
|
+
for cycle in 0..cycles {
|
|
278
|
+
// Re-index the window of files for this cycle. Rotating the window across cycles samples
|
|
279
|
+
// different parts of the codebase rather than measuring the same delta repeatedly.
|
|
280
|
+
let reindex_start = Instant::now();
|
|
281
|
+
for i in 0..files_per_cycle {
|
|
282
|
+
let path = &paths[(cycle * files_per_cycle + i) % paths.len()];
|
|
283
|
+
reindex_file(graph, path, backend);
|
|
284
|
+
}
|
|
285
|
+
reindex_total += reindex_start.elapsed();
|
|
286
|
+
|
|
287
|
+
let resolve_start = Instant::now();
|
|
288
|
+
Resolver::new(graph).resolve();
|
|
289
|
+
let elapsed = resolve_start.elapsed();
|
|
290
|
+
|
|
291
|
+
resolve_total += elapsed;
|
|
292
|
+
resolve_min = resolve_min.min(elapsed);
|
|
293
|
+
resolve_max = resolve_max.max(elapsed);
|
|
294
|
+
|
|
295
|
+
println!(
|
|
296
|
+
" cycle {:>3}: resolve {:9.3}ms",
|
|
297
|
+
cycle + 1,
|
|
298
|
+
elapsed.as_secs_f64() * 1000.0
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let avg = resolve_total / u32::try_from(cycles).expect("cycle count fits in u32");
|
|
303
|
+
|
|
304
|
+
println!(
|
|
305
|
+
" resolve total {:.3}ms avg {:.3}ms min {:.3}ms max {:.3}ms",
|
|
306
|
+
resolve_total.as_secs_f64() * 1000.0,
|
|
307
|
+
avg.as_secs_f64() * 1000.0,
|
|
308
|
+
resolve_min.as_secs_f64() * 1000.0,
|
|
309
|
+
resolve_max.as_secs_f64() * 1000.0,
|
|
310
|
+
);
|
|
311
|
+
println!(
|
|
312
|
+
" reindex+invalidate total {:.3}ms (parse + document merge, excluded from resolve above)",
|
|
313
|
+
reindex_total.as_secs_f64() * 1000.0,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Re-indexes a single file into the graph, running the same invalidation the LSP performs on save.
|
|
318
|
+
fn reindex_file(graph: &mut Graph, path: &Path, backend: IndexerBackend) {
|
|
319
|
+
let Ok(source) = fs::read_to_string(path) else {
|
|
320
|
+
eprintln!("Failed to read file `{}`", path.display());
|
|
321
|
+
return;
|
|
322
|
+
};
|
|
323
|
+
let Ok(url) = Url::from_file_path(path) else {
|
|
324
|
+
eprintln!("Couldn't build URI from path `{}`", path.display());
|
|
325
|
+
return;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
let language = path.extension().map_or(LanguageId::Ruby, LanguageId::from);
|
|
329
|
+
let local_graph = build_local_graph(url.to_string(), &source, &language, backend);
|
|
330
|
+
graph.consume_document_changes(local_graph);
|
|
331
|
+
}
|