Make it easier to see test performance (#5363)

Trying to make it easier to see possible bottlenecks.

Disabling hyperthreading seems like a significant reduction in
contention (15% improvement for me). Going down by half again reduces a
contention a little further, but not significantly from what I see. My
thought is that just flipping the flag is going to work best for people
cross-system versus a "divide by four", but welcome to other opinions
there. Note, I'm not digging into the source of the contention here,
just observing it.

Current default on my system (equivalent to `--threads=128`):

```
Running tests with 128 thread(s)
...
Ran 1272 tests in 3955 ms wall time, 397615 ms across threads
```

Disabling hyperthreads (equivalent to `--threads=64`):

```
Running tests with 64 thread(s)
...
Ran 1272 tests in 3520 ms wall time, 161957 ms across threads
```

`--threads=32`:
```
Running tests with 32 thread(s)
...
Ran 1272 tests in 3329 ms wall time, 69327 ms across threads
```

And for `./autoupdate_testdata.py --threads=64 --print_slowest_tests=5`:

```
Running tests with 64 thread(s)
...
Ran 1272 tests in 3417 ms wall time, 157946 ms across threads
  Slowest tests:
  - toolchain/lower/testdata/function/generic/call_recursive_basic.carbon: 1508 ms, 1484 ms in Run
  - toolchain/lower/testdata/builtins/print_read.carbon: 1506 ms, 1506 ms in Run
  - toolchain/lower/testdata/array/field.carbon: 1488 ms, 1487 ms in Run
  - toolchain/lower/testdata/builtins/int.carbon: 1482 ms, 1475 ms in Run
  - toolchain/lower/testdata/function/definition/params_one.carbon: 1472 ms, 1471 ms in Run
```

In test:

```
==================== Test output for //toolchain/testing:file_test:
Running tests with 64 thread(s)
...
Ran 1272 tests in 2968 ms wall time, 177732 ms across threads
  Slowest tests:
  - toolchain/lower/testdata/builtins/int.carbon: 1544 ms, 1533 ms in Run
  - toolchain/lower/testdata/array/function_param.carbon: 1539 ms, 1537 ms in Run
  - toolchain/lower/testdata/basics/zero.carbon: 1537 ms, 1536 ms in Run
  - toolchain/lower/testdata/function/call/params_one.carbon: 1535 ms, 1534 ms in Run
  - toolchain/lower/testdata/function/definition/params_zero.carbon: 1531 ms, 1531 ms in Run
[==========] Running 1272 tests from 1 test suite.
[----------] Global test environment set-up.
```
This commit is contained in:
Jon Ross-Perkins
2025-04-25 22:06:18 +00:00
committed by GitHub
parent d617cca530
commit 7181a37997
4 changed files with 84 additions and 5 deletions
+56 -5
View File
@@ -22,6 +22,7 @@
#include "testing/file_test/file_test_base.h"
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <functional>
@@ -65,6 +66,9 @@ ABSL_FLAG(unsigned int, threads, 0,
ABSL_FLAG(bool, dump_output, false,
"Instead of verifying files match test output, directly dump output "
"to stderr.");
ABSL_FLAG(int, print_slowest_tests, 5,
"The number of tests to print when showing slowest tests. Set to 0 "
"to disabling printing. Set to -1 to print all tests.");
namespace Carbon::Testing {
@@ -86,6 +90,9 @@ struct FileTestInfo {
// changed) the test file. This may be true even if output passes test
// expectations.
bool autoupdate_differs = false;
// Time spent in the test total, including processing and autoupdate.
std::chrono::milliseconds elapsed_ms = std::chrono::milliseconds(0);
};
// Adapts a `FileTestBase` instance to gtest for outputting results.
@@ -363,6 +370,7 @@ static auto SingleThreaded(llvm::ArrayRef<FileTestInfo> tests) -> bool {
// CrashRecoveryContext.
static auto RunSingleTestHelper(FileTestInfo& test, FileTestBase& test_instance)
-> void {
Timer timer;
// Add a crash trace entry with the single-file test command.
std::string test_command = GetBazelCommand(BazelMode::Test, test.test_name);
llvm::PrettyStackTraceString stack_trace_entry(test_command.c_str());
@@ -372,10 +380,12 @@ static auto RunSingleTestHelper(FileTestInfo& test, FileTestBase& test_instance)
!err.ok()) {
test.test_result = std::move(err).error();
}
test.elapsed_ms += timer.elapsed_ms();
}
// Runs a single test. Uses a CrashRecoveryContext, and returns false on a
// crash.
// crash. For test_elapsed_ms, try to exclude time spent waiting on
// output_mutex.
static auto RunSingleTest(FileTestInfo& test, bool single_threaded,
std::mutex& output_mutex) -> bool {
std::unique_ptr<FileTestBase> test_instance(test.factory_fn());
@@ -386,8 +396,11 @@ static auto RunSingleTest(FileTestInfo& test, bool single_threaded,
}
// Load expected output.
Timer process_timer;
test.test_result = ProcessTestFile(test_instance->test_name(),
absl::GetFlag(FLAGS_autoupdate));
test.elapsed_ms = process_timer.elapsed_ms();
if (test.test_result->ok()) {
// Execution must be serialized for either serial tests or console
// output.
@@ -418,9 +431,11 @@ static auto RunSingleTest(FileTestInfo& test, bool single_threaded,
return true;
}
Timer autoupdate_timer;
test.autoupdate_differs =
RunAutoupdater(test_instance.get(), **test.test_result,
/*dry_run=*/!absl::GetFlag(FLAGS_autoupdate));
test.elapsed_ms += autoupdate_timer.elapsed_ms();
std::unique_lock<std::mutex> lock(output_mutex);
if (absl::GetFlag(FLAGS_dump_output)) {
@@ -447,8 +462,11 @@ auto FileTestEventListener::OnTestProgramStart(
} else {
// Enable the CRC for use in `RunSingleTest`.
llvm::CrashRecoveryContext::Enable();
pool = std::make_unique<llvm::DefaultThreadPool>(llvm::ThreadPoolStrategy{
.ThreadsRequested = absl::GetFlag(FLAGS_threads)});
llvm::ThreadPoolStrategy thread_strategy = {
.ThreadsRequested = absl::GetFlag(FLAGS_threads),
// Disable hyper threads to reduce contention.
.UseHyperThreads = false};
pool = std::make_unique<llvm::DefaultThreadPool>(thread_strategy);
}
if (!absl::GetFlag(FLAGS_dump_output)) {
llvm::errs() << "Running tests with " << pool->getMaxConcurrency()
@@ -457,12 +475,15 @@ auto FileTestEventListener::OnTestProgramStart(
// Guard access to output (stdout and stderr).
std::mutex output_mutex;
std::atomic<bool> crashed = false;
std::atomic<bool> crashed = false;
Timer all_timer;
int run_count = 0;
for (auto& test : tests_) {
if (!test.registered_test->should_run()) {
continue;
}
++run_count;
pool->async([&] {
// If any thread crashed, don't try running more.
@@ -482,7 +503,37 @@ auto FileTestEventListener::OnTestProgramStart(
// We expect to have leaked memory if one or more of our tests crashed.
std::abort();
}
llvm::errs() << "\nDone!\n";
// Calculate the total test time.
auto all_elapsed_ms = all_timer.elapsed_ms();
auto total_elapsed_ms = std::chrono::milliseconds(0);
for (auto& test : tests_) {
total_elapsed_ms += test.elapsed_ms;
}
llvm::errs() << "\nRan " << run_count << " tests in "
<< all_elapsed_ms.count() << " ms wall time, "
<< total_elapsed_ms.count() << " ms across threads\n";
// When there are multiple tests, give additional timing details, particularly
// slowest tests.
auto print_slowest_tests = absl::GetFlag(FLAGS_print_slowest_tests);
if (run_count > 1 && print_slowest_tests != 0) {
llvm::errs() << " Slowest tests:\n";
llvm::sort(tests_, [](const FileTestInfo& lhs, const FileTestInfo& rhs) {
return lhs.elapsed_ms > rhs.elapsed_ms;
});
int count = print_slowest_tests > 0 ? print_slowest_tests : run_count;
for (const auto& test : tests_.take_front(count)) {
std::chrono::milliseconds run_ms(0);
if (test.test_result && test.test_result->ok()) {
run_ms = test.test_result.value()->run_elapsed_ms;
}
llvm::errs() << " - " << test.test_name << ": "
<< test.elapsed_ms.count() << " ms, " << run_ms.count()
<< " ms in Run\n";
}
}
}
// Implements main() within the Carbon::Testing namespace for convenience.