Files
libakgl/scripts/memcheck.sh

121 lines
5.3 KiB
Bash
Raw Normal View History

Check memory with the suites that already exist `cmake --build build --target memcheck` runs every registered CTest suite under valgrind. There are no new test programs, and there should not be any: tests/benchutil.h notices valgrind in LD_PRELOAD and drops the benchmark scale to 0.0005, which turns the perf suites into the broadest path coverage in the tree at a cost valgrind can survive. A benchmark walks one path a hundred thousand times; a leak check wants every path walked once. Same binaries, one flag apart, and the whole run is about thirty seconds. scripts/memcheck.sh wraps `ctest -T memcheck` because that command records defects and still exits 0, which cannot gate anything. It also forces the headless drivers, so the vendor GPU stack is never loaded -- that removes thousands of unfixable findings inside amdgpu_dri.so without suppressing anything, and it is what the suites are written for anyway. Only definite losses, invalid accesses and uninitialised reads count; "still reachable" is what SDL and FreeType keep for the process lifetime and says nothing about this library. The three suppressions in scripts/valgrind.supp are each a decision that a finding belongs to somebody else. Six defects, all filed in TODO.md under "Memory checking", the first of which is the one that matters: not one of the four json_load_file calls in src/ is ever matched by a json_decref, so every asset load abandons its parsed document -- 1.5 KB per sprite, 2.1 KB per character, 9 KB per load of the 2x2 fixture map, and on the order of a megabyte for a real one. A game that reloads a level on death leaks a level's worth of JSON every time. akgl_get_property also reads up to 4 KiB past the end of every property value it copies, and the savegame name tables read past the end of every registry key and write what they find into the file. tests/util.c zeroes three fixtures it used to leave as stack garbage. The library was never at fault there -- the tests handed it uninitialised floats and then made one real call with them -- but sixteen findings of noise in a new gate is how a gate gets ignored. The unit suites' TIMEOUT goes from 30 to 300 because CTest applies the same property to the checked run, where everything is twenty times slower. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 14:59:57 -04:00
#!/bin/bash
#
# Run the existing CTest suites under valgrind and report what leaked.
#
# scripts/memcheck.sh check every suite in ./build
# scripts/memcheck.sh -R tilemap check only the suites matching a regex
# scripts/memcheck.sh -LE perf skip the perf suites
#
# Anything after the build directory is handed straight to ctest, so every
# selection flag ctest understands works here.
#
# Environment:
# AKGL_BUILD_DIR build tree to check (default: build)
# AKGL_MEMCHECK_LOG directory for the per-suite valgrind logs
# (default: $AKGL_BUILD_DIR/Testing/Temporary)
#
# Exit status: 0 when valgrind found nothing, 1 when it found something, 2 on a
# usage or environment error. This is the part `ctest -T memcheck` will not do
# on its own -- it records defects and still exits 0, which is no use as a gate.
set -eu
AKGL_BUILD_DIR="${AKGL_BUILD_DIR:-build}"
function memcheck_die()
{
echo "memcheck: $*" >&2
exit 2
}
# The suite runs headless on purpose. Loading the real GPU stack costs thousands
# of unfixable findings inside the vendor driver, and none of them are libakgl's.
# A suite that sets these hints itself is unaffected; the ones that do not --
# tests/tilemap.c and tests/sprite.c -- pick them up from here.
export SDL_VIDEO_DRIVER="${SDL_VIDEO_DRIVER:-dummy}"
export SDL_RENDER_DRIVER="${SDL_RENDER_DRIVER:-software}"
export SDL_AUDIO_DRIVER="${SDL_AUDIO_DRIVER:-dummy}"
command -v valgrind >/dev/null 2>&1 || memcheck_die "valgrind not found"
command -v ctest >/dev/null 2>&1 || memcheck_die "ctest not found"
[ -f "$AKGL_BUILD_DIR/CMakeCache.txt" ] || memcheck_die "$AKGL_BUILD_DIR is not a configured build tree"
AKGL_MEMCHECK_LOG="${AKGL_MEMCHECK_LOG:-$AKGL_BUILD_DIR/Testing/Temporary}"
# Old logs would be counted as this run's findings. CTest numbers them by test
# id, so a run narrowed with -R leaves the others behind.
rm -f "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log
# The run itself is allowed to fail: a suite that valgrind makes slow enough to
# trip an assertion still produced a log worth reading, and the log is what
# decides the exit status below.
#
# docs_examples and docs_screenshots are excluded, and the exclusion deserves
# its reasons stated: those two tests are *shell scripts* that drive gcc and
# run the compiled snippets as child processes. Valgrind wraps the test
# command, does not trace children, and so spends the whole test memchecking
# /bin/bash -- 413 of bash's own by-design leaks on the first CI run that got
# this far, and not one byte of libakgl. The snippet programs themselves are
# ordinary akgl consumers whose code paths the suites and example games
# already cover below, under valgrind, as real binaries. A caller's own -E
# overrides this one (ctest takes the last), which is fine: a narrowed run
# chose its scope on purpose.
Check memory with the suites that already exist `cmake --build build --target memcheck` runs every registered CTest suite under valgrind. There are no new test programs, and there should not be any: tests/benchutil.h notices valgrind in LD_PRELOAD and drops the benchmark scale to 0.0005, which turns the perf suites into the broadest path coverage in the tree at a cost valgrind can survive. A benchmark walks one path a hundred thousand times; a leak check wants every path walked once. Same binaries, one flag apart, and the whole run is about thirty seconds. scripts/memcheck.sh wraps `ctest -T memcheck` because that command records defects and still exits 0, which cannot gate anything. It also forces the headless drivers, so the vendor GPU stack is never loaded -- that removes thousands of unfixable findings inside amdgpu_dri.so without suppressing anything, and it is what the suites are written for anyway. Only definite losses, invalid accesses and uninitialised reads count; "still reachable" is what SDL and FreeType keep for the process lifetime and says nothing about this library. The three suppressions in scripts/valgrind.supp are each a decision that a finding belongs to somebody else. Six defects, all filed in TODO.md under "Memory checking", the first of which is the one that matters: not one of the four json_load_file calls in src/ is ever matched by a json_decref, so every asset load abandons its parsed document -- 1.5 KB per sprite, 2.1 KB per character, 9 KB per load of the 2x2 fixture map, and on the order of a megabyte for a real one. A game that reloads a level on death leaks a level's worth of JSON every time. akgl_get_property also reads up to 4 KiB past the end of every property value it copies, and the savegame name tables read past the end of every registry key and write what they find into the file. tests/util.c zeroes three fixtures it used to leave as stack garbage. The library was never at fault there -- the tests handed it uninitialised floats and then made one real call with them -- but sixteen findings of noise in a new gate is how a gate gets ignored. The unit suites' TIMEOUT goes from 30 to 300 because CTest applies the same property to the checked run, where everything is twenty times slower. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 14:59:57 -04:00
set +e
ctest --test-dir "$AKGL_BUILD_DIR" -T memcheck --output-on-failure \
-E '^(docs_examples|docs_screenshots)$' "$@"
Check memory with the suites that already exist `cmake --build build --target memcheck` runs every registered CTest suite under valgrind. There are no new test programs, and there should not be any: tests/benchutil.h notices valgrind in LD_PRELOAD and drops the benchmark scale to 0.0005, which turns the perf suites into the broadest path coverage in the tree at a cost valgrind can survive. A benchmark walks one path a hundred thousand times; a leak check wants every path walked once. Same binaries, one flag apart, and the whole run is about thirty seconds. scripts/memcheck.sh wraps `ctest -T memcheck` because that command records defects and still exits 0, which cannot gate anything. It also forces the headless drivers, so the vendor GPU stack is never loaded -- that removes thousands of unfixable findings inside amdgpu_dri.so without suppressing anything, and it is what the suites are written for anyway. Only definite losses, invalid accesses and uninitialised reads count; "still reachable" is what SDL and FreeType keep for the process lifetime and says nothing about this library. The three suppressions in scripts/valgrind.supp are each a decision that a finding belongs to somebody else. Six defects, all filed in TODO.md under "Memory checking", the first of which is the one that matters: not one of the four json_load_file calls in src/ is ever matched by a json_decref, so every asset load abandons its parsed document -- 1.5 KB per sprite, 2.1 KB per character, 9 KB per load of the 2x2 fixture map, and on the order of a megabyte for a real one. A game that reloads a level on death leaks a level's worth of JSON every time. akgl_get_property also reads up to 4 KiB past the end of every property value it copies, and the savegame name tables read past the end of every registry key and write what they find into the file. tests/util.c zeroes three fixtures it used to leave as stack garbage. The library was never at fault there -- the tests handed it uninitialised floats and then made one real call with them -- but sixteen findings of noise in a new gate is how a gate gets ignored. The unit suites' TIMEOUT goes from 30 to 300 because CTest applies the same property to the checked run, where everything is twenty times slower. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 14:59:57 -04:00
AKGL_CTEST_STATUS=$?
set -e
# What counts as a finding. "Definitely lost" is a leak nobody can argue with;
# the invalid-access lines are memcheck telling us the library read or wrote
# outside an allocation, which is worse than a leak and easier to fix.
#
# "are definitely lost", not "definitely lost": the LEAK SUMMARY block ends every
# clean log with "definitely lost: 0 bytes in 0 blocks", and matching that
# reported a finding in all twenty-three suites, including the ones that never
# call malloc.
AKGL_MEMCHECK_PATTERN='are definitely lost|Invalid read|Invalid write|Invalid free|Mismatched free|Source and destination overlap|depends on uninitialised|Use of uninitialised'
total=0
echo
echo "== valgrind findings by suite =="
for log in "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log; do
[ -f "$log" ] || continue
# CTest writes the test name into the log's first line as the command it ran.
suite=$(grep -m1 -oE 'akgl_test_[a-z_]+' "$log" || basename "$log")
count=$(grep -cE "$AKGL_MEMCHECK_PATTERN" "$log" || true)
total=$((total + count))
if [ "$count" -gt 0 ]; then
printf '%-24s %3d finding(s) %s\n' "$suite" "$count" "$log"
fi
done
if [ "$total" -eq 0 ]; then
echo "none"
echo
# A clean valgrind run and a failing test are different things: a suite can
# assert its way to a non-zero exit without leaking a byte, and saying
# "clean" and returning 0 for that would hide it.
if [ "$AKGL_CTEST_STATUS" -ne 0 ]; then
echo "memcheck: valgrind found nothing, but ctest exited $AKGL_CTEST_STATUS -- a suite failed on its own terms" >&2
exit 1
fi
echo "memcheck: clean ($AKGL_MEMCHECK_LOG)"
exit 0
fi
echo
echo "== library frames in those stacks, most frequent first =="
# Keyed on the function name rather than the file, because src/physics.c and
# tests/physics.c are both "physics.c" in a valgrind frame and only one of them
# is the library. Anything a caller can reach is an akgl_ symbol by convention,
# which makes the prefix a usable filter; a static helper appears under whatever
# akgl_ function called it, one frame further down.
grep -hoE 'by 0x[0-9A-Fa-f]+: akgl_[A-Za-z0-9_]+ \([a-z_]+\.c:[0-9]+\)' \
"$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log 2>/dev/null |
sed -E 's/^by 0x[0-9A-Fa-f]+: //' | sort | uniq -c | sort -rn | head -20
echo
echo "memcheck: $total finding(s); full stacks in $AKGL_MEMCHECK_LOG" >&2
exit 1