diff --git a/Cargo.lock b/Cargo.lock index d39943e..606686e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "omnilinter" version = "0.2.0" @@ -377,10 +387,12 @@ dependencies = [ "clap", "criterion", "glob", + "num_cpus", "pest", "pest_derive", "regex", "rstest", + "scoped_threadpool", "serde", "serde_json", "tempdir", @@ -622,6 +634,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "semver" version = "1.0.22" diff --git a/Cargo.toml b/Cargo.toml index c8cd843..06c1833 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,11 @@ harness = false assert_cmd = "2.0.13" clap = { version = "4.5.0", features = ["derive"] } glob = "0.3.1" +num_cpus = "1.16.0" pest = "2.7.7" pest_derive = "2.7.7" regex = { version = "1.10.3", default-features = false, features = ["std", "unicode-perl"] } +scoped_threadpool = "0.1.9" serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.113" tempdir = "0.3.7" diff --git a/src/main.rs b/src/main.rs index 4199464..261530b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,8 +16,10 @@ use crate::formatters::json as format_json; use crate::formatters::text as format_text; use crate::r#match::MatchResult; use clap::{Parser, ValueEnum}; +use scoped_threadpool::Pool as ThreadPool; use std::collections::HashSet; use std::path::PathBuf; +use std::sync::{Arc, Mutex}; const CONFIG_FILE_NAME: &str = "omnilinter.conf"; @@ -60,6 +62,10 @@ struct Args { #[arg(long, value_name = "EXITCODE")] error_exitcode: Option, + /// If any matches are found, exit with given code + #[arg(short = 'j', long = "jobs", value_name = "COUNT")] + num_threads: Option, + /// Directories to operate on #[arg(value_name = "TARGET_DIR")] roots: Vec, @@ -105,11 +111,23 @@ fn main() { let ruleset = config.ruleset.compile(); - let mut result = MatchResult::new(); + let result = Arc::new(Mutex::new(MatchResult::new())); - for root in &roots { - result.append(apply_ruleset(&ruleset, &root)); - } + let num_threads = args.num_threads.unwrap_or_else(|| num_cpus::get()); + let mut pool = ThreadPool::new(num_threads.try_into().unwrap_or(1)); + + pool.scoped(|scope| { + let ruleset = &ruleset; + for root in &roots { + let result = result.clone(); + scope.execute(move || { + let res = apply_ruleset(&ruleset, &root); + result.lock().unwrap().append(res); + }); + } + }); + + let result = Arc::into_inner(result).unwrap().into_inner().unwrap(); match args.output_format { OutputFormat::ByRoot => { diff --git a/src/match.rs b/src/match.rs index cf55146..df7a854 100644 --- a/src/match.rs +++ b/src/match.rs @@ -24,6 +24,11 @@ impl<'a> MatchResult<'a> { } } +// Although MatchResult contains Rc<> in individual matches, it is safe +// to transfer across thread boundaries as a whole, because Rc's are only +// shared inside of it +unsafe impl Send for MatchResult<'_> {} + pub struct Match<'a> { pub rule: &'a Rule, pub root: &'a Path,