Release the error context when a path resolves against its root
akgl_path_relative took its ENOENT branch by returning from inside the HANDLE block, which skips the RELEASE_ERROR that FINISH ends with. The handled context was never given back, so one entry of AKERR_ARRAY_ERROR was lost per call and the 129th call hit "Unable to pull an error context from the array!" and exited the process. That branch is not an edge case: every asset path inside a tilemap is resolved relative to the map's own directory, so it is taken several times per map load. A game that loaded fifty levels died in the loader. It was found by a benchmark that loaded the fixture map in a loop, which is the first thing in this tree to call it more than a hundred times. The fallback now runs after FINISH, so the context is released on the way past. tests/util.c resolves AKERR_MAX_ARRAY_ERROR * 2 paths through the branch and asserts the pool is where it started -- against the old code that test does not fail, it terminates the suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
src/util.c
17
src/util.c
@@ -99,6 +99,7 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
|
|||||||
PREPARE_ERROR(e);
|
PREPARE_ERROR(e);
|
||||||
akgl_String *strbuf;
|
akgl_String *strbuf;
|
||||||
char *result;
|
char *result;
|
||||||
|
bool relative_to_root = false;
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
|
FAIL_ZERO_RETURN(e, root, AKERR_NULLPOINTER, "NULL argument");
|
||||||
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
|
FAIL_ZERO_RETURN(e, path, AKERR_NULLPOINTER, "NULL argument");
|
||||||
@@ -115,10 +116,20 @@ akerr_ErrorContext *akgl_path_relative(char *root, char *path, akgl_String *dst)
|
|||||||
IGNORE(akgl_heap_release_string(strbuf));
|
IGNORE(akgl_heap_release_string(strbuf));
|
||||||
} PROCESS(e) {
|
} PROCESS(e) {
|
||||||
} HANDLE(e, ENOENT) {
|
} HANDLE(e, ENOENT) {
|
||||||
// Path is not relative to our current working directory
|
// Path is not relative to our current working directory. Resolve it
|
||||||
// Noop - execution proceeds after the break
|
// against root instead -- but after FINISH, not from in here. Returning
|
||||||
return akgl_path_relative_root(root, path, dst);
|
// from inside a HANDLE block skips the RELEASE_ERROR that FINISH ends
|
||||||
|
// with, so the handled context is never given back to
|
||||||
|
// AKERR_ARRAY_ERROR. That leaks one context per call, and the 129th
|
||||||
|
// call takes the whole process down with "Unable to pull an error
|
||||||
|
// context from the array!". Every map load resolves several paths this
|
||||||
|
// way.
|
||||||
|
relative_to_root = true;
|
||||||
} FINISH(e, true);
|
} FINISH(e, true);
|
||||||
|
|
||||||
|
if ( relative_to_root == true ) {
|
||||||
|
PASS(e, akgl_path_relative_root(root, path, dst));
|
||||||
|
}
|
||||||
SUCCEED_RETURN(e);
|
SUCCEED_RETURN(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
70
tests/util.c
70
tests/util.c
@@ -1,8 +1,30 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
#include <akgl/error.h>
|
#include <akgl/error.h>
|
||||||
|
#include <akgl/heap.h>
|
||||||
|
#include <akgl/staticstring.h>
|
||||||
#include <akgl/util.h>
|
#include <akgl/util.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief How many entries of AKERR_ARRAY_ERROR are currently held by somebody.
|
||||||
|
*
|
||||||
|
* The error contexts are a fixed pool exactly like the object pools: a context
|
||||||
|
* is in use while its reference count is non-zero, and a function that finishes
|
||||||
|
* without releasing one has leaked a slot out of AKERR_MAX_ARRAY_ERROR.
|
||||||
|
*/
|
||||||
|
static int live_error_contexts(void)
|
||||||
|
{
|
||||||
|
int live = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||||
|
if ( AKERR_ARRAY_ERROR[i].refcount != 0 ) {
|
||||||
|
live += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return live;
|
||||||
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
akerr_ErrorContext *test_akgl_rectangle_points_nullpointers(void)
|
||||||
{
|
{
|
||||||
RectanglePoints points;
|
RectanglePoints points;
|
||||||
@@ -306,6 +328,53 @@ akerr_ErrorContext *test_akgl_collide_rectangles_logic(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resolving a path through the root fallback must give its context back.
|
||||||
|
*
|
||||||
|
* akgl_path_relative tries the working directory first and falls back to
|
||||||
|
* resolving against @p root when that reports ENOENT. That fallback used to be
|
||||||
|
* taken by returning from inside the HANDLE block, which skips the
|
||||||
|
* RELEASE_ERROR that FINISH ends with -- so every call down that branch leaked
|
||||||
|
* one entry of AKERR_ARRAY_ERROR, and the 129th call aborted the whole process
|
||||||
|
* with "Unable to pull an error context from the array!". A single map load
|
||||||
|
* resolves several paths this way.
|
||||||
|
*
|
||||||
|
* The loop runs well past AKERR_MAX_ARRAY_ERROR on purpose: at the old
|
||||||
|
* behaviour this test does not fail, it terminates the suite.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *test_akgl_path_relative_releases_contexts(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akgl_String *dst = NULL;
|
||||||
|
int before = 0;
|
||||||
|
int after = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
PASS(errctx, akgl_heap_init());
|
||||||
|
PASS(errctx, akgl_heap_next_string(&dst));
|
||||||
|
before = live_error_contexts();
|
||||||
|
|
||||||
|
for ( i = 0; i < (AKERR_MAX_ARRAY_ERROR * 2); i++ ) {
|
||||||
|
PASS(errctx, akgl_path_relative("assets", "testcharacter.json", dst));
|
||||||
|
}
|
||||||
|
after = live_error_contexts();
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
if ( after != before ) {
|
||||||
|
FAIL_BREAK(
|
||||||
|
errctx,
|
||||||
|
AKGL_ERR_BEHAVIOR,
|
||||||
|
"akgl_path_relative leaked %d error context(s) over %d root-fallback resolutions",
|
||||||
|
(after - before),
|
||||||
|
(AKERR_MAX_ARRAY_ERROR * 2));
|
||||||
|
}
|
||||||
|
} CLEANUP {
|
||||||
|
IGNORE(akgl_heap_release_string(dst));
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -316,6 +385,7 @@ int main(void)
|
|||||||
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
|
CATCH(errctx, test_akgl_collide_point_rectangle_nullpointers());
|
||||||
CATCH(errctx, test_akgl_collide_rectangles_nullpointers());
|
CATCH(errctx, test_akgl_collide_rectangles_nullpointers());
|
||||||
CATCH(errctx, test_akgl_collide_rectangles_logic());
|
CATCH(errctx, test_akgl_collide_rectangles_logic());
|
||||||
|
CATCH(errctx, test_akgl_path_relative_releases_contexts());
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH_NORETURN(errctx);
|
} FINISH_NORETURN(errctx);
|
||||||
|
|||||||
Reference in New Issue
Block a user