Measure the library: perf suites, a recorded baseline, and targets
tests/perf.c and tests/perf_render.c drive the hot paths hard enough to time them -- pool acquire and release at both ends of the scan, the registry, the state-to-sprite lookup, the per-actor update and render, the physics sweep, an all-pairs collision sweep, the JSON accessors, path resolution, the drawing primitives, text, asset loading, a screenful of tiles, and a whole frame through akgl_game_update. They are registered like any other suite but carry the `perf` label, so `ctest -L perf` runs only them and `-LE perf` leaves them out. Every measurement is held to a budget at roughly ten times the recorded baseline, enforced only in an optimized build at full scale. Three things had to be got right before the numbers meant anything, and each was wrong first: - No error checking inside the clock. PASS and CATCH call akerr_valid_error_address, which walks AKERR_ARRAY_ERROR -- more work than several of the calls being measured. - Flush the renderer before stopping it. SDL batches, so the first version measured queueing, reported a tilemap frame 250 times faster than it is, and paid the real cost at teardown inside SDL_DestroyTexture. - A drawing benchmark needs a raw-SDL control doing the same pixel work with the same access pattern. With one, akgl_tilemap_draw turns out to cost 0.2% of the frame it appeared to own: 16.26 ms against a control's 16.23 ms for the same 1200 blits. The rasterizer is the frame. PERFORMANCE.md records the baseline, the frame budget it adds up to, and what the numbers say -- including that the string pool's acquire is 64x slower full than empty because it is a megabyte of PATH_MAX buffers, that a handled missing-sprite condition costs nine times the update it replaces, that text rasterizes and throws away a texture on every call, and that 28 MB of the library's BSS is one akgl_Tilemap. TODO.md gains a Performance section: five defects the stress tests found that the unit suites do not reach (a tilemap load leaking five pooled strings, two JSON accessors that turn pool exhaustion into a segfault rather than AKGL_ERR_HEAP, akgl_game_update crashing without akgl_game_init, and its actor update sweep running once per tilemap layer), and eighteen targets with today's number and whether it is met. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -202,11 +202,27 @@ set(AKGL_TEST_SUITES
|
||||
version
|
||||
)
|
||||
|
||||
# The performance suites are built and registered exactly like the unit suites,
|
||||
# but they are benchmarks: they drive hot paths for millions of iterations, print
|
||||
# a table of nanoseconds per operation, and fail only when a measurement exceeds
|
||||
# a budget set at roughly ten times the recorded baseline. They carry the `perf`
|
||||
# label, so `ctest -L perf` runs only them and `ctest -LE perf` leaves them out
|
||||
# of an ordinary run, and they get a much longer timeout for obvious reasons.
|
||||
#
|
||||
# Set AKGL_BENCH_SCALE in the environment to change how long they run --
|
||||
# `AKGL_BENCH_SCALE=0.1 ctest -L perf` for a tenth of the iterations. Below 1.0
|
||||
# the budgets are measured and reported but not enforced, because a short run is
|
||||
# a noisy one.
|
||||
set(AKGL_PERF_SUITES
|
||||
perf
|
||||
perf_render
|
||||
)
|
||||
|
||||
# The executables carry an akgl_ prefix but the CTest names do not: a vendored
|
||||
# dependency is free to ship its own tests/version.c, and libakstdlib now does.
|
||||
# Its target is created by add_subdirectory even though EXCLUDE_FROM_ALL keeps
|
||||
# it from being built, so an unprefixed test_version here is a configure error.
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
||||
add_executable(akgl_test_${suite} tests/${suite}.c)
|
||||
add_test(NAME ${suite} COMMAND akgl_test_${suite})
|
||||
endforeach()
|
||||
@@ -218,6 +234,14 @@ set_tests_properties(
|
||||
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 30
|
||||
)
|
||||
|
||||
set_tests_properties(
|
||||
${AKGL_PERF_SUITES}
|
||||
PROPERTIES
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
|
||||
TIMEOUT 900
|
||||
LABELS perf
|
||||
)
|
||||
|
||||
# Specify include directories for the library's headers (if applicable)
|
||||
target_include_directories(akgl PUBLIC
|
||||
include/
|
||||
@@ -243,7 +267,9 @@ if(AKGL_COVERAGE)
|
||||
)
|
||||
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP akgl_coverage)
|
||||
# Any suite missing from this list runs outside the fixture and has its
|
||||
# counters discarded by coverage_reset.
|
||||
# counters discarded by coverage_reset. The perf suites are deliberately
|
||||
# outside it: an instrumented -O0 build measures gcov, not libakgl, and the
|
||||
# lines they cover are covered by the unit suites anyway.
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES} semver_unit
|
||||
PROPERTIES FIXTURES_REQUIRED akgl_coverage
|
||||
@@ -278,7 +304,7 @@ target_link_libraries(akgl
|
||||
jansson::jansson
|
||||
)
|
||||
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
||||
target_link_libraries(akgl_test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
||||
target_include_directories(akgl_test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||
endforeach()
|
||||
@@ -300,7 +326,7 @@ if(AKGL_VENDORED_DEPENDENCIES)
|
||||
"$<TARGET_FILE_DIR:akerror::akerror>"
|
||||
"$<TARGET_FILE_DIR:akstdlib::akstdlib>"
|
||||
)
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES)
|
||||
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
||||
set_target_properties(akgl_test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
endforeach()
|
||||
set_target_properties(charviewer akgl PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||
@@ -324,13 +350,13 @@ if(AKGL_VENDORED_DEPENDENCIES)
|
||||
list(APPEND AKGL_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
|
||||
endforeach()
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES}
|
||||
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
||||
PROPERTIES ENVIRONMENT_MODIFICATION "${AKGL_TEST_ENV_MOD}"
|
||||
)
|
||||
else()
|
||||
string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
||||
set_tests_properties(
|
||||
${AKGL_TEST_SUITES}
|
||||
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
||||
PROPERTIES ENVIRONMENT "LD_LIBRARY_PATH=${AKGL_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
|
||||
)
|
||||
endif()
|
||||
|
||||
Reference in New Issue
Block a user