Skip to content

Commit 9d86159

Browse files
alanzmeta-codesync[bot]
authored andcommitted
Improve the performance of elp ssr
Summary: The `elp ssr` feature was initially landed as a quick proof of concept. In paractice, it runs much slower than it should, because it is delegating to the machinery that runs `elp lint`, which does a lot more work than is needed. This diff makes `elp ssr` a completely standalone command, which only calls the specific diagnostic checker for an ad hoc SSR diagnostic. Reviewed By: jcpetruzza Differential Revision: D85760580 fbshipit-source-id: 5bd38f78b08614be99c4a7d4cc69ebed8832fc70
1 parent 1c886bd commit 9d86159

File tree

4 files changed

+260
-44
lines changed

4 files changed

+260
-44
lines changed

crates/elp/src/bin/args.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,16 @@ impl Lint {
783783
}
784784
}
785785

786+
impl Ssr {
787+
pub fn is_format_normal(&self) -> bool {
788+
self.format.is_none()
789+
}
790+
791+
pub fn is_format_json(&self) -> bool {
792+
self.format == Some("json".to_string())
793+
}
794+
}
795+
786796
impl ParseAllElp {
787797
pub fn is_format_normal(&self) -> bool {
788798
self.format.is_none()

crates/elp/src/bin/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2007,7 +2007,7 @@ mod tests {
20072007
simple_snapshot(
20082008
args_vec!["ssr", "ssr: {_@A, _@B}.",],
20092009
"linter",
2010-
expect_file!("../resources/test/linter/ssr_ad_hoc.stdout"),
2010+
expect_file!("../resources/test/linter/ssr_ad_hoc_cli.stdout"),
20112011
true,
20122012
None,
20132013
)
@@ -2018,7 +2018,7 @@ mod tests {
20182018
simple_snapshot(
20192019
args_vec!["ssr", "{_@A, _@B}",],
20202020
"linter",
2021-
expect_file!("../resources/test/linter/ssr_ad_hoc.stdout"),
2021+
expect_file!("../resources/test/linter/ssr_ad_hoc_cli.stdout"),
20222022
true,
20232023
None,
20242024
)

crates/elp/src/bin/ssr_cli.rs

Lines changed: 245 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,47 @@
88
* above-listed licenses.
99
*/
1010

11+
use std::fs;
12+
use std::path::Path;
13+
use std::str;
14+
use std::time::SystemTime;
15+
1116
use anyhow::Result;
1217
use anyhow::bail;
18+
use elp::build::load;
19+
use elp::build::types::LoadResult;
1320
use elp::cli::Cli;
21+
use elp::convert;
22+
use elp::memory_usage::MemoryUsage;
23+
use elp::otp_file_to_ignore;
24+
use elp_eqwalizer::Mode;
25+
use elp_ide::Analysis;
1426
use elp_ide::AnalysisHost;
1527
use elp_ide::diagnostics;
1628
use elp_ide::diagnostics::DiagnosticsConfig;
1729
use elp_ide::diagnostics::FallBackToAll;
1830
use elp_ide::diagnostics::LintConfig;
1931
use elp_ide::diagnostics::MatchSsr;
32+
use elp_ide::elp_ide_db::elp_base_db::AbsPath;
33+
use elp_ide::elp_ide_db::elp_base_db::FileId;
34+
use elp_ide::elp_ide_db::elp_base_db::IncludeOtp;
35+
use elp_ide::elp_ide_db::elp_base_db::ModuleName;
36+
use elp_ide::elp_ide_db::elp_base_db::ProjectId;
37+
use elp_ide::elp_ide_db::elp_base_db::VfsPath;
38+
use elp_log::telemetry;
39+
use elp_project_model::AppName;
40+
use elp_project_model::AppType;
41+
use elp_project_model::DiscoverConfig;
2042
use elp_project_model::buck::BuckQueryConfig;
43+
use hir::Semantic;
44+
use indicatif::ParallelProgressIterator;
45+
use paths::Utf8PathBuf;
46+
use rayon::prelude::ParallelBridge;
47+
use rayon::prelude::ParallelIterator;
2148

2249
use crate::args::Ssr;
23-
use crate::lint_cli;
50+
use crate::reporting;
51+
use crate::reporting::print_memory_usage;
2452

2553
fn normalize_ssr_pattern(pattern: &str) -> String {
2654
if pattern.starts_with("ssr:") {
@@ -35,6 +63,8 @@ pub fn run_ssr_command(
3563
cli: &mut dyn Cli,
3664
query_config: &BuckQueryConfig,
3765
) -> Result<()> {
66+
let start_time = SystemTime::now();
67+
let memory_start = MemoryUsage::now();
3868
// Normalize the SSR pattern
3969
let normalized_pattern = normalize_ssr_pattern(&args.ssr_spec);
4070

@@ -71,51 +101,224 @@ pub fn run_ssr_command(
71101
writeln!(cli, "Reporting all diagnostics codes")?;
72102
}
73103

74-
// Convert Ssr args to Lint args to reuse lint_cli functionality
75-
let lint_args = crate::args::Lint {
76-
project: args.project.clone(),
77-
module: args.module.clone(),
78-
app: args.app.clone(),
79-
file: args.file.clone(),
80-
rebar: args.rebar,
81-
profile: args.profile.clone(),
82-
include_generated: args.include_generated,
83-
include_tests: args.include_tests,
84-
print_diags: true,
85-
format: args.format.clone(),
86-
prefix: None,
87-
include_erlc_diagnostics: false,
88-
include_ct_diagnostics: false,
89-
include_edoc_diagnostics: false,
90-
include_eqwalizer_diagnostics: false,
91-
include_suppressed: false,
92-
use_cli_severity: false,
93-
diagnostic_ignore: None,
94-
diagnostic_filter: Some("ad-hoc: ssr-match".to_string()),
95-
experimental_diags: false,
96-
read_config: false,
97-
config_file: None,
98-
apply_fix: false,
99-
ignore_fix_only: false,
100-
in_place: false,
101-
to: None,
102-
recursive: false,
103-
with_check: false,
104-
check_eqwalize_all: false,
105-
one_shot: false,
106-
report_system_stats: args.report_system_stats,
107-
ignore_apps: vec![],
104+
// Load the project
105+
let mut loaded = load_project(args, cli, query_config)?;
106+
telemetry::report_elapsed_time("ssr operational", start_time);
107+
108+
let r = run_ssr(cli, &mut loaded, &diagnostics_config, args);
109+
110+
telemetry::report_elapsed_time("ssr done", start_time);
111+
112+
let memory_end = MemoryUsage::now();
113+
let memory_used = memory_end - memory_start;
114+
115+
// Print memory usage at the end if requested and format is normal
116+
if args.is_format_normal() && args.report_system_stats {
117+
print_memory_usage(loaded.analysis_host, loaded.vfs, cli)?;
118+
writeln!(cli, "{}", memory_used)?;
119+
}
120+
r
121+
}
122+
123+
pub fn run_ssr(
124+
cli: &mut dyn Cli,
125+
loaded: &mut LoadResult,
126+
diagnostics_config: &DiagnosticsConfig,
127+
args: &Ssr,
128+
) -> Result<()> {
129+
let analysis = loaded.analysis();
130+
let (file_id, name) = match &args.module {
131+
Some(module) => match analysis.module_file_id(loaded.project_id, module)? {
132+
Some(file_id) => {
133+
if args.is_format_normal() {
134+
writeln!(cli, "module specified: {module}")?;
135+
}
136+
(Some(file_id), analysis.module_name(file_id)?)
137+
}
138+
None => panic!("Module not found: {module}"),
139+
},
140+
None => match &args.file {
141+
Some(file_name) => {
142+
if args.is_format_normal() {
143+
writeln!(cli, "file specified: {file_name}")?;
144+
}
145+
let path_buf = Utf8PathBuf::from_path_buf(fs::canonicalize(file_name).unwrap())
146+
.expect("UTF8 conversion failed");
147+
let path = AbsPath::assert(&path_buf);
148+
let path = path.as_os_str().to_str().unwrap();
149+
(
150+
loaded
151+
.vfs
152+
.file_id(&VfsPath::new_real_path(path.to_string()))
153+
.map(|(id, _)| id),
154+
path_buf.as_path().file_name().map(ModuleName::new),
155+
)
156+
}
157+
None => (None, None),
158+
},
108159
};
109160

110-
// Load the project
111-
let mut loaded = lint_cli::load_project(&lint_args, cli, query_config)?;
161+
let mut diags = match (file_id, name) {
162+
(None, _) => do_parse_all(cli, &analysis, &loaded.project_id, diagnostics_config, args)?,
163+
(Some(file_id), Some(name)) => {
164+
if let Some(app) = &args.app
165+
&& let Ok(Some(file_app)) = analysis.file_app_name(file_id)
166+
&& file_app != AppName(app.to_string())
167+
{
168+
panic!("Module {} does not belong to app {}", name.as_str(), app)
169+
}
170+
do_parse_one(&analysis, diagnostics_config, file_id, &name, args)?
171+
.map_or(vec![], |x| vec![x])
172+
}
173+
(Some(file_id), _) => {
174+
panic!("Could not get name from file_id for {file_id:?}")
175+
}
176+
};
177+
if diags.is_empty() {
178+
if args.is_format_normal() {
179+
writeln!(cli, "No matches found")?;
180+
}
181+
} else {
182+
diags.sort_by(|(a, _, _), (b, _, _)| a.cmp(b));
183+
if args.is_format_json() {
184+
for (_name, file_id, diags) in &diags {
185+
for diag in diags {
186+
// We use JSON output for CI, and want to see warnings too.
187+
// So do not filter on errors only
188+
let vfs_path = loaded.vfs.file_path(*file_id);
189+
let analysis = loaded.analysis();
190+
let root_path = &analysis
191+
.project_data(*file_id)
192+
.unwrap_or_else(|_err| panic!("could not find project data"))
193+
.unwrap_or_else(|| panic!("could not find project data"))
194+
.root_dir;
195+
let relative_path = reporting::get_relative_path(root_path, vfs_path);
196+
print_diagnostic_json(diag, &analysis, *file_id, relative_path, false, cli)?;
197+
}
198+
}
199+
} else {
200+
writeln!(cli, "Matches found in {} modules:", diags.len())?;
201+
for (name, file_id, diags) in &diags {
202+
writeln!(cli, " {}: {}", name, diags.len())?;
203+
for diag in diags {
204+
print_diagnostic(diag, &loaded.analysis(), *file_id, false, cli)?;
205+
}
206+
}
207+
}
208+
}
209+
Ok(())
210+
}
211+
212+
fn load_project(
213+
args: &Ssr,
214+
cli: &mut dyn Cli,
215+
query_config: &BuckQueryConfig,
216+
) -> Result<LoadResult> {
217+
log::info!("Loading project at: {:?}", args.project);
218+
let config = DiscoverConfig::new(args.rebar, &args.profile);
219+
load::load_project_at(
220+
cli,
221+
&args.project,
222+
config,
223+
IncludeOtp::Yes,
224+
Mode::Server,
225+
query_config,
226+
)
227+
}
112228

113-
// Run the codemod with the SSR pattern
114-
lint_cli::do_codemod(cli, &mut loaded, &diagnostics_config, &lint_args)
229+
fn do_parse_all(
230+
cli: &dyn Cli,
231+
analysis: &Analysis,
232+
project_id: &ProjectId,
233+
config: &DiagnosticsConfig,
234+
args: &Ssr,
235+
) -> Result<Vec<(String, FileId, Vec<diagnostics::Diagnostic>)>> {
236+
let module_index = analysis.module_index(*project_id).unwrap();
237+
let module_iter = module_index.iter_own();
238+
239+
let pb = cli.progress(module_iter.len() as u64, "Scanning modules");
240+
let app_name = args.app.as_ref().map(|name| AppName(name.to_string()));
241+
242+
Ok(module_iter
243+
.par_bridge()
244+
.progress_with(pb)
245+
.map_with(
246+
analysis.clone(),
247+
|db, (module_name, _file_source, file_id)| {
248+
if !otp_file_to_ignore(db, file_id)
249+
&& db.file_app_type(file_id).ok() != Some(Some(AppType::Dep))
250+
&& (app_name.is_none()
251+
|| db.file_app_name(file_id).ok().as_ref() == Some(&app_name))
252+
{
253+
do_parse_one(db, config, file_id, module_name.as_str(), args).unwrap()
254+
} else {
255+
None
256+
}
257+
},
258+
)
259+
.flatten()
260+
.collect())
115261
}
116262

117-
impl Ssr {
118-
pub fn is_format_normal(&self) -> bool {
119-
self.format.is_none()
263+
fn do_parse_one(
264+
db: &Analysis,
265+
config: &DiagnosticsConfig,
266+
file_id: FileId,
267+
name: &str,
268+
args: &Ssr,
269+
) -> Result<Option<(String, FileId, Vec<diagnostics::Diagnostic>)>> {
270+
if !args.include_tests && db.is_test_suite_or_test_helper(file_id)?.unwrap_or(false) {
271+
return Ok(None);
272+
}
273+
274+
// Run only the SSR lint configured in lints_from_config
275+
let diagnostics = db.with_db(|database| {
276+
let sema = Semantic::new(database);
277+
let mut diags = Vec::new();
278+
config
279+
.lints_from_config
280+
.get_diagnostics(&mut diags, &sema, file_id);
281+
sema.parse(file_id);
282+
diags
283+
})?;
284+
285+
if !diagnostics.is_empty() {
286+
let res = (name.to_string(), file_id, diagnostics);
287+
Ok(Some(res))
288+
} else {
289+
Ok(None)
120290
}
121291
}
292+
293+
fn print_diagnostic(
294+
diag: &diagnostics::Diagnostic,
295+
analysis: &Analysis,
296+
file_id: FileId,
297+
use_cli_severity: bool,
298+
cli: &mut dyn Cli,
299+
) -> Result<(), anyhow::Error> {
300+
let line_index = analysis.line_index(file_id)?;
301+
writeln!(cli, " {}", diag.print(&line_index, use_cli_severity))?;
302+
Ok(())
303+
}
304+
305+
fn print_diagnostic_json(
306+
diagnostic: &diagnostics::Diagnostic,
307+
analysis: &Analysis,
308+
file_id: FileId,
309+
path: &Path,
310+
use_cli_severity: bool,
311+
cli: &mut dyn Cli,
312+
) -> Result<(), anyhow::Error> {
313+
let line_index = analysis.line_index(file_id)?;
314+
let converted_diagnostic =
315+
convert::ide_to_arc_diagnostic(&line_index, path, diagnostic, use_cli_severity);
316+
writeln!(
317+
cli,
318+
"{}",
319+
serde_json::to_string(&converted_diagnostic).unwrap_or_else(|err| panic!(
320+
"print_diagnostics_json failed for '{converted_diagnostic:?}': {err}"
321+
))
322+
)?;
323+
Ok(())
324+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Matches found in 1 modules:
2+
app_a: 1
3+
15:12-15:17::[WeakWarning] [ad-hoc: ssr-match] SSR pattern matched: ssr: {_@A, _@B}.

0 commit comments

Comments
 (0)