Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 65 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ use std::collections::HashMap;
use std::collections::HashSet;
use std::convert::TryFrom;
use std::fs::OpenOptions;
use std::io::Write;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use version_control::VersionControl;

pub mod git;
Expand Down Expand Up @@ -168,6 +170,7 @@ pub fn do_lint(
revision_opt: RevisionOpt,
tee_json: Option<String>,
only_lint_under_config_dir: bool,
show_timings: bool,
) -> Result<i32> {
debug!(
"Running linters: {:?}",
Expand Down Expand Up @@ -215,16 +218,19 @@ pub fn do_lint(

log_utils::log_files("Linting files: ", &files);

let wall_start = Instant::now();
let mut thread_handles = Vec::new();
let spinners = Arc::new(MultiProgress::new());

// Too lazy to learn rust's fancy concurrent programming stuff, just spawn a thread per linter and join them.
let all_lints = Arc::new(Mutex::new(HashMap::new()));
let timings: Arc<Mutex<Vec<(String, Duration)>>> = Arc::new(Mutex::new(Vec::new()));

for linter in linters {
let all_lints = Arc::clone(&all_lints);
let files = Arc::clone(&files);
let spinners = Arc::clone(&spinners);
let timings = Arc::clone(&timings);

let handle = thread::spawn(move || -> Result<()> {
let mut spinner = None;
Expand All @@ -235,7 +241,16 @@ pub fn do_lint(
spinner = Some(_spinner);
}

let linter_code = linter.code.clone();
let start = Instant::now();
let lints = linter.run(&files);
let elapsed = start.elapsed();

// Record timing
{
let mut timings = timings.lock().expect("timings mutex poisoned");
timings.push((linter_code.clone(), elapsed));
}

// If we're applying patches later, don't consider lints that would
// be fixed by that.
Expand All @@ -252,9 +267,9 @@ pub fn do_lint(
group_lints_by_file(&mut all_lints, lints);

let spinner_message = if is_success {
format!("{} {}", linter.code, style("success!").green())
format!("{} {}", linter_code, style("success!").green())
} else {
format!("{} {}", linter.code, style("failure").red())
format!("{} {}", linter_code, style("failure").red())
};

if enable_spinners {
Expand All @@ -270,6 +285,54 @@ pub fn do_lint(
handle.join().unwrap()?;
}

// Print timing summary if requested
if show_timings {
let mut timings = timings.lock().expect("timings mutex poisoned");
timings.sort_by(|a, b| b.1.cmp(&a.1)); // Sort by duration, descending

let mut stderr = Term::stderr();
writeln!(stderr)?;

// Calculate column width based on longest linter name
let max_name_len = timings
.iter()
.map(|(code, _)| code.len())
.max()
.unwrap_or(10)
.max(10); // minimum 10 chars
let line_width = max_name_len + 22; // name + spacing + time + pct

writeln!(stderr, "{}", style("Linter Timings:").bold())?;
writeln!(stderr, "{}", style("─".repeat(line_width)).dim())?;

let wall_time = wall_start.elapsed();
for (code, duration) in timings.iter() {
let secs = duration.as_secs_f64();
let pct = if wall_time.as_secs_f64() > 0.0 {
(secs / wall_time.as_secs_f64()) * 100.0
} else {
0.0
};
writeln!(
stderr,
" {:<width$} {:>8.2}s {:>5.1}%",
code,
secs,
pct,
width = max_name_len
)?;
}
writeln!(stderr, "{}", style("─".repeat(line_width)).dim())?;
writeln!(
stderr,
" {:<width$} {:>8.2}s",
style("Total (wall clock)").bold(),
wall_time.as_secs_f64(),
width = max_name_len
)?;
writeln!(stderr)?;
}

// Unwrap is fine because all other owners hsould have been joined.
let all_lints = all_lints.lock().unwrap();

Expand Down
6 changes: 6 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ struct Args {
/// If set, will only lint files under the directory where the configuration file is located and its subdirectories.
#[clap(long, global = true)]
only_lint_under_config_dir: bool,

/// If set, print a summary of how long each linter took to run.
#[clap(long, global = true)]
timings: bool,
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -305,6 +309,7 @@ fn do_main() -> Result<i32> {
revision_opt,
args.tee_json,
only_lint_under_config_dir,
args.timings,
)
}
SubCommand::Lint => {
Expand All @@ -319,6 +324,7 @@ fn do_main() -> Result<i32> {
revision_opt,
args.tee_json,
only_lint_under_config_dir,
args.timings,
)
}
SubCommand::Rage {
Expand Down
4 changes: 3 additions & 1 deletion tests/snapshots/integration_test__invalid_config_fails.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ expression: output_lines
- ""
- "STDERR:"
- "error: Config file had invalid schema"
- ""
- " Caused by:"
- " missing field `linter`"
- "caused_by: missing field `linter`"

Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
---
source: tests/integration_test.rs
expression: output_lines

---
- "STDOUT:"
- ""
- ""
- "STDERR:"
- "WARNING: No previous init data found. If this is the first time you're running lintrunner, you should run `lintrunner init`."
- "error: Failed to find provided file: 'blahblahblah'"
- ""
- " Caused by:"
- " No such file or directory (os error 2)"
- "caused_by: No such file or directory (os error 2)"

5 changes: 3 additions & 2 deletions tests/snapshots/integration_test__unknown_config_fails.snap
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
---
source: tests/integration_test.rs
expression: output_lines

---
- "STDOUT:"
- ""
- ""
- "STDERR:"
- "error: Could not read lintrunner config at: 'asdfasdfasdf'"
- ""
- " Caused by:"
- " No such file or directory (os error 2)"
- "caused_by: No such file or directory (os error 2)"