using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using JsonEvalRs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Runtime.InteropServices;
namespace JsonEvalBenchmark
{
class Program
{
private static string? _projectRoot;
static void Main(string[] args)
{
Console.WriteLine("๐ JSON Eval RS - Benchmark Suite");
Console.WriteLine();
string scenario = args.Length > 0 ? args[0] : "zcc";
Console.WriteLine($"๐ Scenario: '{scenario}'");
Console.WriteLine();
try
{
_projectRoot = FindProjectRoot();
if (_projectRoot == null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("โ Error: Could not find project root (Cargo.toml not found)");
Console.ResetColor();
Environment.Exit(1);
}
Console.WriteLine($"๐ Project Root: {_projectRoot}");
Console.WriteLine();
if (!BuildRelease())
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("โ Build failed. Aborting benchmark.");
Console.ResetColor();
Environment.Exit(1);
}
var rustResult = RunRustBenchmark(scenario);
var csharpResult = RunCSharpBenchmark(scenario);
PrintComparisonResults(rustResult, csharpResult);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("โ
Benchmark suite completed successfully!");
Console.ResetColor();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"โ Error: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
Console.ResetColor();
Environment.Exit(1);
}
}
private static string? FindProjectRoot()
{
string currentDir = Directory.GetCurrentDirectory();
string projectRoot = currentDir;
while (!File.Exists(Path.Combine(projectRoot, "Cargo.toml")) && Directory.GetParent(projectRoot) != null)
{
projectRoot = Directory.GetParent(projectRoot)!.FullName;
}
if (!File.Exists(Path.Combine(projectRoot, "Cargo.toml")))
{
return null;
}
return projectRoot;
}
private static bool BuildRelease()
{
Console.WriteLine("==================================================");
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("๐จ Step 1: Building Release");
Console.ResetColor();
Console.WriteLine("==================================================");
Console.WriteLine("๐ง Building Rust library (--release --features ffi)...");
if (!RunCommand("cargo", "build --release --features ffi", _projectRoot!))
{
return false;
}
string libName = GetLibraryFileName("json_eval_rs");
string libSource = Path.Combine(_projectRoot!, "target", "release", libName);
string libDest = Path.Combine(_projectRoot!, "bindings", "csharp-example", "bin", "Release", "net8.0", libName);
Console.WriteLine($"๐ Ensuring library is accessible ({libName})...");
if (File.Exists(libSource))
{
Directory.CreateDirectory(Path.GetDirectoryName(libDest)!);
File.Copy(libSource, libDest, overwrite: true);
Console.WriteLine($" โ Copied {libName} to {libDest}");
}
else
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" โ ๏ธ Library not found at {libSource}");
Console.ResetColor();
}
Console.WriteLine("โ
Build completed successfully!");
Console.WriteLine();
return true;
}
private static string GetLibraryFileName(string baseName)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return $"{baseName}.dll";
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return $"lib{baseName}.dylib";
else return $"lib{baseName}.so";
}
private static bool RunCommand(string fileName, string arguments, string workingDirectory)
{
var startInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = new Process { StartInfo = startInfo };
process.Start();
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"โ Command failed with exit code {process.ExitCode}");
Console.WriteLine($"Command: {fileName} {arguments}");
if (!string.IsNullOrEmpty(output))
{
Console.WriteLine($"Output: {output}");
}
if (!string.IsNullOrEmpty(error))
{
Console.WriteLine($"Error: {error}");
}
Console.ResetColor();
return false;
}
if (!string.IsNullOrEmpty(output))
{
var lines = output.Split('\n');
foreach (var line in lines)
{
if (line.Contains("Compiling") || line.Contains("Finished") || line.Contains("error"))
{
Console.WriteLine($" {line.Trim()}");
}
}
}
return true;
}
private class BenchmarkResult
{
public bool Success { get; set; }
public double TotalMs { get; set; }
public double ParsingMs { get; set; }
public double EvaluationMs { get; set; }
public string Scenario { get; set; } = string.Empty;
public JObject? EvaluatedSchema { get; set; }
public int DifferenceCount { get; set; }
}
private static BenchmarkResult RunCSharpBenchmark(string scenario)
{
Console.WriteLine("==================================================");
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("๐ฏ Step 3: Running C# Benchmark");
Console.ResetColor();
Console.WriteLine("==================================================");
Console.WriteLine($"Scenario: {scenario}");
Console.WriteLine($"Schema: {Path.Combine(_projectRoot!, "samples", $"{scenario}.json")}");
Console.WriteLine($"Data: {Path.Combine(_projectRoot!, "samples", $"{scenario}-data.json")}");
Console.WriteLine();
Console.WriteLine("๐ Loading files...");
string samplesPath = Path.Combine(_projectRoot!, "samples");
string schemaPath = Path.Combine(samplesPath, $"{scenario}.json");
string dataPath = Path.Combine(samplesPath, $"{scenario}-data.json");
string comparePath = Path.Combine(samplesPath, $"{scenario}-evaluated-compare.json");
if (!File.Exists(schemaPath))
{
throw new FileNotFoundException($"Schema file not found: {schemaPath}");
}
if (!File.Exists(dataPath))
{
throw new FileNotFoundException($"Data file not found: {dataPath}");
}
string schemaJson = File.ReadAllText(schemaPath);
string dataJson = File.ReadAllText(dataPath);
JObject? compareData = null;
if (File.Exists(comparePath))
{
compareData = JObject.Parse(File.ReadAllText(comparePath));
}
Console.WriteLine("โฑ๏ธ Running evaluation...");
Console.WriteLine();
var totalStopwatch = Stopwatch.StartNew();
var parsingStopwatch = Stopwatch.StartNew();
JSONEval eval;
try
{
eval = new JSONEval(schemaJson, context: "{}", data: dataJson);
}
catch (Exception ex)
{
throw new JsonEvalException($"Failed to create JSONEval instance: {ex.Message}", ex);
}
parsingStopwatch.Stop();
Console.WriteLine($" ๐ Parse (new): {parsingStopwatch.Elapsed.TotalMilliseconds:F3}ms");
var evalStopwatch = Stopwatch.StartNew();
try
{
eval.Evaluate(dataJson);
}
catch (Exception ex)
{
throw new JsonEvalException($"Evaluation failed: {ex.Message}", ex);
}
evalStopwatch.Stop();
Console.WriteLine($" โก Eval: {evalStopwatch.Elapsed.TotalMilliseconds:F3}ms");
totalStopwatch.Stop();
Console.WriteLine($" โฑ๏ธ Total: {totalStopwatch.Elapsed.TotalMilliseconds:F3}ms");
Console.WriteLine();
JObject result = eval.GetEvaluatedSchema(skipLayout: true);
Console.WriteLine("๐พ Saving results...");
string outputDir = Path.Combine(_projectRoot!, "samples");
Directory.CreateDirectory(outputDir);
string evaluatedPath = $"{outputDir}/{scenario}-evaluated-schema.json";
string parsedPath = $"{outputDir}/{scenario}-parsed-schema.json";
string sortedPath = $"{outputDir}/{scenario}-sorted-evaluations.json";
File.WriteAllText(evaluatedPath, result.ToString(Formatting.Indented));
var schemaValue = result.SelectToken("$.$params") ?? new JObject();
File.WriteAllText(parsedPath, schemaValue.ToString(Formatting.Indented));
File.WriteAllText(sortedPath, result.ToString(Formatting.Indented));
Console.WriteLine("โ
Results saved:");
Console.WriteLine($" - {evaluatedPath}");
Console.WriteLine($" - {parsedPath}");
Console.WriteLine($" - {sortedPath}");
Console.WriteLine();
int differenceCount = 0;
List<string> differences = new List<string>();
if (compareData != null)
{
differences = FindDifferences(result.SelectToken("$.$params.others")?.ToObject<JObject>() ?? new JObject(), compareData.SelectToken("$.others")?.ToObject<JObject>() ?? new JObject(), "$");
differenceCount = differences.Count;
if (differenceCount > 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"โ ๏ธ Comparison: Results differ from baseline ({differenceCount} difference(s)):");
Console.ResetColor();
foreach (var diff in differences)
{
Console.WriteLine($" - {diff}");
}
Console.WriteLine();
}
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("โ
Comparison: Results match baseline");
Console.ResetColor();
Console.WriteLine();
}
}
eval.Dispose();
return new BenchmarkResult
{
Success = true,
TotalMs = totalStopwatch.Elapsed.TotalMilliseconds,
ParsingMs = parsingStopwatch.Elapsed.TotalMilliseconds,
EvaluationMs = evalStopwatch.Elapsed.TotalMilliseconds,
Scenario = scenario,
EvaluatedSchema = result,
DifferenceCount = differenceCount
};
}
private static BenchmarkResult RunRustBenchmark(string scenario)
{
Console.WriteLine("==================================================");
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("๐ฆ Step 2: Running Rust Benchmark");
Console.ResetColor();
Console.WriteLine("==================================================");
Console.WriteLine($"Running: cargo run --release --example basic -- {scenario} --compare");
Console.WriteLine();
var startInfo = new ProcessStartInfo
{
FileName = "cargo",
Arguments = $"run --release --example basic -- {scenario} --compare",
WorkingDirectory = _projectRoot!,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = new Process { StartInfo = startInfo };
process.Start();
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"โ ๏ธ Rust benchmark failed with exit code {process.ExitCode}");
if (!string.IsNullOrEmpty(error))
{
Console.WriteLine($"Error: {error}");
}
Console.ResetColor();
Console.WriteLine();
return new BenchmarkResult { Success = false, Scenario = scenario };
}
var totalMatch = Regex.Match(output, @"(?:Total|Execution time):\s*([0-9.]+)(s|ms|ยตs|ns)", RegexOptions.IgnoreCase);
var parsingMatch = Regex.Match(output, @"Parse\s*\(new\):\s*([0-9.]+)(s|ms|ยตs|ns)", RegexOptions.IgnoreCase);
var evalMatch = Regex.Match(output, @"Eval:\s*([0-9.]+)(s|ms|ยตs|ns)", RegexOptions.IgnoreCase);
double parsing = ParseDuration(parsingMatch);
double evaluation = ParseDuration(evalMatch);
double total = ParseDuration(totalMatch);
if (total == 0 && parsing > 0 && evaluation > 0)
{
total = parsing + evaluation;
}
if (total == 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("โ ๏ธ Could not parse Rust benchmark output");
Console.WriteLine("Output:");
Console.WriteLine(output);
Console.ResetColor();
Console.WriteLine();
return new BenchmarkResult { Success = false, Scenario = scenario };
}
if (parsing > 0)
{
Console.WriteLine($" ๐ Parse (new): {parsing:F3}ms");
}
if (evaluation > 0)
{
Console.WriteLine($" โก Eval: {evaluation:F3}ms");
}
Console.WriteLine($" โฑ๏ธ Total: {total:F3}ms");
var diffMatch = Regex.Match(output, @"(\d+) difference\(s\)");
if (diffMatch.Success)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" โ ๏ธ {diffMatch.Groups[1].Value} difference(s) from baseline");
Console.ResetColor();
}
else if (output.Contains("differs from"))
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(" โ ๏ธ Differences detected from baseline");
Console.ResetColor();
}
Console.WriteLine();
return new BenchmarkResult
{
Success = true,
TotalMs = total,
ParsingMs = parsing,
EvaluationMs = evaluation,
Scenario = scenario
};
}
private static void PrintComparisonResults(BenchmarkResult rustResult, BenchmarkResult csharpResult)
{
Console.WriteLine("==================================================");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("๐ Step 4: Performance Comparison");
Console.ResetColor();
Console.WriteLine("==================================================");
Console.WriteLine("๐ฆ Rust (Native):");
if (rustResult.Success)
{
Console.WriteLine($" Total: {rustResult.TotalMs:F3}ms");
Console.WriteLine($" - Parse: {rustResult.ParsingMs:F3}ms");
Console.WriteLine($" - Eval: {rustResult.EvaluationMs:F3}ms");
}
else
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(" โ ๏ธ Benchmark unavailable");
Console.ResetColor();
}
Console.WriteLine();
Console.WriteLine("๐ฏ C# (FFI):");
Console.WriteLine($" Total: {csharpResult.TotalMs:F3}ms");
Console.WriteLine($" - Parse: {csharpResult.ParsingMs:F3}ms");
Console.WriteLine($" - Eval: {csharpResult.EvaluationMs:F3}ms");
if (csharpResult.DifferenceCount > 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" โ ๏ธ {csharpResult.DifferenceCount} differences from baseline");
Console.ResetColor();
}
Console.WriteLine();
if (rustResult.Success)
{
double overhead = (csharpResult.TotalMs / rustResult.TotalMs - 1.0) * 100;
Console.WriteLine("๐ FFI Overhead:");
if (overhead > 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($" Total: +{overhead:F1}%");
}
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($" Total: {overhead:F1}% (faster!)");
}
if (rustResult.ParsingMs > 0 && csharpResult.ParsingMs > 0)
{
double parsingOverhead = (csharpResult.ParsingMs / rustResult.ParsingMs - 1.0) * 100;
Console.WriteLine($" - Parse: {(parsingOverhead >= 0 ? "+" : "")}{parsingOverhead:F1}%");
}
if (rustResult.EvaluationMs > 0 && csharpResult.EvaluationMs > 0)
{
double evalOverhead = (csharpResult.EvaluationMs / rustResult.EvaluationMs - 1.0) * 100;
Console.WriteLine($" - Eval: {(evalOverhead >= 0 ? "+" : "")}{evalOverhead:F1}%");
}
Console.ResetColor();
Console.WriteLine();
}
Console.WriteLine("๐พ Memory Usage (C#):");
Console.WriteLine($" Working Set: {Process.GetCurrentProcess().WorkingSet64 / 1024 / 1024:F2} MB");
Console.WriteLine($" Private: {Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024:F2} MB");
Console.WriteLine();
}
private static double ParseDuration(Match match)
{
if (!match.Success)
return 0;
double value = double.Parse(match.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture);
string unit = match.Groups[2].Value.ToLower();
return unit switch
{
"s" => value * 1000.0,
"ms" => value,
"ยตs" or "us" => value / 1000.0,
"ns" => value / 1000000.0,
_ => value
};
}
private static List<string> FindDifferences(JToken actual, JToken expected, string path)
{
var differences = new List<string>();
if (actual.Type != expected.Type)
{
differences.Add($"{path} type differs: actual={actual.Type} expected={expected.Type}");
return differences;
}
if (actual is JObject actualObj && expected is JObject expectedObj)
{
foreach (var prop in expectedObj.Properties())
{
var actualProp = actualObj.Property(prop.Name);
if (actualProp == null)
{
differences.Add($"{path}.{prop.Name} missing in actual");
}
else
{
differences.AddRange(FindDifferences(actualProp.Value, prop.Value, $"{path}.{prop.Name}"));
}
}
foreach (var prop in actualObj.Properties())
{
if (expectedObj.Property(prop.Name) == null)
{
differences.Add($"{path}.{prop.Name} extra in actual");
}
}
}
else if (actual is JArray actualArray && expected is JArray expectedArray)
{
if (actualArray.Count != expectedArray.Count)
{
differences.Add($"{path} array length differs: actual={actualArray.Count} expected={expectedArray.Count}");
}
else
{
for (int i = 0; i < actualArray.Count; i++)
{
differences.AddRange(FindDifferences(actualArray[i], expectedArray[i], $"{path}[{i}]"));
}
}
}
else if (actual is JValue actualValue && expected is JValue expectedValue)
{
if (!JToken.DeepEquals(actualValue, expectedValue))
{
differences.Add($"{path} differs: actual={actualValue} expected={expectedValue}");
}
}
return differences;
}
}
}