Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
os: ${{ fromJson(github.event_name == 'workflow_dispatch' && '["ubuntu-latest","windows-latest","macos-latest"]' || '["ubuntu-latest"]') }}

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For workflow_dispatch, the matrix always schedules all 3 OS jobs even when an input (e.g. run_windows) is false; those jobs still provision a runner just to run the gate step and then skip the rest. Consider building the matrix.os list from the dispatch inputs so disabled OS values are omitted entirely, reducing runner usage and queue time.

Suggested change
os: ${{ fromJson(github.event_name == 'workflow_dispatch' && '["ubuntu-latest","windows-latest","macos-latest"]' || '["ubuntu-latest"]') }}
os: ${{ fromJson(github.event_name == 'workflow_dispatch' && format('[{0}{1}{2}]',
inputs.run_ubuntu && '"ubuntu-latest"' || '',
inputs.run_windows && (inputs.run_ubuntu && ',"windows-latest"' || '"windows-latest"') || '',
inputs.run_macos && ((inputs.run_ubuntu || inputs.run_windows) && ',"macos-latest"' || '"macos-latest"') || ''
) || '["ubuntu-latest"]') }}

Copilot uses AI. Check for mistakes.

steps:
- name: Select target OS execution
Expand All @@ -44,11 +44,9 @@ jobs:
run: |
$eventName = "${{ github.event_name }}"
$os = "${{ matrix.os }}"
$shouldRun = $false
$shouldRun = $true

if ($eventName -ne "workflow_dispatch") {
$shouldRun = $os -eq "ubuntu-latest"
} else {
if ($eventName -eq "workflow_dispatch") {
switch ($os) {
"ubuntu-latest" { $shouldRun = "${{ inputs.run_ubuntu }}" -eq "true"; break }
"windows-latest" { $shouldRun = "${{ inputs.run_windows }}" -eq "true"; break }
Expand Down Expand Up @@ -104,7 +102,7 @@ jobs:

$runsResponse = gh api "repos/$repo/actions/workflows/$workflowFile/runs?branch=next&event=push&status=success&per_page=50"
$runs = ($runsResponse | ConvertFrom-Json).workflow_runs
$artifactId = $null
$baselineRunId = $null

foreach ($run in $runs) {
if ($run.id -eq ${{ github.run_id }}) {
Expand All @@ -117,20 +115,18 @@ jobs:
Select-Object -First 1

if ($artifact) {
$artifactId = $artifact.id
$baselineRunId = $run.id
break
}
}

if (-not $artifactId) {
if (-not $baselineRunId) {
Write-Host "No baseline artifact found for '$artifactName'."
exit 0
}

New-Item -ItemType Directory -Force -Path baseline | Out-Null
gh api "repos/$repo/actions/artifacts/$artifactId/zip" --output baseline/baseline.zip
Expand-Archive -Path baseline/baseline.zip -DestinationPath baseline -Force
Remove-Item baseline/baseline.zip -Force
gh run download $baselineRunId --name $artifactName --dir baseline --repo $repo

$baselineJson = Get-ChildItem -Path baseline -Filter "current-normalized.json" -Recurse | Select-Object -First 1
if ($baselineJson) {
Expand Down
16 changes: 8 additions & 8 deletions src/FastCloner.Benchmark.CI/Reporting/BenchmarkDiffReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,13 @@ private static string BuildCommentMarkdown(
sb.AppendLine($"- Baseline generated (UTC): `{baseline.GeneratedAtUtc:yyyy-MM-dd HH:mm:ss}`");
sb.AppendLine($"- Regression thresholds: time > `{FormatPercent(options.TimeThreshold)}`, alloc > `{FormatPercent(options.AllocThreshold)}`");
sb.AppendLine();
sb.AppendLine("| Benchmark | FC Time (Baseline) | FC Time (Current) | Delta Time | FC Alloc (Baseline) | FC Alloc (Current) | Delta Alloc | Status |");
sb.AppendLine("|---|---:|---:|---|---:|---:|---|---|");
sb.AppendLine("| Status | Benchmark | FC Time (Baseline) | FC Time (Current) | Delta Time | FC Alloc (Baseline) | FC Alloc (Current) | Delta Alloc |");
sb.AppendLine("|---|---|---:|---:|---|---:|---:|---|");

foreach (BaselineDiffItem item in diff.Items)
{
sb.AppendLine(
$"| {item.Benchmark} | {FormatNanoseconds(item.BaselineMeanNanoseconds)} | {FormatNanoseconds(item.CurrentMeanNanoseconds)} | {FormatCurrentDelta(item.TimeDeltaRatio, options.SameThreshold, "faster", "slower")} | {FormatBytes(item.BaselineAllocatedBytes)} | {FormatBytes(item.CurrentAllocatedBytes)} | {FormatCurrentDelta(item.AllocDeltaRatio, options.SameThreshold, "less", "more")} | {FormatStatus(item.Status)} |");
$"| {FormatStatus(item.Status)} | {item.Benchmark} | {FormatNanoseconds(item.BaselineMeanNanoseconds)} | {FormatNanoseconds(item.CurrentMeanNanoseconds)} | {FormatCurrentDelta(item.TimeDeltaRatio, options.SameThreshold, "faster", "slower")} | {FormatBytes(item.BaselineAllocatedBytes)} | {FormatBytes(item.CurrentAllocatedBytes)} | {FormatCurrentDelta(item.AllocDeltaRatio, options.SameThreshold, "less", "more")} |");
}

AppendStatusSection(sb, "Regressions", diff.Items.Where(item => item.Status == DiffStatus.Regression).ToList(), options.SameThreshold);
Expand Down Expand Up @@ -333,11 +333,11 @@ private static string FormatStatus(DiffStatus status)
{
return status switch
{
DiffStatus.Regression => "regression",
DiffStatus.Improvement => "improvement",
DiffStatus.Mixed => "mixed",
DiffStatus.NewBenchmark => "new",
_ => "stable"
DiffStatus.Regression => "🔴",
DiffStatus.Improvement => "🟢",
DiffStatus.Mixed => "🟡",
DiffStatus.NewBenchmark => "🆕",
_ => ""
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,7 @@ private static double ParseTimeToNanoseconds(string value)
"us" => amount * 1000d,
"ms" => amount * 1_000_000d,
"s" => amount * 1_000_000_000d,
"μs" => amount * 1000d,
"µs" => amount * 1000d,
"μs" or "µs" => amount * 1000d,
_ => amount
};
}
Expand Down
112 changes: 112 additions & 0 deletions src/FastCloner/Code/FastClonerGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ private struct TypeCloneDispatchCache2
private FastClonerCache.TypeCloneMetadata? metadataB;
private Func<object, FastCloneState, object>? recursiveB;
private Func<object, FastCloneState, object>? worklistB;
private Type? typeC;
private FastClonerCache.TypeCloneMetadata? metadataC;
private Func<object, FastCloneState, object>? recursiveC;
private Func<object, FastCloneState, object>? worklistC;
private Type? typeD;
private FastClonerCache.TypeCloneMetadata? metadataD;
private Func<object, FastCloneState, object>? recursiveD;
private Func<object, FastCloneState, object>? worklistD;

public void Resolve(
Type runtimeType,
Expand All @@ -93,10 +101,38 @@ public void Resolve(
return;
}

if (runtimeType == typeC && metadataC is not null)
{
metadata = metadataC;
recursiveCloner = recursiveC;
worklistCloner = worklistC;
PromoteThirdToFirst();
return;
}

if (runtimeType == typeD && metadataD is not null)
{
metadata = metadataD;
recursiveCloner = recursiveD;
worklistCloner = worklistD;
PromoteFourthToFirst();
return;
}

metadata = GetTypeMetadata(runtimeType, state);
recursiveCloner = metadata.RecursiveCloner;
worklistCloner = metadata.WorklistCloner ?? recursiveCloner;

typeD = typeC;
metadataD = metadataC;
recursiveD = recursiveC;
worklistD = worklistC;

typeC = typeB;
metadataC = metadataB;
recursiveC = recursiveB;
worklistC = worklistB;

typeB = typeA;
metadataB = metadataA;
recursiveB = recursiveA;
Expand Down Expand Up @@ -128,6 +164,72 @@ private void PromoteSecondToFirst()
worklistA = worklistB;
worklistB = previousWorklistA;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
[SuppressMessage("ReSharper", "SwapViaDeconstruction")]
private void PromoteThirdToFirst()
{
Type? previousTypeA = typeA;
Type? previousTypeB = typeB;
typeA = typeC;
typeB = previousTypeA;
typeC = previousTypeB;

FastClonerCache.TypeCloneMetadata? previousMetadataA = metadataA;
FastClonerCache.TypeCloneMetadata? previousMetadataB = metadataB;
metadataA = metadataC;
metadataB = previousMetadataA;
metadataC = previousMetadataB;

Func<object, FastCloneState, object>? previousRecursiveA = recursiveA;
Func<object, FastCloneState, object>? previousRecursiveB = recursiveB;
recursiveA = recursiveC;
recursiveB = previousRecursiveA;
recursiveC = previousRecursiveB;

Func<object, FastCloneState, object>? previousWorklistA = worklistA;
Func<object, FastCloneState, object>? previousWorklistB = worklistB;
worklistA = worklistC;
worklistB = previousWorklistA;
worklistC = previousWorklistB;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
[SuppressMessage("ReSharper", "SwapViaDeconstruction")]
private void PromoteFourthToFirst()
{
Type? previousTypeA = typeA;
Type? previousTypeB = typeB;
Type? previousTypeC = typeC;
typeA = typeD;
typeB = previousTypeA;
typeC = previousTypeB;
typeD = previousTypeC;

FastClonerCache.TypeCloneMetadata? previousMetadataA = metadataA;
FastClonerCache.TypeCloneMetadata? previousMetadataB = metadataB;
FastClonerCache.TypeCloneMetadata? previousMetadataC = metadataC;
metadataA = metadataD;
metadataB = previousMetadataA;
metadataC = previousMetadataB;
metadataD = previousMetadataC;

Func<object, FastCloneState, object>? previousRecursiveA = recursiveA;
Func<object, FastCloneState, object>? previousRecursiveB = recursiveB;
Func<object, FastCloneState, object>? previousRecursiveC = recursiveC;
recursiveA = recursiveD;
recursiveB = previousRecursiveA;
recursiveC = previousRecursiveB;
recursiveD = previousRecursiveC;

Func<object, FastCloneState, object>? previousWorklistA = worklistA;
Func<object, FastCloneState, object>? previousWorklistB = worklistB;
Func<object, FastCloneState, object>? previousWorklistC = worklistC;
worklistA = worklistD;
worklistB = previousWorklistA;
worklistC = previousWorklistB;
worklistD = previousWorklistC;
}
}

internal static FastClonerCache.TypeCloneMetadata GetTypeMetadata(Type type) =>
Expand Down Expand Up @@ -862,6 +964,7 @@ internal static T[] Clone1DimArraySafeInternal<T>(T[] obj, FastCloneState state)
return outArray;
}

bool hasOptionalTypeOverrides = FastClonerCache.HasActiveTypeBehaviorOverrides;
TypeCloneDispatchCache2 dispatch = default;
for (int i = 0; i < l; i++)
{
Expand All @@ -873,6 +976,15 @@ internal static T[] Clone1DimArraySafeInternal<T>(T[] obj, FastCloneState state)
}

Type runtimeType = item.GetType();
if (!hasOptionalTypeOverrides &&
((!runtimeType.IsValueType && FastClonerSafeTypes.CanReturnSameObject(runtimeType)) ||
runtimeType.IsPrimitive ||
runtimeType.IsEnum))
{
outArray[i] = item;
continue;
}
Comment thread
cursor[bot] marked this conversation as resolved.

dispatch.Resolve(runtimeType, state, out FastClonerCache.TypeCloneMetadata metadata, out Func<object, FastCloneState, object>? recursiveCloner, out Func<object, FastCloneState, object>? worklistCloner);

outArray[i] = (T)CloneClassInternalResolved(item, state, runtimeType, metadata, recursiveCloner, worklistCloner)!;
Expand Down
Loading