159 lines
5.1 KiB
C++
159 lines
5.1 KiB
C++
/*
|
|
* BenchNoThrowOnCancel.cpp
|
|
*
|
|
* This source file is part of the FoundationDB open source project
|
|
*
|
|
* Copyright 2013-2026 Apple Inc. and the FoundationDB project 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 "benchmark/benchmark.h"
|
|
|
|
#include "flow/ThreadHelper.h"
|
|
#include "flow/flow.h"
|
|
|
|
#include <cstdint>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
struct CleanupCounter {
|
|
uint64_t* cleanupCount;
|
|
~CleanupCounter() { ++*cleanupCount; }
|
|
};
|
|
|
|
// Baseline: cancellation resumes the coroutine through actor_cancelled(), so the
|
|
// catch count should match the number of cancelled futures.
|
|
Future<Void> cancelWithThrow(Future<Void> signal, uint64_t* cleanupCount, uint64_t* caughtCount) {
|
|
CleanupCounter cleanup{ cleanupCount };
|
|
try {
|
|
co_await signal;
|
|
} catch (Error& e) {
|
|
ASSERT_EQ(e.code(), error_code_actor_cancelled);
|
|
++*caughtCount;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
// Treatment: cancellation destroys the frame directly. The catch block is kept
|
|
// as a sentinel to prove that RAII cleanup ran without exception unwinding.
|
|
Future<Void> cancelWithoutThrow(Future<Void> signal,
|
|
uint64_t* cleanupCount,
|
|
uint64_t* caughtCount,
|
|
NoThrowOnCancel = {}) {
|
|
CleanupCounter cleanup{ cleanupCount };
|
|
try {
|
|
co_await signal;
|
|
} catch (Error& e) {
|
|
ASSERT_EQ(e.code(), error_code_actor_cancelled);
|
|
++*caughtCount;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
enum class CancelImpl { Throwing, NoThrow };
|
|
enum class CancelScenario { ConstructAndCancel, BatchCancel };
|
|
|
|
template <CancelImpl Impl>
|
|
Future<Void> makeCancelFuture(Future<Void> signal, uint64_t* cleanupCount, uint64_t* caughtCount) {
|
|
if constexpr (Impl == CancelImpl::Throwing) {
|
|
return cancelWithThrow(std::move(signal), cleanupCount, caughtCount);
|
|
} else {
|
|
return cancelWithoutThrow(std::move(signal), cleanupCount, caughtCount);
|
|
}
|
|
}
|
|
|
|
template <CancelImpl Impl, CancelScenario Scenario>
|
|
Future<Void> benchNoThrowOnCancelActor(benchmark::State* state) {
|
|
Promise<Void> signal;
|
|
uint64_t cleanupCount = 0;
|
|
uint64_t caughtCount = 0;
|
|
|
|
if constexpr (Scenario == CancelScenario::ConstructAndCancel) {
|
|
while (state->KeepRunning()) {
|
|
Future<Void> f = makeCancelFuture<Impl>(signal.getFuture(), &cleanupCount, &caughtCount);
|
|
ASSERT(!f.isReady());
|
|
benchmark::DoNotOptimize(f);
|
|
f.cancel();
|
|
benchmark::DoNotOptimize(f);
|
|
benchmark::ClobberMemory();
|
|
}
|
|
} else {
|
|
const int batchSize = state->range(0);
|
|
std::vector<Future<Void>> futures;
|
|
futures.reserve(batchSize);
|
|
for (auto _ : *state) {
|
|
benchmark::DoNotOptimize(_);
|
|
// Exclude construction from this scenario so the timed region focuses
|
|
// on the cancellation path itself.
|
|
state->PauseTiming();
|
|
futures.clear();
|
|
for (int i = 0; i < batchSize; ++i) {
|
|
futures.push_back(makeCancelFuture<Impl>(signal.getFuture(), &cleanupCount, &caughtCount));
|
|
ASSERT(!futures.back().isReady());
|
|
}
|
|
benchmark::DoNotOptimize(futures.data());
|
|
state->ResumeTiming();
|
|
for (auto& f : futures) {
|
|
f.cancel();
|
|
benchmark::DoNotOptimize(f);
|
|
}
|
|
benchmark::ClobberMemory();
|
|
state->PauseTiming();
|
|
for (auto const& f : futures) {
|
|
ASSERT(f.isReady() && f.isError() && f.getError().code() == error_code_actor_cancelled);
|
|
}
|
|
}
|
|
state->ResumeTiming();
|
|
}
|
|
|
|
uint64_t cancels = state->iterations();
|
|
if constexpr (Scenario == CancelScenario::BatchCancel) {
|
|
cancels *= static_cast<uint64_t>(state->range(0));
|
|
}
|
|
ASSERT_EQ(cleanupCount, cancels);
|
|
if constexpr (Impl == CancelImpl::Throwing) {
|
|
ASSERT_EQ(caughtCount, cancels);
|
|
} else {
|
|
ASSERT_EQ(caughtCount, 0);
|
|
}
|
|
state->SetItemsProcessed(static_cast<int64_t>(cancels));
|
|
co_return;
|
|
}
|
|
|
|
template <CancelImpl Impl, CancelScenario Scenario>
|
|
void benchNoThrowOnCancel(benchmark::State& state) {
|
|
onMainThread([&state] { return benchNoThrowOnCancelActor<Impl, Scenario>(&state); }).blockUntilReady();
|
|
}
|
|
|
|
BENCHMARK_TEMPLATE(benchNoThrowOnCancel, CancelImpl::Throwing, CancelScenario::ConstructAndCancel)
|
|
->Name("NoThrowOnCancel/throwing/construct_and_cancel")
|
|
->ReportAggregatesOnly(true);
|
|
|
|
BENCHMARK_TEMPLATE(benchNoThrowOnCancel, CancelImpl::NoThrow, CancelScenario::ConstructAndCancel)
|
|
->Name("NoThrowOnCancel/no_throw/construct_and_cancel")
|
|
->ReportAggregatesOnly(true);
|
|
|
|
BENCHMARK_TEMPLATE(benchNoThrowOnCancel, CancelImpl::Throwing, CancelScenario::BatchCancel)
|
|
->Name("NoThrowOnCancel/throwing/batch_cancel")
|
|
->Arg(1024)
|
|
->ReportAggregatesOnly(true);
|
|
|
|
BENCHMARK_TEMPLATE(benchNoThrowOnCancel, CancelImpl::NoThrow, CancelScenario::BatchCancel)
|
|
->Name("NoThrowOnCancel/no_throw/batch_cancel")
|
|
->Arg(1024)
|
|
->ReportAggregatesOnly(true);
|
|
|
|
} // namespace
|