blob: c15c866d5d7018439208873939015aa65826b1a2 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.Json;
using ExternalPoint = Apache.Fory.Benchmarks.CSharp.ExternalTypes.Point;
using ExternalUser = Apache.Fory.Benchmarks.CSharp.ExternalTypes.User;
using ForyRuntime = Apache.Fory.Fory;
namespace Apache.Fory.Benchmarks.CSharp;
internal sealed record EquivalenceCase(
string DataType,
string Operation,
int SerializedSize,
string SerializedFrameBase64,
Action Action);
internal sealed record EquivalenceMeasurement(
double OperationsPerSecond,
double AverageNanoseconds,
long Iterations,
double ElapsedSeconds);
internal sealed record EquivalenceSideResult(
string DataType,
string Operation,
int SerializedSize,
string SerializedFrameBase64,
EquivalenceMeasurement Measurement,
double AllocatedBytesPerOperation);
internal sealed record EquivalenceSideOutput(
string GeneratedAtUtc,
string RuntimeVersion,
string OsDescription,
string OsArchitecture,
string ProcessArchitecture,
int ProcessorCount,
double WarmupSeconds,
double DurationSeconds,
int AllocationIterations,
string Implementation,
List<EquivalenceSideResult> Results);
internal static class ExternalEquivalenceBenchmark
{
private const uint UserTypeId = 101;
private const uint PointTypeId = 102;
private const uint UserHolderTypeId = 103;
private const uint ListHolderTypeId = 104;
private const uint MapHolderTypeId = 105;
private const int BatchSize = 256;
public static int Run(
ExternalBenchmarkOptions options,
JsonSerializerOptions jsonOptions)
{
string implementation = ImplementationName(options.Implementation);
List<EquivalenceCase> cases = BuildCases(
options,
options.Implementation);
if (cases.Count == 0)
{
throw new ArgumentException("no external-equivalence benchmark cases selected");
}
Console.WriteLine("=== Fory C# External-Type Equivalence Benchmark ===");
Console.WriteLine($"Implementation: {implementation}");
Console.WriteLine($"Cases: {cases.Count}");
Console.WriteLine(
$"Warmup: {options.WarmupSeconds.ToString("F2", CultureInfo.InvariantCulture)}s");
Console.WriteLine(
$"Duration: {options.DurationSeconds.ToString("F2", CultureInfo.InvariantCulture)}s");
Console.WriteLine($"Allocation iterations: {options.AllocationIterations}");
Console.WriteLine();
List<EquivalenceSideResult> results = new(cases.Count);
foreach (EquivalenceCase benchmarkCase in cases)
{
Console.WriteLine(
$"Running {implementation}/{benchmarkCase.DataType}/{benchmarkCase.Operation}...");
results.Add(RunCase(benchmarkCase, options));
}
EquivalenceSideOutput output = new(
GeneratedAtUtc: DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture),
RuntimeVersion: Environment.Version.ToString(),
OsDescription: RuntimeInformation.OSDescription,
OsArchitecture: RuntimeInformation.OSArchitecture.ToString(),
ProcessArchitecture: RuntimeInformation.ProcessArchitecture.ToString(),
ProcessorCount: Environment.ProcessorCount,
WarmupSeconds: options.WarmupSeconds,
DurationSeconds: options.DurationSeconds,
AllocationIterations: options.AllocationIterations,
Implementation: implementation,
Results: results);
string outputPath = Path.GetFullPath(options.OutputPath);
string? parent = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(parent))
{
Directory.CreateDirectory(parent);
}
File.WriteAllText(outputPath, JsonSerializer.Serialize(output, jsonOptions));
PrintSummary(implementation, results);
Console.WriteLine();
Console.WriteLine($"Results written to {outputPath}");
return 0;
}
private static EquivalenceSideResult RunCase(
EquivalenceCase benchmarkCase,
ExternalBenchmarkOptions options)
{
RunForDuration(benchmarkCase.Action, options.WarmupSeconds);
CollectGarbage();
EquivalenceMeasurement measurement = MeasureTime(
benchmarkCase.Action,
options.DurationSeconds);
CollectGarbage();
long allocatedBytes = MeasureAllocation(
benchmarkCase.Action,
options.AllocationIterations);
double bytesPerOperation =
(double)allocatedBytes / options.AllocationIterations;
return new EquivalenceSideResult(
benchmarkCase.DataType,
benchmarkCase.Operation,
benchmarkCase.SerializedSize,
benchmarkCase.SerializedFrameBase64,
measurement,
bytesPerOperation);
}
private static EquivalenceMeasurement MeasureTime(Action action, double durationSeconds)
{
long iterations = 0;
Stopwatch stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed.TotalSeconds < durationSeconds)
{
for (int i = 0; i < BatchSize; i++)
{
action();
}
iterations += BatchSize;
}
double elapsed = stopwatch.Elapsed.TotalSeconds;
return new EquivalenceMeasurement(
iterations / elapsed,
elapsed * 1_000_000_000.0 / iterations,
iterations,
elapsed);
}
private static long MeasureAllocation(Action action, int iterations)
{
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < iterations; i++)
{
action();
}
return GC.GetAllocatedBytesForCurrentThread() - before;
}
private static void RunForDuration(Action action, double durationSeconds)
{
Stopwatch stopwatch = Stopwatch.StartNew();
while (stopwatch.Elapsed.TotalSeconds < durationSeconds)
{
action();
}
}
private static void CollectGarbage()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
private static List<EquivalenceCase> BuildCases(
ExternalBenchmarkOptions options,
ExternalImplementation implementation)
{
return implementation switch
{
ExternalImplementation.Ordinary => BuildOrdinaryCases(options),
ExternalImplementation.External => BuildExternalCases(options),
_ => throw new ArgumentException(
"external-equivalence requires one selected implementation"),
};
}
private static List<EquivalenceCase> BuildOrdinaryCases(
ExternalBenchmarkOptions options)
{
ForyRuntime writer = BuildFory();
ForyRuntime reader = BuildFory();
RegisterOrdinary(writer);
RegisterOrdinary(reader);
List<EquivalenceCase> cases = [];
AddCase(
"class-root",
EquivalenceData.CreateOrdinaryUser(),
writer,
reader,
OrdinaryUsersEqual,
options,
cases);
AddCase(
"struct-root",
EquivalenceData.CreateOrdinaryPoint(),
writer,
reader,
(expected, decoded) =>
expected.X == decoded.X && expected.Y == decoded.Y,
options,
cases);
AddCase(
"holder-field",
new OrdinaryUserHolder
{
Value = EquivalenceData.CreateOrdinaryUser(),
},
writer,
reader,
(expected, decoded) =>
OrdinaryUsersEqual(expected.Value, decoded.Value),
options,
cases);
List<OrdinaryUser> list = EquivalenceData.CreateOrdinaryList();
AddCase(
"list-field",
new OrdinaryListHolder
{
Values = list,
},
writer,
reader,
(expected, decoded) =>
OrdinaryListsEqual(expected.Values, decoded.Values),
options,
cases);
AddCase(
"list-root",
list,
writer,
reader,
OrdinaryListsEqual,
options,
cases);
Dictionary<string, OrdinaryUser> map = EquivalenceData.CreateOrdinaryMap();
AddCase(
"map-field",
new OrdinaryMapHolder
{
Values = map,
},
writer,
reader,
(expected, decoded) =>
OrdinaryMapsEqual(expected.Values, decoded.Values),
options,
cases);
AddCase(
"map-root",
map,
writer,
reader,
OrdinaryMapsEqual,
options,
cases);
return cases;
}
private static List<EquivalenceCase> BuildExternalCases(
ExternalBenchmarkOptions options)
{
ForyRuntime writer = BuildFory();
ForyRuntime reader = BuildFory();
RegisterTargetTypes(writer);
RegisterTargetTypes(reader);
List<EquivalenceCase> cases = [];
AddCase(
"class-root",
EquivalenceData.CreateExternalUser(),
writer,
reader,
ExternalUsersEqual,
options,
cases);
AddCase(
"struct-root",
EquivalenceData.CreateExternalPoint(),
writer,
reader,
(expected, decoded) =>
expected.X == decoded.X && expected.Y == decoded.Y,
options,
cases);
AddCase(
"holder-field",
new ExternalUserHolder
{
Value = EquivalenceData.CreateExternalUser(),
},
writer,
reader,
(expected, decoded) =>
ExternalUsersEqual(expected.Value, decoded.Value),
options,
cases);
List<ExternalUser> list = EquivalenceData.CreateExternalList();
AddCase(
"list-field",
new ExternalListHolder
{
Values = list,
},
writer,
reader,
(expected, decoded) =>
ExternalListsEqual(expected.Values, decoded.Values),
options,
cases);
AddCase(
"list-root",
list,
writer,
reader,
ExternalListsEqual,
options,
cases);
Dictionary<string, ExternalUser> map = EquivalenceData.CreateExternalMap();
AddCase(
"map-field",
new ExternalMapHolder
{
Values = map,
},
writer,
reader,
(expected, decoded) =>
ExternalMapsEqual(expected.Values, decoded.Values),
options,
cases);
AddCase(
"map-root",
map,
writer,
reader,
ExternalMapsEqual,
options,
cases);
return cases;
}
private static ForyRuntime BuildFory()
{
return ForyRuntime.Builder().Compatible(true).Build();
}
private static void RegisterOrdinary(ForyRuntime fory)
{
fory.Register<OrdinaryUser>(UserTypeId);
fory.Register<OrdinaryPoint>(PointTypeId);
fory.Register<OrdinaryUserHolder>(UserHolderTypeId);
fory.Register<OrdinaryListHolder>(ListHolderTypeId);
fory.Register<OrdinaryMapHolder>(MapHolderTypeId);
}
private static void RegisterTargetTypes(ForyRuntime fory)
{
fory.Register<ExternalUser>(UserTypeId);
fory.Register<ExternalPoint>(PointTypeId);
fory.Register<ExternalUserHolder>(UserHolderTypeId);
fory.Register<ExternalListHolder>(ListHolderTypeId);
fory.Register<ExternalMapHolder>(MapHolderTypeId);
}
private static void AddCase<T>(
string dataType,
T value,
ForyRuntime writer,
ForyRuntime reader,
Func<T, T, bool> valuesEqual,
ExternalBenchmarkOptions options,
List<EquivalenceCase> cases)
{
if (!options.IsDataEnabled(dataType))
{
return;
}
byte[] payload = writer.Serialize(value);
T decoded = reader.Deserialize<T>(payload);
if (!valuesEqual(value, decoded))
{
throw new InvalidOperationException(
$"decoded value differs for {dataType}");
}
string frameBase64 = Convert.ToBase64String(payload);
Action serialize = () =>
{
EquivalenceSink<byte[]>.Value = writer.Serialize(value);
};
Action deserialize = () =>
{
EquivalenceSink<T>.Value = reader.Deserialize<T>(payload);
};
cases.Add(new EquivalenceCase(
dataType,
"serialize",
payload.Length,
frameBase64,
serialize));
cases.Add(new EquivalenceCase(
dataType,
"deserialize",
payload.Length,
frameBase64,
deserialize));
}
private static bool OrdinaryUsersEqual(
OrdinaryUser expected,
OrdinaryUser decoded)
{
return expected.Id == decoded.Id
&& expected.Score == decoded.Score
&& expected.Name == decoded.Name;
}
private static bool ExternalUsersEqual(
ExternalUser expected,
ExternalUser decoded)
{
return expected.Id == decoded.Id
&& expected.Score == decoded.Score
&& expected.Name == decoded.Name;
}
private static bool OrdinaryListsEqual(
IReadOnlyList<OrdinaryUser> expected,
IReadOnlyList<OrdinaryUser> decoded)
{
if (expected.Count != decoded.Count)
{
return false;
}
for (int i = 0; i < expected.Count; i++)
{
if (!OrdinaryUsersEqual(expected[i], decoded[i]))
{
return false;
}
}
return true;
}
private static bool ExternalListsEqual(
IReadOnlyList<ExternalUser> expected,
IReadOnlyList<ExternalUser> decoded)
{
if (expected.Count != decoded.Count)
{
return false;
}
for (int i = 0; i < expected.Count; i++)
{
if (!ExternalUsersEqual(expected[i], decoded[i]))
{
return false;
}
}
return true;
}
private static bool OrdinaryMapsEqual(
IReadOnlyDictionary<string, OrdinaryUser> expected,
IReadOnlyDictionary<string, OrdinaryUser> decoded)
{
if (expected.Count != decoded.Count)
{
return false;
}
foreach ((string key, OrdinaryUser expectedUser) in expected)
{
if (!decoded.TryGetValue(key, out OrdinaryUser? decodedUser)
|| !OrdinaryUsersEqual(expectedUser, decodedUser))
{
return false;
}
}
return true;
}
private static bool ExternalMapsEqual(
IReadOnlyDictionary<string, ExternalUser> expected,
IReadOnlyDictionary<string, ExternalUser> decoded)
{
if (expected.Count != decoded.Count)
{
return false;
}
foreach ((string key, ExternalUser expectedUser) in expected)
{
if (!decoded.TryGetValue(key, out ExternalUser? decodedUser)
|| !ExternalUsersEqual(expectedUser, decodedUser))
{
return false;
}
}
return true;
}
private static string ImplementationName(ExternalImplementation implementation)
{
return implementation switch
{
ExternalImplementation.Ordinary => "ordinary",
ExternalImplementation.External => "external",
_ => throw new ArgumentException(
"external-equivalence requires one selected implementation"),
};
}
private static void PrintSummary(
string implementation,
List<EquivalenceSideResult> results)
{
Console.WriteLine();
Console.WriteLine(
$"=== External-Type {implementation} Summary ===");
foreach (EquivalenceSideResult result in results)
{
Console.WriteLine(
$"{result.DataType}/{result.Operation}: "
+ $"{result.Measurement.AverageNanoseconds:N1} ns/op");
Console.WriteLine(
$" allocated={result.AllocatedBytesPerOperation:N2} B/op");
}
}
private static class EquivalenceSink<T>
{
public static T Value = default!;
}
}