Files
KPN/tests/CMakeLists.txt
T
dtourolleandClaude Opus 5 a3f61fcb3c perf: make the benchmark able to answer the question, then ask it
PERF_PLAN phase 0, plus B1/B2 which turned out to cost seconds rather
than the minutes budgeted for them. No library code is touched.

The harness could not support the conclusions drawn from it. items_for()
shrank the sample as work per item grew, so exactly the rows under
investigation -- chain-16 and chain-32 -- ran 50 to 200 items and swung
4-8x between passes. Sample size now derives from a time budget with a
floor, using work_us * stages / units as the per-item cost. The old
ladder's error was treating depth as a throughput cost: past the core
count it is, below it depth costs only latency.

Rows now report median of N repetitions after a discarded warm-up, with
IQR and range, so an unreliable row says so instead of being averaged
into a table. The CSV header records nproc, governor and AC state, which
immediately caught this laptop running on battery under powersave.

A3 needed no experiment in the end: ru_nivcsw and ru_nvcsw are captured
around every timed region and reported per item, so involuntary switches
against depth is a column rather than a run.

bench_dispatch answers B1 and B2 without instrumenting the scheduler.
Sleeping is inferred from ru_nvcsw, since a thread blocking on a
condition variable books a voluntary context switch. B1: a ThreadPool(1)
dispatch is 291 ns null, 466 ns with a payload, against the ~290 ns the
plan estimated -- so the abandon criterion is not met and workstream B
stays alive.

B2's answer is not the one the question expected. It is not whether
workers sleep but which pool: on a private ThreadPool(1) the worker never
sleeps, because it resubmits into its own queue and finds the work
already there; on any pool of two or more it sleeps exactly once per
task, because submit() round-robins to a different worker, which is
asleep. That is the whole 466 ns to 1.7 us difference, and it inverts
half the plan. B5 (bounded spin) buys nothing in the default
configuration, and A5 must not make a shared pool the default until the
wake cost is fixed, or every graph that already fits its cores gets 3-4x
worse per dispatch.

G1 lands as tests/soak_wedge.cpp, superseding benchmarks/repro_wedge.cpp,
which was never wired into any build. Always compiled so it cannot rot;
its CTest cases register only under -DKPN_ENABLE_SOAK_TESTS=ON, so the
default test count is unchanged. A wedge is a hang, and a hang under
CTest is an unattributable timeout, so it carries a watchdog that aborts
naming the iteration and phase.

Phase 0's gate is not yet cleared: the acceptance run belongs on the
reference machine, not here. A 3-pass check lands every row within 0.7%
against the 4-8x swings described above, which is encouraging and is not
the same thing.

Provisional, recorded so it can be checked: chain-16 came out 6% behind
TBB rather than 28.5%. If that survives a proper run, the deep-chain
deficit is substantially an artefact of the N=200 rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 12:13:11 +02:00

112 lines
5.2 KiB
CMake

cmake_minimum_required(VERSION 3.21)
# ── Catch2 ────────────────────────────────────────────────────────────────────
find_package(Catch2 3 QUIET)
if(NOT Catch2_FOUND)
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.3
)
FetchContent_MakeAvailable(Catch2)
endif()
# ── Google Test ───────────────────────────────────────────────────────────────
find_package(GTest QUIET)
if(NOT GTest_FOUND)
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
endif()
# ── Test executable ───────────────────────────────────────────────────────────
add_executable(kpn_tests
test_fixed_string.cpp
test_traits.cpp
test_channel.cpp
test_node.cpp
test_network.cpp
test_static_network.cpp
test_shared_resource.cpp
test_pool_node.cpp
test_backpressure_deadlock.cpp
test_scheduler.cpp
test_submit_gate.cpp
)
target_link_libraries(kpn_tests PRIVATE
kpn
Catch2::Catch2WithMain
GTest::gtest
)
# ── Channel stress suite (separate executable) ────────────────────────────────
# Contended SPSC tests for the lock-free Channel<T>. Kept out of kpn_tests
# because each case runs many reps / tens of thousands of items and is slow.
# Most valuable under -DKPN_SANITIZER=thread, but correct (and run) without it.
add_executable(kpn_tests_stress test_channel_stress.cpp)
target_link_libraries(kpn_tests_stress PRIVATE kpn Catch2::Catch2WithMain)
# ── Wedge soak (PERF_PLAN G1) ─────────────────────────────────────────────────
# Long-running end-to-end loop over the configurations that historically wedged.
# Always built, so it cannot rot, but its CTest cases are registered only under
# -DKPN_ENABLE_SOAK_TESTS=ON: they run for minutes and would otherwise dominate
# every `ctest` invocation. Performance work runs it before and after a change:
#
# cmake -B build -DKPN_ENABLE_SOAK_TESTS=ON -DKPN_SOAK_ITERS=50000
# cmake --build build --target kpn_soak_wedge
# ctest --test-dir build -L soak
#
# The binary self-diagnoses: an iteration that stops making progress trips a
# watchdog that aborts naming the iteration and phase, rather than hanging.
add_executable(kpn_soak_wedge soak_wedge.cpp)
target_link_libraries(kpn_soak_wedge PRIVATE kpn)
target_compile_options(kpn_soak_wedge PRIVATE -O2)
option(KPN_ENABLE_SOAK_TESTS "Register the wedge soak cases with CTest" OFF)
set(KPN_SOAK_ITERS 5000 CACHE STRING "Iterations per wedge soak case")
# ── Sanitizer flags ───────────────────────────────────────────────────────────
# kpn_sanitizer_flags() is defined in the top-level CMakeLists and is a no-op
# unless -DKPN_SANITIZER=... is set. Sanitizer must be on both compile and link.
kpn_sanitizer_flags(_kpn_san)
if(_kpn_san)
foreach(_t kpn_tests kpn_tests_stress kpn_soak_wedge)
target_compile_options(${_t} PRIVATE ${_kpn_san})
target_link_options(${_t} PRIVATE ${_kpn_san})
endforeach()
endif()
include(CTest)
include(Catch)
# DISCOVERY_MODE PRE_TEST defers test enumeration to `ctest` run time. The
# default (POST_BUILD) runs each test binary during the build to list its
# cases — which fails a sanitizer build: a TSan/ASan binary needs a fixed
# address-space layout and aborts on startup ("unexpected memory mapping")
# under the container's ASLR, breaking the build before any test runs. The
# tsan.yaml job invokes the binaries directly (not via ctest), so deferring
# discovery costs nothing there and keeps `ctest` working for normal builds.
catch_discover_tests(kpn_tests DISCOVERY_MODE PRE_TEST)
# Register the stress suite under its own label so CI can run / time it
# separately from the fast unit tests.
catch_discover_tests(kpn_tests_stress DISCOVERY_MODE PRE_TEST PROPERTIES LABELS "stress")
if(KPN_ENABLE_SOAK_TESTS)
# pool: the configuration the August wedges were reproduced on.
add_test(NAME soak.wedge.pool
COMMAND kpn_soak_wedge --mode=pool --depth=4 --threads=4
--items=1000 --work-us=10 --iters=${KPN_SOAK_ITERS})
# private: one pool per node — the model workstream A would change.
add_test(NAME soak.wedge.private
COMMAND kpn_soak_wedge --mode=priv --depth=8
--items=1000 --work-us=10 --iters=${KPN_SOAK_ITERS})
set_tests_properties(soak.wedge.pool soak.wedge.private PROPERTIES
LABELS "soak" TIMEOUT 3600)
endif()