fix(bindings): add SafeHandleWrapper + memory growth checks; bump 0.9.1 (#571)

- Introduce SafeHandleWrapper with gating, short drain wait, and deferred release on last in-flight exit.
- Wire Engine/Program/Rvm/CompiledPolicy to wrapper (centralized handle use, interop helper).
- Add C# memory growth tests (using/finalizer paths) and extend xtask C# runner options.
- Add pooled marshalling utilities, ResultHelpers, and API cleanups; update versions/changelog.

Fixes #570. Closes #554

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2026-02-09 23:52:05 +05:30
committed by GitHub
parent 455d2aa588
commit 96360fa9d8
39 changed files with 1496 additions and 1151 deletions
@@ -12,7 +12,7 @@ namespace Benchmarks
public class CompiledPolicyEvaluationBenchmark
{
private static readonly string TestDataPath = Path.Combine(
Directory.GetCurrentDirectory(),
Directory.GetCurrentDirectory(),
"..", "..", "..",
"benches", "evaluation", "test_data"
);
@@ -33,7 +33,7 @@ namespace Benchmarks
private static readonly string[] PolicyNames = new[]
{
"rbac_policy",
"api_access_policy",
"api_access_policy",
"data_sensitivity_policy",
"time_based_policy",
"data_processing_policy",
@@ -46,21 +46,21 @@ namespace Benchmarks
private static List<(string Policy, string[] Inputs)> LoadPoliciesWithInputs()
{
var result = new List<(string Policy, string[] Inputs)>();
foreach (var (policyFile, inputFiles) in PolicyInputFiles)
{
var policyPath = Path.Combine(TestDataPath, "policies", policyFile);
var policy = File.ReadAllText(policyPath);
var inputs = inputFiles.Select(inputFile =>
{
var inputPath = Path.Combine(TestDataPath, "inputs", inputFile);
return File.ReadAllText(inputPath);
}).ToArray();
result.Add((policy, inputs));
}
return result;
}
@@ -68,14 +68,14 @@ namespace Benchmarks
{
var policiesWithInputs = LoadPoliciesWithInputs();
var compiledPolicies = new List<CompiledPolicy>();
foreach (var (policy, _) in policiesWithInputs)
{
var modules = new[] { new PolicyModule { Id = "policy.rego", Content = policy } };
var modules = new[] { new PolicyModule("policy.rego", policy) };
var compiled = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.bench.allow");
compiledPolicies.Add(compiled);
}
return compiledPolicies;
}
@@ -84,13 +84,13 @@ namespace Benchmarks
var cpuCount = Environment.ProcessorCount;
var maxThreads = cpuCount * 2;
var threadCounts = new List<int> { 1, 2 };
// Add even numbers from 4 to maxThreads
for (int i = 4; i <= maxThreads; i += 2)
{
threadCounts.Add(i);
}
Console.WriteLine($"Running compiled policy benchmark with max_threads: {maxThreads}");
Console.WriteLine($"Testing with thread counts: {string.Join(", ", threadCounts)}");
Console.WriteLine();
@@ -120,19 +120,19 @@ namespace Benchmarks
const int durationSeconds = 3;
var policiesWithInputs = LoadPoliciesWithInputs();
List<CompiledPolicy>? compiledPolicies = null;
if (useSharedPolicies)
{
compiledPolicies = PrepareSharedCompiledPolicies();
}
Console.WriteLine($"Warming up with {threads} threads for {warmupSeconds} seconds...");
// Warmup phase
var (_, _, _, _) = RunBenchmarkPhase(threads, warmupSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: true);
Console.WriteLine($"Running benchmark with {threads} threads for {durationSeconds} seconds...");
// Actual benchmark phase
var (totalEvaluations, evaluationTime, policyCounters, allocatedBytes) = RunBenchmarkPhase(threads, durationSeconds, policiesWithInputs, compiledPolicies, useSharedPolicies, isWarmup: false);
@@ -155,7 +155,7 @@ namespace Benchmarks
{
foreach (var policy in compiledPolicies)
{
policy.Dispose();
DisposeCompiledPolicy(policy);
}
}
@@ -173,8 +173,8 @@ namespace Benchmarks
}
private static (int totalEvaluations, TimeSpan evaluationTime, Dictionary<string, int> policyCounters, long allocatedBytes) RunBenchmarkPhase(
int threads,
int durationSeconds,
int threads,
int durationSeconds,
List<(string Policy, string[] Inputs)> policiesWithInputs,
List<CompiledPolicy>? compiledPolicies,
bool useSharedPolicies,
@@ -208,10 +208,10 @@ namespace Benchmarks
}
barrier.SignalAndWait();
int evaluationCount = 0;
var localEvaluationTime = TimeSpan.Zero;
while (!stopExecution)
{
// Use different policy for each iteration
@@ -226,23 +226,29 @@ namespace Benchmarks
{
// Measure only the evaluation call
var evalStopwatch = Stopwatch.StartNew();
if (useSharedPolicies)
{
var result = compiledPolicies![policyIdx].EvalWithInput(input);
}
else
{
// Compile policy in each iteration
var modules = new[] { new PolicyModule { Id = "policy.rego", Content = policy } };
// Compile policy in each iteration.
var modules = new[] { new PolicyModule("policy.rego", policy) };
var compiled = Compiler.CompilePolicyWithEntrypoint("{}", modules, "data.bench.allow");
var result = compiled.EvalWithInput(input);
compiled.Dispose();
try
{
var result = compiled.EvalWithInput(input);
}
finally
{
DisposeCompiledPolicy(compiled);
}
}
evalStopwatch.Stop();
localEvaluationTime += evalStopwatch.Elapsed;
// Track successful evaluations (only during actual benchmark, not warmup)
if (!isWarmup)
{
@@ -256,10 +262,10 @@ namespace Benchmarks
{
// Ignore evaluation errors for benchmarking purposes
}
evaluationCount++;
}
// Store the actual evaluation time for this thread
if (!isWarmup)
{
@@ -284,11 +290,23 @@ namespace Benchmarks
var totalEvaluations = policyCounters.Values.Sum();
var totalEvaluationTime = evaluationTimes.Values.Aggregate(TimeSpan.Zero, (sum, time) => sum + time);
// Use pure evaluation time (consistent with Rust benchmark)
var evaluationTime = totalEvaluationTime == TimeSpan.Zero ? stopwatch.Elapsed : totalEvaluationTime;
return (totalEvaluations, evaluationTime, policyCounters, allocatedBytes);
}
private static void DisposeCompiledPolicy(CompiledPolicy policy)
{
try
{
policy.Dispose();
}
catch (TimeoutException ex)
{
Console.WriteLine($"Warning: {ex.Message}");
}
}
}
}