// 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 #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 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 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 read_fds; std::vector 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> threads; std::vector read_fds; std::vector 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([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> threads; state.ResumeTiming(); for (size_t i = 0; i < num_threads; ++i) { threads.emplace_back(std::make_unique([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 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