Files
carbon-lang/testing/file_test/run_test.cpp
T
Jon Ross-Perkins 7181a37997 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.
```
2025-04-25 22:06:18 +00:00

191 lines
6.8 KiB
C++

// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "testing/file_test/run_test.h"
#include <gtest/gtest.h>
#include <string>
#include <utility>
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/PrettyStackTrace.h"
#include "testing/file_test/file_test_base.h"
#include "testing/file_test/test_file.h"
namespace Carbon::Testing {
// While these are marked as "internal" APIs, they seem to work and be pretty
// widely used for their exact documented behavior.
using ::testing::internal::CaptureStderr;
using ::testing::internal::CaptureStdout;
using ::testing::internal::GetCapturedStderr;
using ::testing::internal::GetCapturedStdout;
static constexpr llvm::StringLiteral StdinFilename = "STDIN";
// Does replacements in ARGS for %s and %t.
static auto DoArgReplacements(llvm::SmallVector<std::string>& test_args,
const llvm::StringMap<std::string>& replacements,
llvm::ArrayRef<TestFile::Split*> split_files)
-> ErrorOr<Success> {
for (auto* it = test_args.begin(); it != test_args.end(); ++it) {
auto percent = it->find("%");
if (percent == std::string::npos) {
continue;
}
if (percent + 1 >= it->size()) {
return ErrorBuilder() << "% is not allowed on its own: " << *it;
}
char c = (*it)[percent + 1];
switch (c) {
case 's': {
if (*it != "%s") {
return ErrorBuilder() << "%s must be the full argument: " << *it;
}
it = test_args.erase(it);
for (const auto& split : split_files) {
const std::string& filename = split->filename;
if (filename == StdinFilename || filename.ends_with(".h")) {
continue;
}
it = test_args.insert(it, filename);
++it;
}
// Back up once because the for loop will advance.
--it;
break;
}
case 't': {
char* tmpdir = getenv("TEST_TMPDIR");
CARBON_CHECK(tmpdir != nullptr);
it->replace(percent, 2, llvm::formatv("{0}/temp_file", tmpdir));
break;
}
case '{': {
auto end_brace = it->find('}', percent);
if (end_brace == std::string::npos) {
return ErrorBuilder() << "%{ without closing }: " << *it;
}
llvm::StringRef substr(&*(it->begin() + percent + 2),
end_brace - percent - 2);
auto replacement = replacements.find(substr);
if (replacement == replacements.end()) {
return ErrorBuilder()
<< "unknown substitution: %{" << substr << "}: " << *it;
}
it->replace(percent, end_brace - percent + 1, replacement->second);
break;
}
default:
return ErrorBuilder() << "%" << c << " is not supported: " << *it;
}
}
return Success();
}
auto RunTestFile(const FileTestBase& test_base, bool dump_output,
TestFile& test_file) -> ErrorOr<Success> {
llvm::SmallVector<TestFile::Split*> all_splits;
for (auto& split : test_file.file_splits) {
all_splits.push_back(&split);
}
for (auto& split : test_file.include_file_splits) {
all_splits.push_back(&split);
}
// Process arguments.
if (test_file.test_args.empty()) {
test_file.test_args = test_base.GetDefaultArgs();
test_file.test_args.append(test_file.extra_args);
}
CARBON_RETURN_IF_ERROR(DoArgReplacements(
test_file.test_args, test_base.GetArgReplacements(), all_splits));
// stdin needs to exist on-disk for compatibility. We'll use a pointer for it.
FILE* input_stream = nullptr;
auto erase_input_on_exit = llvm::make_scope_exit([&input_stream]() {
if (input_stream) {
// fclose should delete the tmpfile.
fclose(input_stream);
input_stream = nullptr;
}
});
// Create the files in-memory.
llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> fs =
new llvm::vfs::InMemoryFileSystem;
for (const auto& split : all_splits) {
if (split->filename == StdinFilename) {
input_stream = tmpfile();
fwrite(split->content.c_str(), sizeof(char), split->content.size(),
input_stream);
CARBON_CHECK(!fseek(input_stream, 0, SEEK_SET));
} else if (!fs->addFile(split->filename, /*ModificationTime=*/0,
llvm::MemoryBuffer::getMemBuffer(
split->content, split->filename,
/*RequiresNullTerminator=*/false))) {
return ErrorBuilder() << "File is repeated: " << split->filename;
}
}
// Convert the arguments to StringRef and const char* to match the
// expectations of PrettyStackTraceProgram and Run.
llvm::SmallVector<llvm::StringRef> test_args_ref;
llvm::SmallVector<const char*> test_argv_for_stack_trace;
test_args_ref.reserve(test_file.test_args.size());
test_argv_for_stack_trace.reserve(test_file.test_args.size() + 1);
for (const auto& arg : test_file.test_args) {
test_args_ref.push_back(arg);
test_argv_for_stack_trace.push_back(arg.c_str());
}
// Add a trailing null so that this is a proper argv.
test_argv_for_stack_trace.push_back(nullptr);
// Add a stack trace entry for the test invocation.
llvm::PrettyStackTraceProgram stack_trace_entry(
test_argv_for_stack_trace.size() - 1, test_argv_for_stack_trace.data());
// Conditionally capture console output. We use a scope exit to ensure the
// captures terminate even on run failures.
if (test_file.capture_console_output) {
CaptureStderr();
CaptureStdout();
}
// Prepare string streams to capture output. In order to address casting
// constraints, we split calls to Run as a ternary based on whether we want to
// capture output.
llvm::raw_svector_ostream output_stream(test_file.actual_stdout);
llvm::raw_svector_ostream error_stream(test_file.actual_stderr);
Timer timer;
ErrorOr<FileTestBase::RunResult> run_result =
dump_output ? test_base.Run(test_args_ref, fs, input_stream, llvm::outs(),
llvm::errs())
: test_base.Run(test_args_ref, fs, input_stream,
output_stream, error_stream);
test_file.run_elapsed_ms = timer.elapsed_ms();
// Ensure stdout/stderr are always fetched, even when discarded on error.
if (test_file.capture_console_output) {
// No need to flush stderr.
llvm::outs().flush();
test_file.actual_stdout += GetCapturedStdout();
test_file.actual_stderr += GetCapturedStderr();
}
if (!run_result.ok()) {
return std::move(run_result).error();
}
test_file.run_result = std::move(*run_result);
return Success();
}
} // namespace Carbon::Testing