mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
The per-file tree-sitter parse was bounded by a 5s WALL-CLOCK budget. Under CI
contention a worker thread can be descheduled long enough for 5s of wall time
to elapse against near-zero CPU, so a small, perfectly parseable file was
abandoned with a false "parse timeout" and yielded ZERO defs. When that file
defined a symbol other files resolve against (e.g. celery.app.Celery.send_task
backing an ASYNC_CALLS edge, or a userpb gRPC client backing GRPC_CALLS), the
cross-file service edge then failed to classify -- surfacing as the flaky
convergence_probe / edge_types_probe service-edge assertions (count 0), a
verdict decided by wall-clock scheduling rather than the input (violates O9,
"no timeout decides a test").
Gate the parse on per-thread CPU time instead, with a generous wall ceiling as
a backstop:
- New portable helper cbm_thread_cpu_time_ns() (compat.c/.h): POSIX/macOS
clock_gettime(CLOCK_THREAD_CPUTIME_ID), Windows GetThreadTimes (kernel+user);
returns 0 on clock failure, degrading safely to the wall ceiling. It is NOT
cbm_clock_gettime, which is wall on every platform (QueryPerformanceCounter
on Windows).
- The progress callback's payload becomes {cpu_deadline_ns, wall_ceiling_ns};
it trips when this thread's CPU time passes the budget OR wall time passes
t0 + budget * 12 (~60s for the 5s budget). Descheduling burns wall, not CPU,
so a starved-but-parseable file survives; a CPU-spinning parse still trips
the CPU deadline; a genuinely stuck parse still trips the wall ceiling
(bounded, no hang).
RED-repro (deterministic, CBM_ENABLE_TEST_SEAMS only): a thread-local wall-clock
offset armed by CBM_TEST_WALL_STALL_ON emulates a descheduled worker (wall
reads budget + 1s ahead while CPU is untouched -- above the old 1x wall budget,
below the ceiling). async_calls_celery_wall_stall_seam arms it for the Celery
DEFINITION file and asserts ASYNC_CALLS >= 1. With the fix the parse completes
and the edge resolves; with the budget reverted to wall-only + a 1x ceiling the
def file is dropped and the edge count is 0.
Verified (macOS, sanitized): edge_types_probe + convergence_probe 107 passed /
0 failed with the fix; 58 passed / 1 failed on revert (only the seam test, at
the ASYNC_CALLS assertion); 107 restored. Regression smoke (extraction
parse_coverage pipeline lang_contract grammar_regression) 698 passed / 0
failed. lint-ci clean (cppcheck + clang-format Homebrew LLVM + NOLINT); the
Windows GetThreadTimes path is compile-checked by CI (not runnable on macOS).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
238 lines
8.9 KiB
C
238 lines
8.9 KiB
C
/*
|
|
* compat.h — Cross-platform compatibility macros and shims.
|
|
*
|
|
* Provides portable TLS, sleep, strdup/strndup, and getline across
|
|
* POSIX (macOS/Linux) and Windows. Include this instead of using
|
|
* platform-specific macros directly.
|
|
*/
|
|
#ifndef CBM_COMPAT_H
|
|
#define CBM_COMPAT_H
|
|
|
|
#include <errno.h>
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
/* stdlib.h declares getenv (cbm_tmpdir) and, on Windows, _putenv_s (cbm_setenv/
|
|
* cbm_unsetenv). The x86-64 mingw toolchain pulled it in transitively, but the
|
|
* aarch64 (CLANGARM64) include chain does not, so include it directly — without
|
|
* it those calls become implicit declarations that conflict with the real
|
|
* stdlib.h types and fail to compile on native ARM64 Windows. */
|
|
#include <stdlib.h>
|
|
|
|
/* ── Thread-local storage ─────────────────────────────────────── */
|
|
/* _Thread_local is C11 standard — works on GCC, Clang, and MSVC (2019+).
|
|
* __declspec(thread) is MSVC-only and doesn't work on MinGW GCC. */
|
|
#define CBM_TLS _Thread_local
|
|
|
|
/* ── Sleep ────────────────────────────────────────────────────── */
|
|
#ifdef _WIN32
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
#define cbm_usleep(us) Sleep((DWORD)((us) / 1000))
|
|
#else
|
|
#include <unistd.h>
|
|
#define cbm_usleep(us) usleep((useconds_t)(us))
|
|
#endif
|
|
|
|
/* ── strdup / strndup ─────────────────────────────────────────── */
|
|
#ifdef _WIN32
|
|
#define cbm_strdup _strdup
|
|
/* Implemented in compat.c */
|
|
char *cbm_strndup(const char *s, size_t n);
|
|
#else
|
|
#define cbm_strdup strdup
|
|
#define cbm_strndup strndup
|
|
#endif
|
|
|
|
/* ── getline (Windows lacks it) ───────────────────────────────── */
|
|
#ifdef _WIN32
|
|
/* Implemented in compat.c */
|
|
ssize_t cbm_getline(char **lineptr, size_t *n, FILE *stream);
|
|
#else
|
|
#define cbm_getline getline
|
|
#endif
|
|
|
|
/* ── fileno ───────────────────────────────────────────────────── */
|
|
#ifdef _WIN32
|
|
#define cbm_fileno _fileno
|
|
#else
|
|
#define cbm_fileno fileno
|
|
#endif
|
|
|
|
/* ── strcasestr (Windows lacks it) ────────────────────────────── */
|
|
#ifdef _WIN32
|
|
/* Implemented in compat.c */
|
|
char *cbm_strcasestr(const char *haystack, const char *needle);
|
|
#else
|
|
#define cbm_strcasestr strcasestr
|
|
#endif
|
|
|
|
/* ── mkdir portability ───────────────────────────────────────── */
|
|
#ifdef _WIN32
|
|
#include <direct.h>
|
|
#define cbm_mkdir(path) _mkdir(path)
|
|
#else
|
|
#include <sys/stat.h>
|
|
#define cbm_mkdir(path) mkdir(path, 0755)
|
|
#endif
|
|
|
|
/* ── clock_gettime / nanosleep (Windows lacks them) ──────────── */
|
|
#include <time.h>
|
|
#ifdef _WIN32
|
|
#ifndef CLOCK_MONOTONIC
|
|
#define CLOCK_MONOTONIC 1
|
|
#endif
|
|
/* Implemented in compat.c */
|
|
int cbm_clock_gettime(int clk_id, struct timespec *tp);
|
|
static inline int cbm_nanosleep(const struct timespec *req, struct timespec *rem) {
|
|
(void)rem;
|
|
Sleep((DWORD)(req->tv_sec * 1000 + req->tv_nsec / 1000000));
|
|
return 0;
|
|
}
|
|
#else
|
|
#define cbm_clock_gettime clock_gettime
|
|
#define cbm_nanosleep nanosleep
|
|
#endif
|
|
|
|
/* Sleeps for the full requested duration even when POSIX signals interrupt it. */
|
|
int cbm_nanosleep_full(const struct timespec *req);
|
|
|
|
/* Per-CALLING-THREAD CPU time in nanoseconds. Unlike cbm_clock_gettime — which
|
|
* measures WALL time on every platform (QueryPerformanceCounter on Windows) —
|
|
* this advances only while the calling thread is actually scheduled on a CPU, so
|
|
* a descheduled/starved thread accrues (almost) none. Use it to budget work
|
|
* against real CPU consumed rather than wall-clock, which a contended host
|
|
* inflates without the thread doing any work. POSIX (incl. macOS 10.12+):
|
|
* CLOCK_THREAD_CPUTIME_ID. Windows: GetThreadTimes kernel+user. Returns 0 if the
|
|
* platform clock is unavailable. Implemented in compat.c. */
|
|
uint64_t cbm_thread_cpu_time_ns(void);
|
|
|
|
/* ── gmtime_r (Windows lacks it) ─────────────────────────────── */
|
|
#ifdef _WIN32
|
|
static inline struct tm *cbm_gmtime_r(const time_t *timep, struct tm *result) {
|
|
return gmtime_s(result, timep) == 0 ? result : NULL;
|
|
}
|
|
#else
|
|
#define cbm_gmtime_r gmtime_r
|
|
#endif
|
|
|
|
/* ── mkdtemp (Windows lacks it) ──────────────────────────────── */
|
|
#ifdef _WIN32
|
|
/* Translates /tmp/ to %TEMP%\ and copies result back to tmpl.
|
|
* Callers MUST use char buf[CBM_SZ_256] or larger. */
|
|
char *cbm_mkdtemp(char *tmpl);
|
|
#else
|
|
#define cbm_mkdtemp mkdtemp
|
|
#endif
|
|
|
|
/* ── mkstemp (Windows lacks it) ──────────────────────────────── */
|
|
#ifdef _WIN32
|
|
int cbm_mkstemp(char *tmpl);
|
|
|
|
#else
|
|
#define cbm_mkstemp mkstemp
|
|
#endif
|
|
|
|
/* Rewrite an absolute path into the form the platform's file APIs accept at
|
|
* any length. On Windows, absolute drive paths beyond the legacy 260-char
|
|
* limit are canonicalized and given the extended-length \\?\ prefix
|
|
* (SQLite passes such UTF-8 paths through to CreateFileW unchanged). On
|
|
* POSIX, the path is copied verbatim. Returns false if the buffer is too
|
|
* small or the path cannot be canonicalized. */
|
|
bool cbm_path_for_file_api(const char *path, char *out, size_t out_size);
|
|
|
|
/* ── setenv / unsetenv (Windows lacks them) ──────────────────── */
|
|
#ifdef _WIN32
|
|
static inline int cbm_setenv(const char *name, const char *value, int overwrite) {
|
|
(void)overwrite;
|
|
if (!name || !value) {
|
|
return EINVAL;
|
|
}
|
|
int name_chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, name, -1, NULL, 0);
|
|
int value_chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value, -1, NULL, 0);
|
|
wchar_t *wide_name =
|
|
name_chars > 0 ? (wchar_t *)malloc((size_t)name_chars * sizeof(*wide_name)) : NULL;
|
|
wchar_t *wide_value =
|
|
value_chars > 0 ? (wchar_t *)malloc((size_t)value_chars * sizeof(*wide_value)) : NULL;
|
|
bool converted =
|
|
wide_name && wide_value &&
|
|
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, name, -1, wide_name, name_chars) > 0 &&
|
|
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value, -1, wide_value, value_chars) > 0;
|
|
if (!converted) {
|
|
free(wide_name);
|
|
free(wide_value);
|
|
return EINVAL;
|
|
}
|
|
/* Keep the CRT's narrow environment useful for legacy getenv callers,
|
|
* then repair the process-wide Windows environment with the actual UTF-16
|
|
* value. _putenv_s alone routes UTF-8 path bytes through the active ANSI
|
|
* code page, which corrupts non-ASCII cache roots inherited by children. */
|
|
int status = _putenv_s(name, value);
|
|
if (status == 0 && !SetEnvironmentVariableW(wide_name, wide_value)) {
|
|
status = EINVAL;
|
|
}
|
|
free(wide_name);
|
|
free(wide_value);
|
|
return status;
|
|
}
|
|
static inline int cbm_unsetenv(const char *name) {
|
|
if (!name) {
|
|
return EINVAL;
|
|
}
|
|
int name_chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, name, -1, NULL, 0);
|
|
wchar_t *wide_name =
|
|
name_chars > 0 ? (wchar_t *)malloc((size_t)name_chars * sizeof(*wide_name)) : NULL;
|
|
if (!wide_name ||
|
|
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, name, -1, wide_name, name_chars) <= 0) {
|
|
free(wide_name);
|
|
return EINVAL;
|
|
}
|
|
int status = _putenv_s(name, "");
|
|
if (status == 0) {
|
|
SetLastError(ERROR_SUCCESS);
|
|
if (!SetEnvironmentVariableW(wide_name, NULL) && GetLastError() != ERROR_ENVVAR_NOT_FOUND) {
|
|
status = EINVAL;
|
|
}
|
|
}
|
|
free(wide_name);
|
|
return status;
|
|
}
|
|
#else
|
|
#define cbm_setenv setenv
|
|
#define cbm_unsetenv unsetenv
|
|
#endif
|
|
|
|
/* ── pipe (Windows uses _pipe) ───────────────────────────────── */
|
|
#ifdef _WIN32
|
|
#include <io.h>
|
|
#include <fcntl.h>
|
|
#define cbm_pipe(fds) _pipe(fds, 4096, _O_BINARY)
|
|
#else
|
|
#define cbm_pipe(fds) pipe(fds)
|
|
#endif
|
|
|
|
/* ── Temp directory helper ───────────────────────────────────── */
|
|
static inline const char *cbm_tmpdir(void) {
|
|
#ifdef _WIN32
|
|
const char *t = getenv("TEMP");
|
|
if (!t)
|
|
t = getenv("TMP");
|
|
return t ? t : ".";
|
|
#else
|
|
return "/tmp";
|
|
#endif
|
|
}
|
|
|
|
/* ── Signal handling ──────────────────────────────────────────── */
|
|
/* Windows doesn't have sigaction; provide macro to select signal API. */
|
|
#ifdef _WIN32
|
|
#define CBM_HAS_SIGACTION 0
|
|
#else
|
|
#define CBM_HAS_SIGACTION 1
|
|
#endif
|
|
|
|
#endif /* CBM_COMPAT_H */
|