fix(mem): free the thread locale at exit; a settling gate keeps its latch

Two CI-only lanes went red on 9724d903, both this branch's own doing.

The macOS leak lane: every test passed and LeakSanitizer reported 16 to
101 objects of 1,472 bytes per test process, allocated from
cbm_arena_sprintf. The formatter creates one C locale per thread on macOS
(the vsnprintf locale lock fix) and kept it in a thread-local pointer
only, so every worker thread that ever formatted a name leaked its locale
at exit. A pthread key destructor now frees it when the thread exits; the
main thread keeps its locale until process exit.

The TSan lane on Ubuntu arm: pipeline_backpressure_futile_nap_disengages
counted 8 nap cycles against a bound of 7, the "gate re-paid per pull"
shape the test exists for. The spill shortcut reports "not over" while a
sweep is still bringing memory down, and that reading re-armed the
futility latch, so the next over-budget pull paid a full nap cycle again.
The latch now re-arms only on a genuine under-budget reading; a reading
the shortcut produced is not one.

Verification: the local macOS leak lane (make -f Makefile.cbm test-lsan)
reports zero leaks, 8050 passed / 10 skipped (the CI lane had 16-101
leaked objects per process); the local TSan lane (scripts/test.sh --tsan)
passes 1157 / 9 skipped with the nap-cycle test green (the CI lane had it
at 8 cycles); pipeline, parallel and arena suites twice and lint-ci on the
fixed tree.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-09-14 21:04:16 +02:00
1 parent 9724d90304
commit 9e69cb7d9b
2 files changed
+30 -1

No files matched your search

+21
View File
@@ -18,6 +18,7 @@ enum { ARENA_ALIGN = 7, ARENA_GROW_OK = 1 };
#include <string.h>
#include <stdarg.h>
#if defined(__APPLE__)
#include <pthread.h>
#include <xlocale.h>
#endif
#include <stdio.h>
@@ -144,10 +145,30 @@ char *cbm_arena_strndup(CBMArena *a, const char *s, size_t len) {
* object of its own, so no thread ever waits for another, and the common
* short string is formatted once into a stack buffer. */
#if defined(__APPLE__)
/* One locale object per thread, freed when the thread exits: the object is
* heap memory that the thread-local pointer alone kept, so every worker
* thread that ever formatted a name leaked 1,472 bytes at exit (the macOS
* LSan lane on PR #2202, 16-101 objects per test process). A pthread key
* destructor is the one hook that runs at thread exit for a TLS-held
* resource; the main thread keeps its locale until process exit. */
static _Thread_local locale_t tl_c_locale;
static pthread_key_t tl_c_locale_key;
static pthread_once_t tl_c_locale_once = PTHREAD_ONCE_INIT;
static void arena_c_locale_free(void *loc) {
if (loc) {
freelocale((locale_t)loc);
}
}
static void arena_c_locale_key_init(void) {
(void)pthread_key_create(&tl_c_locale_key, arena_c_locale_free);
}
static locale_t arena_c_locale(void) {
if (!tl_c_locale) {
tl_c_locale = newlocale(LC_ALL_MASK, "C", NULL);
if (tl_c_locale) {
pthread_once(&tl_c_locale_once, arena_c_locale_key_init);
(void)pthread_setspecific(tl_c_locale_key, tl_c_locale);
}
}
return tl_c_locale; /* NULL = the global locale, the pre-fix behaviour */
}
+9 -1
View File
@@ -1020,6 +1020,7 @@ static void extract_worker(int worker_id, void *ctx_ptr) {
pp_spill_enter(ec, "near_budget");
}
}
bool settling = false;
if (over) {
/* Admission control, first response: park what can be parked.
* Only what is still over budget after that -- the floor --
@@ -1030,6 +1031,7 @@ static void extract_worker(int worker_id, void *ctx_ptr) {
over = cbm_mem_over_budget();
if (over && pp_spill_active(ec) && (parked > 0 || pp_spill_work_remains(ec))) {
over = false; /* memory is still on its way down */
settling = true;
}
}
bool futile = atomic_load_explicit(&ec->bp_futile, memory_order_relaxed) != 0;
@@ -1046,7 +1048,13 @@ static void extract_worker(int worker_id, void *ctx_ptr) {
atomic_store_explicit(&ec->bp_futile, 0, memory_order_relaxed);
}
}
} else if (!over && futile) {
} else if (!over && !settling && futile) {
/* Re-arm only on a genuine under-budget reading. A reading the
* spill shortcut produced ("still on its way down") is not
* one: re-arming on it made the next over-budget pull pay a
* full nap cycle again -- the gate re-paid per pull that
* pipeline_backpressure_futile_nap_disengages guards against
* (TSan lane on PR #2202: 8 cycles against a bound of 7). */
atomic_store_explicit(&ec->bp_futile, 0, memory_order_relaxed);
}
}