252 lines
8.4 KiB
C
252 lines
8.4 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 "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. ASan+UBSan 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. */
|
||
#ifdef __SANITIZE_ADDRESS__
|
||
ASSERT(ms < 2000.0);
|
||
#else
|
||
ASSERT(ms < 200.0);
|
||
#endif
|
||
PASS();
|
||
}
|
||
|
||
SUITE(cs_lsp_bench) {
|
||
RUN_TEST(cslsp_bench_resolution_ratio);
|
||
}
|