Skip to content

[rustdoc] Display total time and compilation time of merged doctests #144308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 30, 2025
Merged
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
84 changes: 69 additions & 15 deletions src/librustdoc/doctest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use std::path::{Path, PathBuf};
use std::process::{self, Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::{panic, str};
use std::time::{Duration, Instant};
use std::{fmt, panic, str};

pub(crate) use make::{BuildDocTestBuilder, DocTestBuilder};
pub(crate) use markdown::test as test_markdown;
Expand All @@ -36,6 +37,50 @@ use crate::config::{Options as RustdocOptions, OutputFormat};
use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine};
use crate::lint::init_lints;

/// Type used to display times (compilation and total) information for merged doctests.
struct MergedDoctestTimes {
total_time: Instant,
/// Total time spent compiling all merged doctests.
compilation_time: Duration,
/// This field is used to keep track of how many merged doctests we (tried to) compile.
added_compilation_times: usize,
}

impl MergedDoctestTimes {
fn new() -> Self {
Self {
total_time: Instant::now(),
compilation_time: Duration::default(),
added_compilation_times: 0,
}
}

fn add_compilation_time(&mut self, duration: Duration) {
self.compilation_time += duration;
self.added_compilation_times += 1;
}

fn display_times(&self) {
// If no merged doctest was compiled, then there is nothing to display since the numbers
// displayed by `libtest` for standalone tests are already accurate (they include both
// compilation and runtime).
if self.added_compilation_times > 0 {
println!("{self}");
}
}
}

impl fmt::Display for MergedDoctestTimes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"all doctests ran in {:.2}s; merged doctests compilation took {:.2}s",
self.total_time.elapsed().as_secs_f64(),
self.compilation_time.as_secs_f64(),
)
}
}

/// Options that apply to all doctests in a crate or Markdown file (for `rustdoc foo.md`).
#[derive(Clone)]
pub(crate) struct GlobalTestOptions {
Expand Down Expand Up @@ -295,6 +340,7 @@ pub(crate) fn run_tests(

let mut nb_errors = 0;
let mut ran_edition_tests = 0;
let mut times = MergedDoctestTimes::new();
let target_str = rustdoc_options.target.to_string();

for (MergeableTestKey { edition, global_crate_attrs_hash }, mut doctests) in mergeable_tests {
Expand All @@ -314,13 +360,15 @@ pub(crate) fn run_tests(
for (doctest, scraped_test) in &doctests {
tests_runner.add_test(doctest, scraped_test, &target_str);
}
if let Ok(success) = tests_runner.run_merged_tests(
let (duration, ret) = tests_runner.run_merged_tests(
rustdoc_test_options,
edition,
&opts,
&test_args,
rustdoc_options,
) {
);
times.add_compilation_time(duration);
if let Ok(success) = ret {
ran_edition_tests += 1;
if !success {
nb_errors += 1;
Expand Down Expand Up @@ -354,11 +402,13 @@ pub(crate) fn run_tests(
test::test_main_with_exit_callback(&test_args, standalone_tests, None, || {
// We ensure temp dir destructor is called.
std::mem::drop(temp_dir.take());
times.display_times();
});
}
if nb_errors != 0 {
// We ensure temp dir destructor is called.
std::mem::drop(temp_dir);
times.display_times();
// libtest::ERROR_EXIT_CODE is not public but it's the same value.
std::process::exit(101);
}
Expand Down Expand Up @@ -496,16 +546,19 @@ impl RunnableDocTest {
///
/// This is the function that calculates the compiler command line, invokes the compiler, then
/// invokes the test or tests in a separate executable (if applicable).
///
/// Returns a tuple containing the `Duration` of the compilation and the `Result` of the test.
fn run_test(
doctest: RunnableDocTest,
rustdoc_options: &RustdocOptions,
supports_color: bool,
report_unused_externs: impl Fn(UnusedExterns),
) -> Result<(), TestFailure> {
) -> (Duration, Result<(), TestFailure>) {
let langstr = &doctest.langstr;
// Make sure we emit well-formed executable names for our target.
let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
let output_file = doctest.test_opts.outdir.path().join(rust_out);
let instant = Instant::now();

// Common arguments used for compiling the doctest runner.
// On merged doctests, the compiler is invoked twice: once for the test code itself,
Expand Down Expand Up @@ -589,7 +642,7 @@ fn run_test(
if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
// If we cannot write this file for any reason, we leave. All combined tests will be
// tested as standalone tests.
return Err(TestFailure::CompileError);
return (Duration::default(), Err(TestFailure::CompileError));
}
if !rustdoc_options.nocapture {
// If `nocapture` is disabled, then we don't display rustc's output when compiling
Expand Down Expand Up @@ -660,7 +713,7 @@ fn run_test(
if std::fs::write(&runner_input_file, merged_test_code).is_err() {
// If we cannot write this file for any reason, we leave. All combined tests will be
// tested as standalone tests.
return Err(TestFailure::CompileError);
return (instant.elapsed(), Err(TestFailure::CompileError));
}
if !rustdoc_options.nocapture {
// If `nocapture` is disabled, then we don't display rustc's output when compiling
Expand Down Expand Up @@ -713,7 +766,7 @@ fn run_test(
let _bomb = Bomb(&out);
match (output.status.success(), langstr.compile_fail) {
(true, true) => {
return Err(TestFailure::UnexpectedCompilePass);
return (instant.elapsed(), Err(TestFailure::UnexpectedCompilePass));
}
(true, false) => {}
(false, true) => {
Expand All @@ -729,17 +782,18 @@ fn run_test(
.collect();

if !missing_codes.is_empty() {
return Err(TestFailure::MissingErrorCodes(missing_codes));
return (instant.elapsed(), Err(TestFailure::MissingErrorCodes(missing_codes)));
}
}
}
(false, false) => {
return Err(TestFailure::CompileError);
return (instant.elapsed(), Err(TestFailure::CompileError));
}
}

let duration = instant.elapsed();
if doctest.no_run {
return Ok(());
return (duration, Ok(()));
}

// Run the code!
Expand Down Expand Up @@ -771,17 +825,17 @@ fn run_test(
cmd.output()
};
match result {
Err(e) => return Err(TestFailure::ExecutionError(e)),
Err(e) => return (duration, Err(TestFailure::ExecutionError(e))),
Ok(out) => {
if langstr.should_panic && out.status.success() {
return Err(TestFailure::UnexpectedRunPass);
return (duration, Err(TestFailure::UnexpectedRunPass));
} else if !langstr.should_panic && !out.status.success() {
return Err(TestFailure::ExecutionFailure(out));
return (duration, Err(TestFailure::ExecutionFailure(out)));
}
}
}

Ok(())
(duration, Ok(()))
}

/// Converts a path intended to use as a command to absolute if it is
Expand Down Expand Up @@ -1071,7 +1125,7 @@ fn doctest_run_fn(
no_run: scraped_test.no_run(&rustdoc_options),
merged_test_code: None,
};
let res =
let (_, res) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we're not also tracking this, so we can report (time compiling standalone tests, time compiling merged tests)?

I believe doctests are run with some level of parallelism, so currently we would be reporting the total absolute wall clock time for the total compilation time, but for merged doctests, we're reporting the sum of the amount of time each thread spent compiling. This inconsistency could over-report the impact of merged doctests.

Also, if parallelism is involved, then I think perhaps a more useful metric for minimizing compilation time would be how long the longest test took.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only compile and run standalone tests in parallel, merged doctests are never compiled in parallel (but still run in parallel).

We report raw numbers. Also we cannot get which tests took the longest to run as only libtest has this information in theory (but they don't keep track of it). Finally, I don't see how this is an issue to say how much time was spent in compiling merged doctests.

run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);

if let Err(err) = res {
Expand Down
11 changes: 8 additions & 3 deletions src/librustdoc/doctest/runner.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::fmt::Write;
use std::time::Duration;

use rustc_data_structures::fx::FxIndexSet;
use rustc_span::edition::Edition;
Expand Down Expand Up @@ -67,14 +68,18 @@ impl DocTestRunner {
self.nb_tests += 1;
}

/// Returns a tuple containing the `Duration` of the compilation and the `Result` of the test.
///
/// If compilation failed, it will return `Err`, otherwise it will return `Ok` containing if
/// the test ran successfully.
pub(crate) fn run_merged_tests(
&mut self,
test_options: IndividualTestOptions,
edition: Edition,
opts: &GlobalTestOptions,
test_args: &[String],
rustdoc_options: &RustdocOptions,
) -> Result<bool, ()> {
) -> (Duration, Result<bool, ()>) {
let mut code = "\
#![allow(unused_extern_crates)]
#![allow(internal_features)]
Expand Down Expand Up @@ -204,9 +209,9 @@ std::process::Termination::report(test::test_main(test_args, tests, None))
no_run: false,
merged_test_code: Some(code),
};
let ret =
let (duration, ret) =
run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {});
if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }
(duration, if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) })
}
}

Expand Down
2 changes: 2 additions & 0 deletions tests/rustdoc-ui/2024-doctests-checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//@ compile-flags: --test --test-args=--test-threads=1
//@ normalize-stdout: "tests/rustdoc-ui" -> "$$DIR"
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
//@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME"
//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME"
//@ normalize-stdout: ".rs:\d+:\d+" -> ".rs:$$LINE:$$COL"

/// ```
Expand Down
5 changes: 3 additions & 2 deletions tests/rustdoc-ui/2024-doctests-checks.stdout
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@

running 1 test
test $DIR/2024-doctests-checks.rs - Foo (line 8) ... ok
test $DIR/2024-doctests-checks.rs - Foo (line 10) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME


running 1 test
test $DIR/2024-doctests-checks.rs - Foo (line 15) ... ok
test $DIR/2024-doctests-checks.rs - Foo (line 17) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME

all doctests ran in $TIME; merged doctests compilation took $TIME
2 changes: 2 additions & 0 deletions tests/rustdoc-ui/2024-doctests-crate-attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
//@ normalize-stdout: "tests/rustdoc-ui" -> "$$DIR"
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
//@ normalize-stdout: ".rs:\d+:\d+" -> ".rs:$$LINE:$$COL"
//@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME"
//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME"

/// This doctest is used to ensure that if a crate attribute is present,
/// it will not be part of the merged doctests.
Expand Down
5 changes: 3 additions & 2 deletions tests/rustdoc-ui/2024-doctests-crate-attribute.stdout
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@

running 1 test
test $DIR/2024-doctests-crate-attribute.rs - Foo (line 20) ... ok
test $DIR/2024-doctests-crate-attribute.rs - Foo (line 22) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME


running 1 test
test $DIR/2024-doctests-crate-attribute.rs - Foo (line 11) ... ok
test $DIR/2024-doctests-crate-attribute.rs - Foo (line 13) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME

all doctests ran in $TIME; merged doctests compilation took $TIME
2 changes: 2 additions & 0 deletions tests/rustdoc-ui/doctest/dead-code-2024.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
//@ compile-flags:--test
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
//@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME"
//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME"
//@ failure-status: 101

#![doc(test(attr(allow(unused_variables), deny(warnings))))]
Expand Down
11 changes: 6 additions & 5 deletions tests/rustdoc-ui/doctest/dead-code-2024.stdout
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@

running 1 test
test $DIR/dead-code-2024.rs - f (line 13) - compile ... FAILED
test $DIR/dead-code-2024.rs - f (line 15) - compile ... FAILED

failures:

---- $DIR/dead-code-2024.rs - f (line 13) stdout ----
---- $DIR/dead-code-2024.rs - f (line 15) stdout ----
error: trait `T` is never used
--> $DIR/dead-code-2024.rs:14:7
--> $DIR/dead-code-2024.rs:16:7
|
LL | trait T { fn f(); }
| ^
|
note: the lint level is defined here
--> $DIR/dead-code-2024.rs:12:9
--> $DIR/dead-code-2024.rs:14:9
|
LL | #![deny(warnings)]
| ^^^^^^^^
Expand All @@ -23,7 +23,8 @@ error: aborting due to 1 previous error
Couldn't compile the test.

failures:
$DIR/dead-code-2024.rs - f (line 13)
$DIR/dead-code-2024.rs - f (line 15)

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME

all doctests ran in $TIME; merged doctests compilation took $TIME
2 changes: 2 additions & 0 deletions tests/rustdoc-ui/doctest/dead-code-items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
//@ compile-flags:--test --test-args=--test-threads=1
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
//@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME"
//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME"
//@ failure-status: 101

#![doc(test(attr(deny(warnings))))]
Expand Down
Loading
Loading