mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Four places ask "is this binary instrumented?" and each asked it differently.
One of them, the C# LSP bench, only recognised ASan, so TSan and MSan measured
an instrumented parse against the NATIVE 200ms budget. The other three carried
a hand-copied list of `__SANITIZE_*__` macros that nobody kept in sync.
That drift is what the TSan gap was made of. CFLAGS_TSAN never passed
SANITIZED_DEFINE, and the per-site conditions could not cover for it: they test
`__SANITIZE_THREAD__`, which is GCC's spelling. Clang — the compiler that leg
uses — announces thread instrumentation through `__has_feature(thread_sanitizer)`
only, and no site consulted it. The claim in 0a163d4f that compiler probes could
not have helped is true of the probes we had, not of the one clang actually
offers.
src/foundation/sanitized.h now answers the question once, as CBM_SANITIZED,
from two sources with distinct jobs:
- CBM_SANITIZED_BUILD from the build system stays the source of truth, and is
the ONLY thing that can answer for UBSan and trap-UBSan: undefined-behaviour
instrumentation leaves no macro and no __has_feature bit to probe.
- The clang and GCC probes are the backstop for the three sanitizers that do
announce themselves, so a lane that forgets the define still gets correct
budgets instead of native ones on an instrumented binary.
Deliberately no #error when a probe fires without the define: promoting leaves
the binary correct while the lane gets fixed, and it does not break an
out-of-tree `make CFLAGS_EXTRA=-fsanitize=address` that never went near
Makefile.cbm.
__has_feature is defined away where it does not exist rather than guarded with
`#elif defined(__has_feature)`. The guarded form compiles everywhere but fails
cppcheck, which walks every configuration and rejects the file with "failed to
evaluate #if condition, undefined function-like macro invocation". The
define-away idiom is what clang documents and what tests/test_mem.c already
uses; `defined(...) && __has_feature(...)` is not an option at all, since && does
not spare a preprocessor without the builtin from parsing `0 (0)`.
Verified the resolution rather than assuming it (clang 22, -dM -E):
native CBM_SANITIZED 0
-DCBM_SANITIZED_BUILD=1 CBM_SANITIZED 1
-fsanitize=address CBM_SANITIZED 1
-fsanitize=thread (linux target) CBM_SANITIZED 1
-fsanitize=memory (linux target) CBM_SANITIZED 1
-fsanitize=undefined CBM_SANITIZED 0 <- define-only, as designed
The third row is the one that matters: the TSan failure this header is named
after would have self-healed.
Also wired the define into the instrumented flag sets of our own code that
still lacked it — CXXFLAGS_TSAN (preprocessor.cpp is ours, and CXXFLAGS_TEST
already had it), GRAMMAR_CFLAGS_TEST and GRAMMAR_CFLAGS_TSAN. Neither tree can
include the header today (no -Isrc), so this is the build system keeping its
own promise rather than a behaviour change. Vendored flag sets are untouched:
mimalloc, sqlite3, tre, zstd, lz4 and tree-sitter read no macro of ours.
Behaviour change worth naming: test_cs_lsp_bench now allows 2000ms on the TSan
and MSan lanes instead of 200ms. It loosens a bound that was being applied to
an instrumented binary by accident; it never tightens one.
Not verified locally: this machine has no POSIX-target compiler, so the POSIX
half of subprocess.c was not compiled here. The full Windows test-runner builds
clean with -Werror and the subprocess suite is green (14 passed, 17 skipped);
clang-format clean; macro matrix as above.
Signed-off-by: Mauricio Offermann <mauricio.offermann@gocode.cl>
255 lines
8.6 KiB
C
255 lines
8.6 KiB
C
/*
|
||
* test_cs_lsp_bench.c — parity-tracking benchmark for cs_lsp.
|
||
*
|
||
* The fixture is a synthetic but representative ~260-line C# file that
|
||
* exercises the parity features cs_lsp implements:
|
||
* - using / using static / using alias / global using
|
||
* - file-scoped namespace
|
||
* - class / record / interface / struct / enum
|
||
* - inheritance + interface implementation
|
||
* - generics: List<T>, Dictionary<K,V>, generic methods
|
||
* - async / await with Task<T> / ValueTask<T> unwrap
|
||
* - LINQ method syntax (Where / Select / First / Count / ToList ...)
|
||
* - properties (auto + expression-bodied), fields, indexers
|
||
* - primary constructors (records + C# 12 classes)
|
||
* - method chaining via return-type propagation
|
||
* - this / base dispatch
|
||
* - var inference, foreach element typing
|
||
* - common BCL stdlib calls (Console, string, StringBuilder, Math)
|
||
*
|
||
* This is NOT a Roslyn parity proof — it measures the resolution rate
|
||
* (resolved_calls vs textual calls) on idiomatic code, which is the
|
||
* subset cs_lsp targets. See the honest scope notes in cs_lsp.c.
|
||
*
|
||
* Reports: total textual calls, resolved calls (raw), high-confidence
|
||
* resolutions (>= 0.90), and the resolution ratio. Asserts a floor of
|
||
* 45% under sanitizers (idiomatic-code production expectation is higher;
|
||
* the gap is dominated by ctor-synthetic 0.50 markers and BCL long tail).
|
||
*/
|
||
#include "test_framework.h"
|
||
#include "cbm.h"
|
||
#include "foundation/sanitized.h"
|
||
#include "lsp/cs_lsp.h"
|
||
#include <stdlib.h>
|
||
#include <time.h>
|
||
|
||
static const char *bench_source =
|
||
"global using System;\n"
|
||
"using System.Collections.Generic;\n"
|
||
"using System.Linq;\n"
|
||
"using System.Text;\n"
|
||
"using System.Threading.Tasks;\n"
|
||
"using static System.Math;\n"
|
||
"using Ints = System.Collections.Generic.List<int>;\n"
|
||
"\n"
|
||
"namespace Bench.App;\n"
|
||
"\n"
|
||
"public enum Status { Active, Disabled, Pending }\n"
|
||
"\n"
|
||
"public interface IRepository\n"
|
||
"{\n"
|
||
" User Find(int id);\n"
|
||
" Task<User> FindAsync(int id);\n"
|
||
"}\n"
|
||
"\n"
|
||
"public record Address(string Street, string City)\n"
|
||
"{\n"
|
||
" public string Full() { return Street + \", \" + City; }\n"
|
||
"}\n"
|
||
"\n"
|
||
"public class User\n"
|
||
"{\n"
|
||
" public int Id { get; set; }\n"
|
||
" public string Name { get; set; }\n"
|
||
" public Address Home { get; set; }\n"
|
||
" public Status State { get; set; }\n"
|
||
" public User(int id, string name) { Id = id; Name = name; }\n"
|
||
" public string Display() { return Name.ToUpper(); }\n"
|
||
" public string Greeting() => \"Hi \" + Name.Trim();\n"
|
||
" public string City() { return Home.City; }\n"
|
||
"}\n"
|
||
"\n"
|
||
"public class BaseService\n"
|
||
"{\n"
|
||
" protected readonly StringBuilder _log = new StringBuilder();\n"
|
||
" public virtual string Tag() { return \"base\"; }\n"
|
||
" public void Note(string m) { _log.Append(m).Append(';'); }\n"
|
||
" public string Dump() { return _log.ToString(); }\n"
|
||
"}\n"
|
||
"\n"
|
||
"public class UserService : BaseService, IRepository\n"
|
||
"{\n"
|
||
" private readonly Dictionary<int, User> _users = new Dictionary<int, User>();\n"
|
||
" private readonly List<User> _all = new List<User>();\n"
|
||
"\n"
|
||
" public override string Tag() { return \"users\"; }\n"
|
||
"\n"
|
||
" public void Add(User u)\n"
|
||
" {\n"
|
||
" _users.Add(u.Id, u);\n"
|
||
" _all.Add(u);\n"
|
||
" Note(u.Display());\n"
|
||
" }\n"
|
||
"\n"
|
||
" public User Find(int id)\n"
|
||
" {\n"
|
||
" if (_users.ContainsKey(id))\n"
|
||
" {\n"
|
||
" var u = _users[id];\n"
|
||
" u.Display();\n"
|
||
" return u;\n"
|
||
" }\n"
|
||
" return null;\n"
|
||
" }\n"
|
||
"\n"
|
||
" public async Task<User> FindAsync(int id)\n"
|
||
" {\n"
|
||
" await Task.Delay(1);\n"
|
||
" var u = Find(id);\n"
|
||
" return u;\n"
|
||
" }\n"
|
||
"\n"
|
||
" public List<string> ActiveNames()\n"
|
||
" {\n"
|
||
" return _all\n"
|
||
" .Where(x => x.State == Status.Active)\n"
|
||
" .Select(x => x.Display())\n"
|
||
" .ToList();\n"
|
||
" }\n"
|
||
"\n"
|
||
" public int Count() { return _all.Count(); }\n"
|
||
"\n"
|
||
" public string FirstCity()\n"
|
||
" {\n"
|
||
" var first = _all.First();\n"
|
||
" return first.City();\n"
|
||
" }\n"
|
||
"\n"
|
||
" public void Each()\n"
|
||
" {\n"
|
||
" foreach (var u in _all)\n"
|
||
" {\n"
|
||
" u.Greeting();\n"
|
||
" this.Note(u.Name);\n"
|
||
" }\n"
|
||
" }\n"
|
||
"}\n"
|
||
"\n"
|
||
"public class Calculator\n"
|
||
"{\n"
|
||
" public double Hypotenuse(double a, double b)\n"
|
||
" {\n"
|
||
" return Sqrt(Pow(a, 2) + Pow(b, 2));\n"
|
||
" }\n"
|
||
" public T Echo<T>(T value) { return value; }\n"
|
||
"}\n"
|
||
"\n"
|
||
"public class Program\n"
|
||
"{\n"
|
||
" public static string Render(UserService svc)\n"
|
||
" {\n"
|
||
" var sb = new StringBuilder();\n"
|
||
" var names = svc.ActiveNames();\n"
|
||
" foreach (var n in names)\n"
|
||
" {\n"
|
||
" sb.Append(n).Append('\\n');\n"
|
||
" }\n"
|
||
" return sb.ToString();\n"
|
||
" }\n"
|
||
"\n"
|
||
" public static void Main()\n"
|
||
" {\n"
|
||
" var svc = new UserService();\n"
|
||
" var a = new Address(\"1 St\", \"Town\");\n"
|
||
" var u = new User(1, \"alice\");\n"
|
||
" u.Home = a;\n"
|
||
" svc.Add(u);\n"
|
||
" var found = svc.Find(1);\n"
|
||
" found.Display();\n"
|
||
" found.City();\n"
|
||
" var c = new Calculator();\n"
|
||
" var h = c.Hypotenuse(3.0, 4.0);\n"
|
||
" Console.WriteLine(h.ToString());\n"
|
||
" var rendered = Render(svc);\n"
|
||
" Console.WriteLine(rendered.Trim());\n"
|
||
" var addr = a.Full();\n"
|
||
" Console.WriteLine(addr);\n"
|
||
" svc.Tag();\n"
|
||
" svc.Dump();\n"
|
||
" Ints xs = new Ints();\n"
|
||
" xs.Add(svc.Count());\n"
|
||
" }\n"
|
||
"}\n";
|
||
|
||
static double elapsed_ms(struct timespec t0, struct timespec t1) {
|
||
double s = (double)(t1.tv_sec - t0.tv_sec);
|
||
double ns = (double)(t1.tv_nsec - t0.tv_nsec);
|
||
return s * 1000.0 + ns / 1000000.0;
|
||
}
|
||
|
||
TEST(cslsp_bench_resolution_ratio) {
|
||
/* Perf benchmark: time-budgeted. Under ASan+UBSan the budget is scaled
|
||
* (see the sanitizer-aware time-budget assert below); the benchmark always
|
||
* runs so regressions surface in every configuration. */
|
||
int slen = (int)strlen(bench_source);
|
||
|
||
struct timespec t0;
|
||
struct timespec t1;
|
||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||
CBMFileResult *r = cbm_extract_file(bench_source, slen, CBM_LANG_CSHARP,
|
||
"test", "bench.cs", 0, NULL, NULL);
|
||
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||
ASSERT_NOT_NULL(r);
|
||
|
||
double ms = elapsed_ms(t0, t1);
|
||
int calls = r->calls.count;
|
||
int resolved = r->resolved_calls.count;
|
||
int high_conf = 0;
|
||
for (int i = 0; i < r->resolved_calls.count; i++) {
|
||
if (r->resolved_calls.items[i].confidence >= 0.90f) high_conf++;
|
||
}
|
||
int loc = 0;
|
||
for (const char *p = bench_source; *p; p++) {
|
||
if (*p == '\n') loc++;
|
||
}
|
||
double ratio = calls > 0 ? (double)resolved / (double)calls : 0.0;
|
||
double hi_ratio = calls > 0 ? (double)high_conf / (double)calls : 0.0;
|
||
|
||
printf(" cs bench: %d lines, %d calls, %d resolved (%.0f%%), "
|
||
"%d high-conf (%.0f%%), %.2f ms\n",
|
||
loc, calls, resolved, ratio * 100.0, high_conf, hi_ratio * 100.0,
|
||
ms);
|
||
|
||
/* Free the result BEFORE asserting so a budget miss doesn't leak. */
|
||
cbm_free_result(r);
|
||
|
||
ASSERT_GTE(calls, 1);
|
||
ASSERT_GTE(resolved, 1);
|
||
|
||
/* Floor at 45% under sanitizers. The gap to 100% is dominated by:
|
||
* - ctor-synthetic 0.50 markers (counted as resolved but low-conf)
|
||
* - BCL long-tail calls not in the curated stdlib
|
||
* - LINQ lambda-body inference (out of scope, documented)
|
||
* This benchmark exists to track regressions, not to claim Roslyn
|
||
* parity. */
|
||
if (calls >= 20) {
|
||
ASSERT_GTE(resolved * 100, calls * 45);
|
||
}
|
||
|
||
/* Time budget. Instrumentation slows the parse ~5-10×, so scale the budget
|
||
* when a sanitizer is active. Native: 200 ms for a ~260-line fixture;
|
||
* sanitized: 2000 ms. The condition used to name ASan specifically, which
|
||
* left the TSan and MSan lanes measuring an instrumented parse against the
|
||
* native 200 ms. */
|
||
#if CBM_SANITIZED
|
||
ASSERT(ms < 2000.0);
|
||
#else
|
||
ASSERT(ms < 200.0);
|
||
#endif
|
||
PASS();
|
||
}
|
||
|
||
SUITE(cs_lsp_bench) {
|
||
RUN_TEST(cslsp_bench_resolution_ratio);
|
||
}
|