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
17 changes: 16 additions & 1 deletion .github/workflows/publish-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
signing_enabled:
description: "Whether package signing was enabled for this run."
value: ${{ jobs.package.outputs.signing_enabled }}
signature_verification_enabled:
description: "Whether signed package verification was enabled for this run."
value: ${{ jobs.package.outputs.signature_verification_enabled }}
inputs:
package_version:
description: "Optional package version, usually the release tag without the leading v."
Expand Down Expand Up @@ -47,6 +50,8 @@ on:
required: false
NUGET_SIGN_TIMESTAMP_URL:
required: false
NUGET_SIGN_SKIP_VERIFICATION:
required: false

permissions:
contents: read
Expand All @@ -57,6 +62,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
signing_enabled: ${{ steps.signing.outputs.enabled }}
signature_verification_enabled: ${{ steps.signing.outputs.verify_enabled }}

steps:
- name: Checkout
Expand Down Expand Up @@ -150,6 +156,7 @@ jobs:
NUGET_SIGN_CERTIFICATE_PASSWORD: ${{ secrets.NUGET_SIGN_CERTIFICATE_PASSWORD }}
NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT: ${{ secrets.NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT }}
NUGET_SIGN_TIMESTAMP_URL: ${{ secrets.NUGET_SIGN_TIMESTAMP_URL }}
NUGET_SIGN_SKIP_VERIFICATION: ${{ secrets.NUGET_SIGN_SKIP_VERIFICATION }}
run: |
if [ -n "$NUGET_SIGN_CERTIFICATE_BASE64" ] \
&& [ -n "$NUGET_SIGN_CERTIFICATE_PASSWORD" ] \
Expand All @@ -162,6 +169,14 @@ jobs:
echo "Package signing disabled. Continuing with unsigned packages."
fi

if [ "${NUGET_SIGN_SKIP_VERIFICATION,,}" = "true" ]; then
echo "verify_enabled=false" >> "$GITHUB_OUTPUT"
echo "Signed package verification disabled by NUGET_SIGN_SKIP_VERIFICATION."
else
echo "verify_enabled=true" >> "$GITHUB_OUTPUT"
echo "Signed package verification enabled."
fi

- name: Import signing certificate
if: ${{ steps.signing.outputs.enabled == 'true' }}
env:
Expand All @@ -186,7 +201,7 @@ jobs:
done

- name: Verify signed packages
if: ${{ steps.signing.outputs.enabled == 'true' }}
if: ${{ steps.signing.outputs.enabled == 'true' && steps.signing.outputs.verify_enabled == 'true' }}
env:
NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT: ${{ secrets.NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT }}
run: |
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/publish-attested.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ on:
required: false
NUGET_SIGN_TIMESTAMP_URL:
required: false
NUGET_SIGN_SKIP_VERIFICATION:
required: false

permissions:
contents: write
Expand All @@ -39,6 +41,7 @@ jobs:
NUGET_SIGN_CERTIFICATE_PASSWORD: ${{ secrets.NUGET_SIGN_CERTIFICATE_PASSWORD }}
NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT: ${{ secrets.NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT }}
NUGET_SIGN_TIMESTAMP_URL: ${{ secrets.NUGET_SIGN_TIMESTAMP_URL }}
NUGET_SIGN_SKIP_VERIFICATION: ${{ secrets.NUGET_SIGN_SKIP_VERIFICATION }}

release:
name: Upload artifacts to draft release
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/publish-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ jobs:
NUGET_SIGN_CERTIFICATE_PASSWORD: ${{ secrets.NUGET_SIGN_CERTIFICATE_PASSWORD }}
NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT: ${{ secrets.NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT }}
NUGET_SIGN_TIMESTAMP_URL: ${{ secrets.NUGET_SIGN_TIMESTAMP_URL }}
NUGET_SIGN_SKIP_VERIFICATION: ${{ secrets.NUGET_SIGN_SKIP_VERIFICATION }}

publish-nuget:
name: Publish to NuGet.org
Expand All @@ -130,7 +131,7 @@ jobs:
merge-multiple: true

- name: Verify signed packages
if: ${{ needs.package.outputs.signing_enabled == 'true' }}
if: ${{ needs.package.outputs.signing_enabled == 'true' && needs.package.outputs.signature_verification_enabled == 'true' }}
env:
NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT: ${{ secrets.NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT }}
run: |
Expand Down Expand Up @@ -180,7 +181,7 @@ jobs:
merge-multiple: true

- name: Verify signed packages
if: ${{ needs.package.outputs.signing_enabled == 'true' }}
if: ${{ needs.package.outputs.signing_enabled == 'true' && needs.package.outputs.signature_verification_enabled == 'true' }}
env:
NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT: ${{ secrets.NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT }}
run: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ jobs:
NUGET_SIGN_CERTIFICATE_PASSWORD: ${{ secrets.NUGET_SIGN_CERTIFICATE_PASSWORD }}
NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT: ${{ secrets.NUGET_SIGN_CERTIFICATE_SHA256_FINGERPRINT }}
NUGET_SIGN_TIMESTAMP_URL: ${{ secrets.NUGET_SIGN_TIMESTAMP_URL }}
NUGET_SIGN_SKIP_VERIFICATION: ${{ secrets.NUGET_SIGN_SKIP_VERIFICATION }}
88 changes: 88 additions & 0 deletions Benchmarks/Concurrency/BatchSchedulingBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
using ModularityKit.Mutator.Abstractions.Audit;
using ModularityKit.Mutator.Abstractions.Engine;
using ModularityKit.Mutator.Abstractions.History;
using ModularityKit.Mutator.Benchmarks.Concurrency.Support;
using ModularityKit.Mutator.Benchmarks.Diagnostics.Support;

namespace ModularityKit.Mutator.Benchmarks.Concurrency;

/// <summary>
/// Benchmarks concurrent batch executions competing for limited runtime availability.
/// </summary>
[BenchmarkCategory("Concurrency")]
[MemoryDiagnoser]
[InProcess]
public class BatchSchedulingBenchmarks
{
private const int RuntimeSlots = 2;

private IMutationEngine _engine = null!;
private BatchScenario[] _scenarios = null!;

/// <summary>
/// Controls how many batch executions compete for runtime slots during a single benchmark iteration.
/// </summary>
[Params(2, 4)]
public int ConcurrentBatches { get; set; }

/// <summary>
/// Controls how many mutations each competing batch executes.
/// </summary>
[Params(4, 16)]
public int BatchSize { get; set; }

/// <summary>
/// Prepares the engine and precomputed batch scenarios for the selected parameters.
/// </summary>
[GlobalSetup]
public void Setup()
{
_engine = ConcurrencyBenchmarkScenario.BuildEngine(
RuntimeSlots,
services =>
{
services.AddSingleton<IMutationAuditor, NoOpAuditor>();
services.AddSingleton<IMutationHistoryStore, NoOpHistoryStore>();
});

_scenarios = Enumerable
.Range(0, ConcurrentBatches)
.Select(CreateBatchScenario)
.ToArray();
}

/// <summary>
/// Measures scheduler pressure when several ordered batches compete for a limited number of engine slots.
/// </summary>
[Benchmark]
public async Task ConcurrentBatches_LimitedRuntimeAvailability()
{
var tasks = new Task[ConcurrentBatches];

for (var index = 0; index < ConcurrentBatches; index++)
{
var scenario = _scenarios[index];
tasks[index] = _engine.ExecuteBatchAsync(scenario.Mutations, scenario.State);
}

await Task.WhenAll(tasks);
GC.KeepAlive(tasks);
}

private BatchScenario CreateBatchScenario(int batchIndex)
{
var stateId = $"batch-state-{batchIndex}";
var mutations = Enumerable
.Range(0, BatchSize)
.Select(step => (IMutation<ConcurrencyState>)ConcurrencyBenchmarkScenario.CreateCommitMutation(stateId, $"batch-{batchIndex}-{step}"))
.ToArray();

return new BatchScenario(new ConcurrencyState(batchIndex, 0), mutations);
}

private sealed record BatchScenario(
ConcurrencyState State,
IReadOnlyList<IMutation<ConcurrencyState>> Mutations);
}
64 changes: 64 additions & 0 deletions Benchmarks/Concurrency/GateContentionBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
using ModularityKit.Mutator.Abstractions.Audit;
using ModularityKit.Mutator.Abstractions.Engine;
using ModularityKit.Mutator.Abstractions.History;
using ModularityKit.Mutator.Benchmarks.Concurrency.Support;
using ModularityKit.Mutator.Benchmarks.Diagnostics.Support;

namespace ModularityKit.Mutator.Benchmarks.Concurrency;

/// <summary>
/// Benchmarks state-level gate contention in the core mutation runtime.
/// </summary>
[BenchmarkCategory("Concurrency")]
[MemoryDiagnoser]
[InProcess]
public class GateContentionBenchmarks
{
private const string SharedStateId = "shared-concurrency-state";

private IMutationEngine _engine = null!;
private ConcurrencyState _state = null!;

/// <summary>
/// Prepares an engine with a concurrency limit high enough to isolate state-gate contention.
/// </summary>
[GlobalSetup]
public void Setup()
{
_engine = ConcurrencyBenchmarkScenario.BuildEngine(
maxConcurrentMutations: 4,
configureServices: services =>
{
services.AddSingleton<IMutationAuditor, NoOpAuditor>();
services.AddSingleton<IMutationHistoryStore, NoOpHistoryStore>();
});

_state = new ConcurrencyState(42, 0);
}

/// <summary>
/// Measures two concurrent executions targeting the same state identifier while one execution blocks the gate.
/// </summary>
[Benchmark]
public async Task SharedStateGate_TwoConcurrentExecutions()
{
using var gate = new BlockingMutationGate();
var firstMutation = ConcurrencyBenchmarkScenario.CreateBlockingMutation(gate, SharedStateId, "first");
var secondMutation = ConcurrencyBenchmarkScenario.CreateCommitMutation(SharedStateId, "second");

var firstTask = _engine.ExecuteAsync(firstMutation, _state);

if (!gate.WaitForEntries(expectedEntries: 1, timeout: TimeSpan.FromSeconds(5)))
throw new InvalidOperationException("Blocking benchmark mutation did not enter the gate in time.");

var secondTask = _engine.ExecuteAsync(secondMutation, _state);

Thread.SpinWait(100_000);
gate.Release();

var results = await Task.WhenAll(firstTask, secondTask);
GC.KeepAlive(results);
}
}
68 changes: 68 additions & 0 deletions Benchmarks/Concurrency/ParallelExecutionBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using BenchmarkDotNet.Attributes;
using Microsoft.Extensions.DependencyInjection;
using ModularityKit.Mutator.Abstractions.Audit;
using ModularityKit.Mutator.Abstractions.Engine;
using ModularityKit.Mutator.Abstractions.History;
using ModularityKit.Mutator.Benchmarks.Concurrency.Support;
using ModularityKit.Mutator.Benchmarks.Diagnostics.Support;

namespace ModularityKit.Mutator.Benchmarks.Concurrency;

/// <summary>
/// Benchmarks parallel execution throughput across distinct runtime state identifiers.
/// </summary>
[BenchmarkCategory("Concurrency")]
[MemoryDiagnoser]
[InProcess]
public class ParallelExecutionBenchmarks
{
private IMutationEngine _engine = null!;
private ConcurrencyState[] _states = null!;
private IncrementConcurrencyMutation[] _mutations = null!;

/// <summary>
/// Controls how many distinct mutation executions run in parallel during a single benchmark iteration.
/// </summary>
[Params(2, 8)]
public int Parallelism { get; set; }

/// <summary>
/// Prepares the engine, state snapshots, and mutation list for the selected parallelism level.
/// </summary>
[GlobalSetup]
public void Setup()
{
_engine = ConcurrencyBenchmarkScenario.BuildEngine(
Parallelism,
services =>
{
services.AddSingleton<IMutationAuditor, NoOpAuditor>();
services.AddSingleton<IMutationHistoryStore, NoOpHistoryStore>();
});

_states = Enumerable
.Range(0, Parallelism)
.Select(index => new ConcurrencyState(index, 0))
.ToArray();

_mutations = Enumerable
.Range(0, Parallelism)
.Select(index => ConcurrencyBenchmarkScenario.CreateCommitMutation($"parallel-state-{index}", $"parallel-{index}"))
.ToArray();
}

/// <summary>
/// Measures concurrent execution across distinct state identifiers without diagnostics storage noise.
/// </summary>
[Benchmark(Baseline = true)]
public async Task ParallelDistinctStates_ExecuteAsync()
{
var tasks = new Task[Parallelism];

for (var index = 0; index < Parallelism; index++)
tasks[index] = _engine.ExecuteAsync(_mutations[index], _states[index]);

await Task.WhenAll(tasks);
GC.KeepAlive(tasks);
}
}
68 changes: 68 additions & 0 deletions Benchmarks/Concurrency/Support/BlockingGateMutation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using ModularityKit.Mutator.Abstractions;
using ModularityKit.Mutator.Abstractions.Changes;
using ModularityKit.Mutator.Abstractions.Context;
using ModularityKit.Mutator.Abstractions.Engine;
using ModularityKit.Mutator.Abstractions.Intent;
using ModularityKit.Mutator.Abstractions.Results;

namespace ModularityKit.Mutator.Benchmarks.Concurrency.Support;

/// <summary>
/// Minimal commit mutation that blocks inside the execution pipeline to expose gate contention.
/// </summary>
internal sealed class BlockingGateMutation(
MutationContext context,
BlockingMutationGate gate) : IMutation<ConcurrencyState>
{
/// <summary>
/// Gets the benchmark mutation intent metadata.
/// </summary>
public MutationIntent Intent { get; } = new()
{
OperationName = "BlockingGateMutation",
Category = "Benchmark",
Description = "Block inside mutation execution to expose core runtime gate contention.",
RiskLevel = MutationRiskLevel.Low,
IsReversible = true
};

/// <summary>
/// Gets the execution context bound to the benchmark mutation instance.
/// </summary>
public MutationContext Context { get; } = context;

/// <summary>
/// Applies the benchmark mutation after waiting on the shared gate.
/// </summary>
/// <param name="state">The input state.</param>
/// <returns>The successful mutation result containing the updated state and changes.</returns>
public MutationResult<ConcurrencyState> Apply(ConcurrencyState state)
{
gate.Enter();

var nextState = state with
{
Counter = state.Counter + 1,
Revision = state.Revision + 1
};

return MutationResult<ConcurrencyState>.Success(
nextState,
ChangeSet.Single(
StateChange.Modified(nameof(ConcurrencyState.Counter), state.Counter, nextState.Counter)));
}

/// <summary>
/// Validates the provided state before mutation execution.
/// </summary>
/// <param name="state">The input state.</param>
/// <returns>A successful validation result.</returns>
public ValidationResult Validate(ConcurrencyState state) => ValidationResult.Success();

/// <summary>
/// Simulates the benchmark mutation using the same state transition as commit execution.
/// </summary>
/// <param name="state">The input state.</param>
/// <returns>The simulated mutation result.</returns>
public MutationResult<ConcurrencyState> Simulate(ConcurrencyState state) => Apply(state);
}
Loading
Loading