351 lines
9.5 KiB
C++
351 lines
9.5 KiB
C++
// Copyright 2020 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include <unistd.h>
|
|
|
|
#include "gtest/gtest.h"
|
|
#include "absl/synchronization/barrier.h"
|
|
#include "benchmark/benchmark.h"
|
|
#include "test/util/cleanup.h"
|
|
#include "test/util/file_descriptor.h"
|
|
#include "test/util/logging.h"
|
|
#include "test/util/test_util.h"
|
|
#include "test/util/thread_util.h"
|
|
|
|
namespace gvisor {
|
|
namespace testing {
|
|
|
|
namespace {
|
|
|
|
constexpr int kBusyMax = 250;
|
|
|
|
// Do some CPU-bound busy-work.
|
|
int busy(int max) {
|
|
// Prevent the compiler from optimizing this work away,
|
|
volatile int count = 0;
|
|
|
|
for (int i = 1; i < max; i++) {
|
|
for (int j = 2; j < i / 2; j++) {
|
|
if (i % j == 0) {
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void BM_CPUBoundUniprocess(benchmark::State& state) {
|
|
for (auto _ : state) {
|
|
busy(kBusyMax);
|
|
}
|
|
}
|
|
|
|
BENCHMARK(BM_CPUBoundUniprocess);
|
|
|
|
void BM_CPUBoundAsymmetric(benchmark::State& state) {
|
|
const size_t max = state.max_iterations;
|
|
pid_t child = fork();
|
|
if (child == 0) {
|
|
for (int i = 0; i < max; i++) {
|
|
busy(kBusyMax);
|
|
}
|
|
_exit(0);
|
|
}
|
|
ASSERT_THAT(child, SyscallSucceeds());
|
|
ASSERT_TRUE(state.KeepRunningBatch(max));
|
|
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceeds());
|
|
EXPECT_TRUE(WIFEXITED(status));
|
|
EXPECT_EQ(0, WEXITSTATUS(status));
|
|
ASSERT_FALSE(state.KeepRunning());
|
|
}
|
|
|
|
BENCHMARK(BM_CPUBoundAsymmetric)->UseRealTime();
|
|
|
|
void BM_CPUBoundSymmetric(benchmark::State& state) {
|
|
std::vector<pid_t> children;
|
|
auto child_cleanup = Cleanup([&] {
|
|
for (const pid_t child : children) {
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceeds());
|
|
EXPECT_TRUE(WIFEXITED(status));
|
|
EXPECT_EQ(0, WEXITSTATUS(status));
|
|
}
|
|
ASSERT_FALSE(state.KeepRunning());
|
|
});
|
|
|
|
const int processes = state.range(0);
|
|
for (int i = 0; i < processes; i++) {
|
|
size_t cur = (state.max_iterations + (processes - 1)) / processes;
|
|
if ((state.iterations() + cur) >= state.max_iterations) {
|
|
cur = state.max_iterations - state.iterations();
|
|
}
|
|
pid_t child = fork();
|
|
if (child == 0) {
|
|
for (int i = 0; i < cur; i++) {
|
|
busy(kBusyMax);
|
|
}
|
|
_exit(0);
|
|
}
|
|
ASSERT_THAT(child, SyscallSucceeds());
|
|
if (cur > 0) {
|
|
// We can have a zero cur here, depending.
|
|
ASSERT_TRUE(state.KeepRunningBatch(cur));
|
|
}
|
|
children.push_back(child);
|
|
}
|
|
}
|
|
|
|
BENCHMARK(BM_CPUBoundSymmetric)->Range(2, 16)->UseRealTime();
|
|
|
|
// Child routine for ProcessSwitch/ThreadSwitch.
|
|
// Reads from readfd and writes the result to writefd.
|
|
void SwitchChild(int readfd, int writefd) {
|
|
while (1) {
|
|
char buf;
|
|
int ret = ReadFd(readfd, &buf, 1);
|
|
if (ret == 0) {
|
|
break;
|
|
}
|
|
TEST_CHECK_MSG(ret == 1, "read failed");
|
|
|
|
ret = WriteFd(writefd, &buf, 1);
|
|
if (ret == -1) {
|
|
TEST_CHECK_MSG(errno == EPIPE, "unexpected write failure");
|
|
break;
|
|
}
|
|
TEST_CHECK_MSG(ret == 1, "write failed");
|
|
}
|
|
}
|
|
|
|
// Send bytes in a loop through a series of pipes, each passing through a
|
|
// different process.
|
|
//
|
|
// Proc 0 Proc 1
|
|
// * ----------> *
|
|
// ^ Pipe 1 |
|
|
// | |
|
|
// | Pipe 0 | Pipe 2
|
|
// | |
|
|
// | |
|
|
// | Pipe 3 v
|
|
// * <---------- *
|
|
// Proc 3 Proc 2
|
|
//
|
|
// This exercises context switching through multiple processes.
|
|
void BM_ProcessSwitch(benchmark::State& state) {
|
|
// Code below assumes there are at least two processes.
|
|
const int num_processes = state.range(0);
|
|
ASSERT_GE(num_processes, 2);
|
|
|
|
std::vector<pid_t> children;
|
|
auto child_cleanup = Cleanup([&] {
|
|
for (const pid_t child : children) {
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceeds());
|
|
EXPECT_TRUE(WIFEXITED(status));
|
|
EXPECT_EQ(0, WEXITSTATUS(status));
|
|
}
|
|
});
|
|
|
|
// Must come after children, as the FDs must be closed before the children
|
|
// will exit.
|
|
std::vector<FileDescriptor> read_fds;
|
|
std::vector<FileDescriptor> write_fds;
|
|
|
|
for (int i = 0; i < num_processes; i++) {
|
|
int fds[2];
|
|
ASSERT_THAT(pipe(fds), SyscallSucceeds());
|
|
read_fds.emplace_back(fds[0]);
|
|
write_fds.emplace_back(fds[1]);
|
|
}
|
|
|
|
// This process is one of the processes in the loop. It will be considered
|
|
// index 0.
|
|
for (int i = 1; i < num_processes; i++) {
|
|
// Read from current pipe index, write to next.
|
|
const int read_index = i;
|
|
const int read_fd = read_fds[read_index].get();
|
|
|
|
const int write_index = (i + 1) % num_processes;
|
|
const int write_fd = write_fds[write_index].get();
|
|
|
|
// std::vector isn't safe to use from the fork child.
|
|
FileDescriptor* read_array = read_fds.data();
|
|
FileDescriptor* write_array = write_fds.data();
|
|
|
|
pid_t child = fork();
|
|
if (!child) {
|
|
// Close all other FDs.
|
|
for (int j = 0; j < num_processes; j++) {
|
|
if (j != read_index) {
|
|
read_array[j].reset();
|
|
}
|
|
if (j != write_index) {
|
|
write_array[j].reset();
|
|
}
|
|
}
|
|
|
|
SwitchChild(read_fd, write_fd);
|
|
_exit(0);
|
|
}
|
|
ASSERT_THAT(child, SyscallSucceeds());
|
|
children.push_back(child);
|
|
}
|
|
|
|
// Read from current pipe index (0), write to next (1).
|
|
const int read_index = 0;
|
|
const int read_fd = read_fds[read_index].get();
|
|
|
|
const int write_index = 1;
|
|
const int write_fd = write_fds[write_index].get();
|
|
|
|
// Kick start the loop.
|
|
char buf = 'a';
|
|
ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
|
|
for (auto _ : state) {
|
|
ASSERT_THAT(ReadFd(read_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
}
|
|
}
|
|
|
|
BENCHMARK(BM_ProcessSwitch)->Range(2, 16)->UseRealTime();
|
|
|
|
// Equivalent to BM_ThreadSwitch using threads instead of processes.
|
|
void BM_ThreadSwitch(benchmark::State& state) {
|
|
// Code below assumes there are at least two threads.
|
|
const int num_threads = state.range(0);
|
|
ASSERT_GE(num_threads, 2);
|
|
|
|
// Must come after threads, as the FDs must be closed before the children
|
|
// will exit.
|
|
std::vector<std::unique_ptr<ScopedThread>> threads;
|
|
std::vector<FileDescriptor> read_fds;
|
|
std::vector<FileDescriptor> write_fds;
|
|
|
|
for (int i = 0; i < num_threads; i++) {
|
|
int fds[2];
|
|
ASSERT_THAT(pipe(fds), SyscallSucceeds());
|
|
read_fds.emplace_back(fds[0]);
|
|
write_fds.emplace_back(fds[1]);
|
|
}
|
|
|
|
// This thread is one of the threads in the loop. It will be considered
|
|
// index 0.
|
|
for (int i = 1; i < num_threads; i++) {
|
|
// Read from current pipe index, write to next.
|
|
//
|
|
// Transfer ownership of the FDs to the thread.
|
|
const int read_index = i;
|
|
const int read_fd = read_fds[read_index].release();
|
|
|
|
const int write_index = (i + 1) % num_threads;
|
|
const int write_fd = write_fds[write_index].release();
|
|
|
|
threads.emplace_back(std::make_unique<ScopedThread>([read_fd, write_fd] {
|
|
FileDescriptor read(read_fd);
|
|
FileDescriptor write(write_fd);
|
|
SwitchChild(read.get(), write.get());
|
|
}));
|
|
}
|
|
|
|
// Read from current pipe index (0), write to next (1).
|
|
const int read_index = 0;
|
|
const int read_fd = read_fds[read_index].get();
|
|
|
|
const int write_index = 1;
|
|
const int write_fd = write_fds[write_index].get();
|
|
|
|
// Kick start the loop.
|
|
char buf = 'a';
|
|
ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
|
|
for (auto _ : state) {
|
|
ASSERT_THAT(ReadFd(read_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
ASSERT_THAT(WriteFd(write_fd, &buf, 1), SyscallSucceedsWithValue(1));
|
|
}
|
|
|
|
// The two FDs still owned by this thread are closed, causing the next thread
|
|
// to exit its loop and close its FDs, and so on until all threads exit.
|
|
}
|
|
|
|
BENCHMARK(BM_ThreadSwitch)->Range(2, 16)->UseRealTime();
|
|
|
|
void BM_ThreadStart(benchmark::State& state) {
|
|
const int num_threads = state.range(0);
|
|
|
|
for (auto _ : state) {
|
|
state.PauseTiming();
|
|
|
|
auto barrier = new absl::Barrier(num_threads + 1);
|
|
std::vector<std::unique_ptr<ScopedThread>> threads;
|
|
|
|
state.ResumeTiming();
|
|
|
|
for (size_t i = 0; i < num_threads; ++i) {
|
|
threads.emplace_back(std::make_unique<ScopedThread>([barrier] {
|
|
if (barrier->Block()) {
|
|
delete barrier;
|
|
}
|
|
}));
|
|
}
|
|
|
|
if (barrier->Block()) {
|
|
delete barrier;
|
|
}
|
|
|
|
state.PauseTiming();
|
|
|
|
for (const auto& thread : threads) {
|
|
thread->Join();
|
|
}
|
|
|
|
state.ResumeTiming();
|
|
}
|
|
}
|
|
|
|
BENCHMARK(BM_ThreadStart)->Range(1, 2048)->UseRealTime();
|
|
|
|
// Benchmark the complete fork + exit + wait.
|
|
void BM_ProcessLifecycle(benchmark::State& state) {
|
|
const int num_procs = state.range(0);
|
|
|
|
std::vector<pid_t> pids(num_procs);
|
|
for (auto _ : state) {
|
|
for (size_t i = 0; i < num_procs; ++i) {
|
|
int pid = fork();
|
|
if (pid == 0) {
|
|
_exit(0);
|
|
}
|
|
ASSERT_THAT(pid, SyscallSucceeds());
|
|
pids[i] = pid;
|
|
}
|
|
|
|
for (const int pid : pids) {
|
|
ASSERT_THAT(RetryEINTR(waitpid)(pid, nullptr, 0),
|
|
SyscallSucceedsWithValue(pid));
|
|
}
|
|
}
|
|
}
|
|
|
|
BENCHMARK(BM_ProcessLifecycle)->Range(1, 512)->UseRealTime();
|
|
|
|
} // namespace
|
|
|
|
} // namespace testing
|
|
} // namespace gvisor
|