diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
new file mode 100644
index 0000000..834cfc7
--- /dev/null
+++ b/.github/workflows/benchmarks.yml
@@ -0,0 +1,79 @@
+name: Benchmarks
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ deployments: write
+
+jobs:
+ benchmark:
+ name: Run BenchmarkDotNet and publish charts
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 8.x
+ 9.x
+
+ - name: Restore
+ run: dotnet restore benchmarks/NetFramework.Tasks.Management.Benchmarks/NetFramework.Tasks.Management.Benchmarks.csproj
+
+ - name: Run benchmarks (net8.0)
+ run: |
+ dotnet run -c Release -f net8.0 \
+ --project benchmarks/NetFramework.Tasks.Management.Benchmarks/ \
+ -- --filter '*' \
+ --exporters Json \
+ --iterationCount 5 \
+ --warmupCount 3 \
+ --artifacts /tmp/bdn-net8
+
+ - name: Run benchmarks (net9.0)
+ run: |
+ dotnet run -c Release -f net9.0 \
+ --project benchmarks/NetFramework.Tasks.Management.Benchmarks/ \
+ -- --filter '*' \
+ --exporters Json \
+ --iterationCount 5 \
+ --warmupCount 3 \
+ --artifacts /tmp/bdn-net9
+
+ - name: Merge JSON results
+ run: |
+ # Collect all BenchmarkDotNet JSON result files from both runs
+ JSON_FILES=$(find /tmp/bdn-net8/results /tmp/bdn-net9/results -name '*.json' 2>/dev/null | tr '\n' ' ')
+ echo "Found JSON files: $JSON_FILES"
+
+ # BenchmarkDotNet JSON format: { "Title": "...", "Benchmarks": [...] }
+ # Merge all into a single array under "Benchmarks"
+ jq -s '
+ {
+ Title: "NetFramework.Tasks.Management Benchmarks",
+ Benchmarks: (map(.Benchmarks) | add)
+ }
+ ' $JSON_FILES > /tmp/bdn-merged.json
+
+ echo "Merged benchmark count: $(jq '.Benchmarks | length' /tmp/bdn-merged.json)"
+
+ - name: Store benchmark results
+ uses: benchmark-action/github-action-benchmark@v1
+ with:
+ tool: benchmarkdotnet
+ output-file-path: /tmp/bdn-merged.json
+ gh-pages-branch: gh-pages
+ benchmark-data-dir-path: dev/bench
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ auto-push: true
+ comment-on-alert: true
+ alert-threshold: '150%'
+ fail-on-alert: false
diff --git a/README.md b/README.md
index 621fcce..a892b3f 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,27 @@ if (_tasks.DequeueTaskDisposedDataModel(out var record) == TaskManagementStatus.
}
```
+## Benchmarks
+
+Performance is tracked automatically on every push to `main` and published as interactive charts:
+
+**[View benchmark charts](https://ryujose.github.io/NetTaskManagement/dev/bench/)**
+
+Benchmarks cover:
+- `LifecycleBenchmarks` — individual lifecycle stages: Register, Start, Cancel, Delete, and full end-to-end
+- `GetTasksStatusBenchmarks` — dictionary snapshot cost at 1, 10, and 50 tasks
+- `CancelAllTasksBenchmarks` — `Parallel.ForEach` cancellation fan-out at 1, 10, and 50 tasks
+
+All benchmarks run on **net8.0** and **net9.0** with `[MemoryDiagnoser]` enabled (reports allocated bytes per operation).
+
+To run locally (Release mode required):
+
+```bash
+dotnet run -c Release -f net8.0 --project benchmarks/NetFramework.Tasks.Management.Benchmarks/ -- --filter '*'
+```
+
+See [benchmarks/README.md](benchmarks/NetFramework.Tasks.Management.Benchmarks/README.md) for full run instructions and filter examples.
+
## Examples
| Example | Description |
diff --git a/benchmarks/NetFramework.Tasks.Management.Benchmarks/CancelAllTasksBenchmarks.cs b/benchmarks/NetFramework.Tasks.Management.Benchmarks/CancelAllTasksBenchmarks.cs
new file mode 100644
index 0000000..e8609f3
--- /dev/null
+++ b/benchmarks/NetFramework.Tasks.Management.Benchmarks/CancelAllTasksBenchmarks.cs
@@ -0,0 +1,70 @@
+using BenchmarkDotNet.Attributes;
+using Microsoft.Extensions.Logging.Abstractions;
+using NetFramework.Tasks.Management.Abstractions.Enums;
+using System;
+using System.Collections.Concurrent;
+using System.Threading;
+
+namespace NetFramework.Tasks.Management.Benchmarks
+{
+ ///
+ /// Measures CancelAllTasks() as the number of running tasks scales.
+ ///
+ /// [InvocationCount(1)] is required: CancelAllTasks mutates CTS state so a second
+ /// call in the same iteration would see already-cancelled tokens and skew results.
+ /// IterationSetup re-registers and re-starts all tasks before each measurement.
+ ///
+ [MemoryDiagnoser]
+ [InvocationCount(1)]
+ public class CancelAllTasksBenchmarks
+ {
+ private static readonly Action