1. Install the upload CLI
Install perfall-cli once from PyPI on the machine that will publish benchmark results.
pip install perfall-cli
Documentation
Perfall accepts benchmark artifacts from seven supported adapters. Install
perfall-cli from PyPI once, generate the artifact for your stack,
and upload it with your project API key.
Install perfall-cli once from PyPI on the machine that will publish benchmark results.
pip install perfall-cli
Open the Projects page, create a project, and reveal the API key when you are ready to wire your benchmark job.
Pick the framework below that matches your stack and run its export command to create a JSON or JSONL artifact.
Run perfall ingest <adapter> with the artifact path and your project API key, then return after the next run to compare trends.
Want to confirm the CLI, API key, and dashboard path before wiring a real benchmark suite? Download the sample pytest-benchmark artifact and upload it with your project API key.
perfall ingest pytest-benchmark \
--file pytest-benchmark-sample.json \
--api-key "$PERFALL_API_KEY"
Python
Use pytest-benchmark's JSON export when your benchmark suite already lives in pytest.
def test_sort(benchmark):
values = [3, 1, 2]
benchmark(sorted, values)
pytest --benchmark-only --benchmark-json benchmark.json
perfall ingest pytest-benchmark --file benchmark.json --api-key "$PERFALL_API_KEY"
Go
Use Go's built-in benchmark support and upload the JSON event stream from `go test -bench`.
func BenchmarkSort(b *testing.B) {
for i := 0; i < b.N; i++ {
values := []int{3, 1, 2}
sort.Ints(values)
}
}
go test -bench . -benchmem -json ./... > go-benchmark.jsonl
perfall ingest go-test --file go-benchmark.jsonl --api-key "$PERFALL_API_KEY"
Java
Use JMH when you need stable JVM benchmark output and export the run as JSON.
@State(Scope.Thread)
public class SortBenchmark {
@Benchmark
public int[] sort() {
int[] values = {3, 1, 2};
Arrays.sort(values);
return values;
}
}
java -jar benchmarks.jar -rf json -rff jmh-result.json
perfall ingest jmh --file jmh-result.json --api-key "$PERFALL_API_KEY"
Rust
Use Criterion.rs with cargo-criterion when you want a machine-readable JSONL stream.
fn sort_benchmark(c: &mut Criterion) {
c.bench_function("sort_u64", |b| {
b.iter(|| {
let mut values = vec![3_u64, 1, 2];
values.sort();
})
});
}
cargo criterion --message-format=json > cargo-criterion.jsonl
perfall ingest criterion --file cargo-criterion.jsonl --api-key "$PERFALL_API_KEY"
JavaScript / TypeScript
Use Vitest benchmark mode when you want benchmark cases alongside your existing JS or TS tests.
import { bench, describe } from 'vitest';
describe('sort', () => {
bench('sort numbers', () => {
[3, 1, 2].sort();
});
});
npx vitest bench --outputJson vitest-benchmark.json
perfall ingest vitest --file vitest-benchmark.json --api-key "$PERFALL_API_KEY"
C++
Use Google Benchmark JSON output when your native benchmark suite already builds a benchmark binary.
static void BM_Sort(benchmark::State& state) {
for (auto _ : state) {
std::array<int, 3> values{3, 1, 2};
std::sort(values.begin(), values.end());
}
}
BENCHMARK(BM_Sort);
BENCHMARK_MAIN();
./benchmarks --benchmark_out=google-benchmark.json --benchmark_out_format=json
perfall ingest google-benchmark --file google-benchmark.json --api-key "$PERFALL_API_KEY"
.NET
Use BenchmarkDotNet with a JSON exporter when you want structured benchmark output from a .NET project.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Exporters.Json;
[JsonExporterAttribute.Brief]
public class MyBenchmarks
{
[Benchmark]
public void Sort()
{
var values = new[] { 3, 1, 2 };
Array.Sort(values);
}
}
dotnet run -c Release --project benchmarks.csproj
perfall ingest benchmarkdotnet --file BenchmarkDotNet.Artifacts/results/MyBenchmarks-report-brief.json --api-key "$PERFALL_API_KEY"
CI regression gate
Perfall can upload the current run, compare it with the median of five recent compatible baseline-branch samples for each metric, print a concise summary, and exit non-zero when a regression exceeds your configured percentage. Latency and cost metrics are lower-is-better, while throughput metrics are higher-is-better.
perfall ingest pytest-benchmark \
--file benchmark.json \
--api-key "$PERFALL_API_KEY" \
--baseline-branch main \
--baseline-window 5 \
--fail-on-regression-percent 5
name: Perfall benchmark gate
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
checks: write
issues: write
jobs:
benchmarks:
runs-on: ubuntu-latest
env:
PERFALL_API_KEY: ${{ secrets.PERFALL_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- id: perfall_ingest
run: |
python -m pip install --upgrade pip
python -m pip install perfall-cli pytest pytest-benchmark
pytest --benchmark-only --benchmark-json benchmark.json
- run: |
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
perfall ingest pytest-benchmark \
--file benchmark.json \
--api-key "$PERFALL_API_KEY" \
--baseline-branch "${{ github.event.pull_request.base.ref }}" \
--baseline-window 5 \
--fail-on-regression-percent 5
else
perfall ingest pytest-benchmark \
--file benchmark.json \
--api-key "$PERFALL_API_KEY"
fi
- name: Publish Perfall benchmark check
if: ${{ always() }}
uses: actions/github-script@v7
env:
PERFALL_DASHBOARD_URL: ${{ steps.perfall_ingest.outputs.perfall_dashboard_url }}
PERFALL_PR_COMMENT: ${{ steps.perfall_ingest.outputs.perfall_pr_comment_markdown }}
with:
script: |
const summary = process.env.PERFALL_PR_COMMENT || 'Perfall did not publish a benchmark verdict.';
const failed = summary.includes('Regression gate: **FAILED**');
const warming = summary.includes('insufficient history');
await github.rest.checks.create({
owner: context.repo.owner, repo: context.repo.repo,
name: 'Perfall benchmark', head_sha: context.sha, status: 'completed',
conclusion: failed ? 'failure' : warming ? 'neutral' : 'success',
details_url: process.env.PERFALL_DASHBOARD_URL || undefined,
output: { title: failed ? 'Regression threshold exceeded' : warming ? 'Warming up: no comparable baseline' : 'Regression gate passed', summary }
});
gitlab-ci.perfall-benchmark-example.yml to .gitlab-ci.yml (or include its job) and store PERFALL_API_KEY as a masked variable in Settings > CI/CD > Variables. GitLab merge-request pipelines use CI_MERGE_REQUEST_TARGET_BRANCH_NAME; default-branch pipelines upload history without a gate.PERFALL_API_KEY.perfall-cli from PyPI directly and does not depend on any repo-local GitHub Action files.(max - min) / |median| × 100. Spread at or below 5% is stable; higher spread is variable. A variable baseline never suppresses a threshold breach. A metric with fewer than five samples has insufficient history and warms up without failing; use --baseline-window 1 only when you intentionally want the latest-sample comparison.baseline-branch to the PR base branch, such as GitHub Actions github.event.pull_request.base.ref.contents: read, issues: write, and checks: write; the check runs with always() so a failed threshold still leaves review evidence. A breach fails the check, while insufficient baseline history is shown as a neutral warming-up check.ops/s, B/s, and items/s are higher-is-better; a drop beyond the threshold fails the gate.The CLI is the recommended path, but direct integrations can POST normalized JSON to the ingest API when they need more control.
POST https://perfall.it/ingest/v1/samples
{
"api_key": "project-api-key",
"run": {
"tool": "pytest-benchmark",
"branch": "main"
},
"samples": [
{
"identifier": "bench.sort.mean",
"value": 10.4,
"timestamp": "2026-03-19T05:30:00+00:00"
}
]
}
api_key and at least one samples[] entry.identifier and a numeric value.status, project_id, accepted_samples, and identifiers.account_daily_quota_exceeded.POST https://perfall.it/ingest/submit.Create an account, create a project, and copy its API key from the Projects page before running the upload command for your framework.