Skip to content

Commit 544c300

Browse files
committed
Improve speed of cargo dev fmt.
1 parent f00c58b commit 544c300

File tree

8 files changed

+113
-100
lines changed

8 files changed

+113
-100
lines changed

clippy_dev/src/fmt.rs

Lines changed: 28 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::utils::{
2-
ClippyInfo, ErrAction, FileUpdater, UpdateMode, UpdateStatus, panic_action, run_with_args_split, run_with_output,
2+
ErrAction, FileUpdater, UpdateMode, UpdateStatus, panic_action, run_with_output, split_args_for_threads,
3+
walk_dir_no_dot_or_target,
34
};
45
use itertools::Itertools;
56
use rustc_lexer::{TokenKind, tokenize};
@@ -9,7 +10,6 @@ use std::io::{self, Read};
910
use std::ops::ControlFlow;
1011
use std::path::PathBuf;
1112
use std::process::{self, Command, Stdio};
12-
use walkdir::WalkDir;
1313

1414
pub enum Error {
1515
Io(io::Error),
@@ -260,50 +260,27 @@ fn fmt_syms(update_mode: UpdateMode) {
260260
);
261261
}
262262

263-
fn run_rustfmt(clippy: &ClippyInfo, update_mode: UpdateMode) {
263+
fn run_rustfmt(update_mode: UpdateMode) {
264264
let mut rustfmt_path = String::from_utf8(run_with_output(
265265
"rustup which rustfmt",
266266
Command::new("rustup").args(["which", "rustfmt"]),
267267
))
268268
.expect("invalid rustfmt path");
269269
rustfmt_path.truncate(rustfmt_path.trim_end().len());
270270

271-
let mut cargo_path = String::from_utf8(run_with_output(
272-
"rustup which cargo",
273-
Command::new("rustup").args(["which", "cargo"]),
274-
))
275-
.expect("invalid cargo path");
276-
cargo_path.truncate(cargo_path.trim_end().len());
277-
278-
// Start all format jobs first before waiting on the results.
279-
let mut children = Vec::with_capacity(16);
280-
for &path in &[
281-
".",
282-
"clippy_config",
283-
"clippy_dev",
284-
"clippy_lints",
285-
"clippy_lints_internal",
286-
"clippy_utils",
287-
"rustc_tools_util",
288-
"lintcheck",
289-
] {
290-
let mut cmd = Command::new(&cargo_path);
291-
cmd.current_dir(clippy.path.join(path))
292-
.args(["fmt"])
293-
.env("RUSTFMT", &rustfmt_path)
294-
.stdout(Stdio::null())
295-
.stdin(Stdio::null())
296-
.stderr(Stdio::piped());
297-
if update_mode.is_check() {
298-
cmd.arg("--check");
299-
}
300-
match cmd.spawn() {
301-
Ok(x) => children.push(("cargo fmt", x)),
302-
Err(ref e) => panic_action(&e, ErrAction::Run, "cargo fmt".as_ref()),
303-
}
304-
}
271+
let args: Vec<_> = walk_dir_no_dot_or_target()
272+
.filter_map(|e| {
273+
let e = e.expect("error reading `.`");
274+
e.path()
275+
.as_os_str()
276+
.as_encoded_bytes()
277+
.ends_with(b".rs")
278+
.then(|| e.into_path().into_os_string())
279+
})
280+
.collect();
305281

306-
run_with_args_split(
282+
let mut children: Vec<_> = split_args_for_threads(
283+
32,
307284
|| {
308285
let mut cmd = Command::new(&rustfmt_path);
309286
if update_mode.is_check() {
@@ -312,27 +289,18 @@ fn run_rustfmt(clippy: &ClippyInfo, update_mode: UpdateMode) {
312289
cmd.stdout(Stdio::null())
313290
.stdin(Stdio::null())
314291
.stderr(Stdio::piped())
315-
.args(["--config", "show_parse_errors=false"]);
292+
.args(["--unstable-features", "--skip-children"]);
316293
cmd
317294
},
318-
|cmd| match cmd.spawn() {
319-
Ok(x) => children.push(("rustfmt", x)),
320-
Err(ref e) => panic_action(&e, ErrAction::Run, "rustfmt".as_ref()),
321-
},
322-
WalkDir::new("tests")
323-
.into_iter()
324-
.filter_entry(|p| p.path().file_name().is_none_or(|x| x != "skip_rustfmt"))
325-
.filter_map(|e| {
326-
let e = e.expect("error reading `tests`");
327-
e.path()
328-
.as_os_str()
329-
.as_encoded_bytes()
330-
.ends_with(b".rs")
331-
.then(|| e.into_path().into_os_string())
332-
}),
333-
);
295+
args.iter(),
296+
)
297+
.map(|mut cmd| match cmd.spawn() {
298+
Ok(x) => x,
299+
Err(ref e) => panic_action(&e, ErrAction::Run, "rustfmt".as_ref()),
300+
})
301+
.collect();
334302

335-
for (name, child) in &mut children {
303+
for child in &mut children {
336304
match child.wait() {
337305
Ok(status) => match (update_mode, status.exit_ok()) {
338306
(UpdateMode::Check | UpdateMode::Change, Ok(())) => {},
@@ -353,25 +321,17 @@ fn run_rustfmt(clippy: &ClippyInfo, update_mode: UpdateMode) {
353321
{
354322
eprintln!("{s}");
355323
}
356-
panic_action(&e, ErrAction::Run, name.as_ref());
324+
panic_action(&e, ErrAction::Run, "rustfmt".as_ref());
357325
},
358326
},
359-
Err(ref e) => panic_action(e, ErrAction::Run, name.as_ref()),
327+
Err(ref e) => panic_action(e, ErrAction::Run, "rustfmt".as_ref()),
360328
}
361329
}
362330
}
363331

364332
// the "main" function of cargo dev fmt
365-
pub fn run(clippy: &ClippyInfo, update_mode: UpdateMode) {
366-
if clippy.has_intellij_hook {
367-
eprintln!(
368-
"error: a local rustc repo is enabled as path dependency via `cargo dev setup intellij`.\n\
369-
Not formatting because that would format the local repo as well!\n\
370-
Please revert the changes to `Cargo.toml`s with `cargo dev remove intellij`."
371-
);
372-
return;
373-
}
374-
run_rustfmt(clippy, update_mode);
333+
pub fn run(update_mode: UpdateMode) {
334+
run_rustfmt(update_mode);
375335
fmt_syms(update_mode);
376336
if let Err(e) = fmt_conf(update_mode.is_check()) {
377337
e.display();

clippy_dev/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ fn main() {
2626
allow_staged,
2727
allow_no_vcs,
2828
} => dogfood::dogfood(fix, allow_dirty, allow_staged, allow_no_vcs),
29-
DevCommand::Fmt { check } => fmt::run(&clippy, utils::UpdateMode::from_check(check)),
29+
DevCommand::Fmt { check } => fmt::run(utils::UpdateMode::from_check(check)),
3030
DevCommand::UpdateLints { check } => update_lints::update(utils::UpdateMode::from_check(check)),
3131
DevCommand::NewLint {
3232
pass,

clippy_dev/src/rename_lint.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use crate::update_lints::{RenamedLint, find_lint_decls, generate_lint_files, read_deprecated_lints};
22
use crate::utils::{
33
FileUpdater, RustSearcher, Token, UpdateMode, UpdateStatus, Version, delete_dir_if_exists, delete_file_if_exists,
4-
try_rename_dir, try_rename_file,
4+
try_rename_dir, try_rename_file, walk_dir_no_dot_or_target,
55
};
66
use rustc_lexer::TokenKind;
77
use std::ffi::OsString;
88
use std::fs;
99
use std::path::Path;
10-
use walkdir::WalkDir;
1110

1211
/// Runs the `rename_lint` command.
1312
///
@@ -133,15 +132,8 @@ pub fn rename(clippy_version: Version, old_name: &str, new_name: &str, uplift: b
133132
}
134133

135134
let mut update_fn = file_update_fn(old_name, new_name, mod_edit);
136-
for file in WalkDir::new(".").into_iter().filter_entry(|e| {
137-
// Skip traversing some of the larger directories.
138-
e.path()
139-
.as_os_str()
140-
.as_encoded_bytes()
141-
.get(2..)
142-
.is_none_or(|x| x != "target".as_bytes() && x != ".git".as_bytes())
143-
}) {
144-
let file = file.expect("error reading clippy directory");
135+
for file in walk_dir_no_dot_or_target() {
136+
let file = file.expect("error reading `.`");
145137
if file.path().as_os_str().as_encoded_bytes().ends_with(b".rs") {
146138
updater.update_file(file.path(), &mut update_fn);
147139
}

clippy_dev/src/utils.rs

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
use core::fmt::{self, Display};
2+
use core::num::NonZero;
23
use core::ops::Range;
34
use core::slice;
45
use core::str::FromStr;
56
use rustc_lexer::{self as lexer, FrontmatterAllowed};
6-
use std::env;
77
use std::ffi::OsStr;
88
use std::fs::{self, OpenOptions};
99
use std::io::{self, Read as _, Seek as _, SeekFrom, Write};
1010
use std::path::{Path, PathBuf};
1111
use std::process::{self, Command, ExitStatus, Stdio};
12+
use std::{env, thread};
13+
use walkdir::WalkDir;
1214

1315
#[cfg(not(windows))]
1416
static CARGO_CLIPPY_EXE: &str = "cargo-clippy";
@@ -682,25 +684,71 @@ pub fn run_with_output(path: &(impl AsRef<Path> + ?Sized), cmd: &mut Command) ->
682684
f(path.as_ref(), cmd)
683685
}
684686

685-
pub fn run_with_args_split(
686-
mut make_cmd: impl FnMut() -> Command,
687-
mut run_cmd: impl FnMut(&mut Command),
688-
args: impl Iterator<Item: AsRef<OsStr>>,
689-
) {
690-
let mut cmd = make_cmd();
691-
let mut len = 0;
692-
for arg in args {
693-
len += arg.as_ref().len();
694-
cmd.arg(arg);
695-
// Very conservative limit
696-
if len > 10000 {
697-
run_cmd(&mut cmd);
698-
cmd = make_cmd();
699-
len = 0;
687+
/// Splits an argument list across multiple `Command` invocations.
688+
///
689+
/// The argument list will be split into a number of batches based on
690+
/// `thread::available_parallelism`, with `min_batch_size` setting a lower bound on the size of each
691+
/// batch.
692+
///
693+
/// If the size of the arguments would exceed the system limit additional batches will be created.
694+
pub fn split_args_for_threads(
695+
min_batch_size: usize,
696+
make_cmd: impl FnMut() -> Command,
697+
args: impl ExactSizeIterator<Item: AsRef<OsStr>>,
698+
) -> impl Iterator<Item = Command> {
699+
struct Iter<F, I> {
700+
make_cmd: F,
701+
args: I,
702+
min_batch_size: usize,
703+
batch_size: usize,
704+
thread_count: usize,
705+
}
706+
impl<F, I> Iterator for Iter<F, I>
707+
where
708+
F: FnMut() -> Command,
709+
I: ExactSizeIterator<Item: AsRef<OsStr>>,
710+
{
711+
type Item = Command;
712+
fn next(&mut self) -> Option<Self::Item> {
713+
if self.thread_count > 1 {
714+
self.thread_count -= 1;
715+
}
716+
let mut cmd = (self.make_cmd)();
717+
let mut cmd_len = 0usize;
718+
for arg in self.args.by_ref().take(self.batch_size) {
719+
cmd.arg(arg.as_ref());
720+
// `+ 8` to account for the `argv` pointer on unix.
721+
// Windows is complicated since the arguments are first converted to UTF-16ish,
722+
// but this needs to account for the space between arguments and whatever additional
723+
// is needed to escape within an argument.
724+
cmd_len += arg.as_ref().len() + 8;
725+
cmd_len += 8;
726+
727+
// Windows has a command length limit of 32767. For unix systems this is more
728+
// complicated since the limit includes environment variables and room needs to be
729+
// left to edit them once the program starts, but the total size comes from
730+
// `getconf ARG_MAX`.
731+
//
732+
// For simplicity we use 30000 here under a few assumptions.
733+
// * Individual arguments aren't super long (the final argument is still added)
734+
// * `ARG_MAX` is set to a reasonable amount. Basically every system will be configured way above
735+
// what windows supports, but POSIX only requires `4096`.
736+
if cmd_len > 30000 {
737+
self.batch_size = self.args.len().div_ceil(self.thread_count).max(self.min_batch_size);
738+
break;
739+
}
740+
}
741+
(cmd_len != 0).then_some(cmd)
700742
}
701743
}
702-
if len != 0 {
703-
run_cmd(&mut cmd);
744+
let thread_count = thread::available_parallelism().map_or(1, NonZero::get);
745+
let batch_size = args.len().div_ceil(thread_count).max(min_batch_size);
746+
Iter {
747+
make_cmd,
748+
args,
749+
min_batch_size,
750+
batch_size,
751+
thread_count,
704752
}
705753
}
706754

@@ -720,3 +768,12 @@ pub fn delete_dir_if_exists(path: &Path) {
720768
Err(ref e) => panic_action(e, ErrAction::Delete, path),
721769
}
722770
}
771+
772+
/// Walks all items excluding top-level dot files/directories and any target directories.
773+
pub fn walk_dir_no_dot_or_target() -> impl Iterator<Item = ::walkdir::Result<::walkdir::DirEntry>> {
774+
WalkDir::new(".").into_iter().filter_entry(|e| {
775+
e.path()
776+
.file_name()
777+
.is_none_or(|x| x != "target" && x.as_encoded_bytes().first().copied() != Some(b'.'))
778+
})
779+
}

rustfmt.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ edition = "2024"
66
error_on_line_overflow = true
77
imports_granularity = "Module"
88
style_edition = "2024"
9-
ignore = ["tests/ui/crashes/ice-10912.rs"]
9+
ignore = [
10+
"tests/ui/crashes/ice-9405.rs",
11+
"tests/ui/crashes/ice-10912.rs",
12+
"tests/ui/non_expressive_names_error_recovery.rs",
13+
]

tests/ui/skip_rustfmt/non_expressive_names_error_recovery.stderr renamed to tests/ui/non_expressive_names_error_recovery.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: expected one of `!`, `(`, `+`, `,`, `::`, `<`, or `>`, found `)`
2-
--> tests/ui/skip_rustfmt/non_expressive_names_error_recovery.rs:6:19
2+
--> tests/ui/non_expressive_names_error_recovery.rs:6:19
33
|
44
LL | fn aa(a: Aa<String) {
55
| ^ expected one of 7 possible tokens

0 commit comments

Comments
 (0)