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
82 changes: 82 additions & 0 deletions .github/workflows/performance-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Performance Gate

on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled, edited]

permissions:
contents: read

concurrency:
group: performance-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true

env:
DOTNET_NOLOGO: 'true'
DOTNET_CLI_TELEMETRY_OPTOUT: 'true'

jobs:
gate:
name: Hermetic counter + quality gate
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json
cache: true
cache-dependency-path: '**/*.csproj'

- name: Restore
run: dotnet restore tools/AgentMemory.Cli/AgentMemory.Cli.csproj

- name: Build performance CLI
run: >
dotnet build tools/AgentMemory.Cli/AgentMemory.Cli.csproj
-c Release --no-restore

- name: Run zero and remote profiles through the gate
shell: bash
env:
PERF_COUNTER_CHANGE_ALLOWED: ${{ contains(github.event.pull_request.labels.*.name, 'perf-counter-change') }}
PERF_COUNTER_CHANGE_JUSTIFICATION: ${{ github.event.pull_request.body }}
run: |
set -euo pipefail

for latency in zero remote; do
output="artifacts/perf-ci/$latency"
dotnet run --project tools/AgentMemory.Cli/AgentMemory.Cli.csproj \
-c Release --no-build -- \
perf \
--label "ci-$latency" \
--iterations 3 \
--warmup 1 \
--latency "$latency" \
--output "$output"

report="$(find "$output" -type f -name summary.json -print -quit)"
if [[ -z "$report" ]]; then
echo "::error::perf did not produce summary.json for $latency"
exit 1
fi

dotnet run --project tools/AgentMemory.Cli/AgentMemory.Cli.csproj \
-c Release --no-build -- \
perf gate \
--baseline eng/perf/baselines/hermetic-S.json \
--report "$report" \
--allow-counter-change "$PERF_COUNTER_CHANGE_ALLOWED" \
--counter-change-justification "$PERF_COUNTER_CHANGE_JUSTIFICATION"
done

- name: Upload performance reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: performance-gate-reports
path: artifacts/perf-ci
if-no-files-found: ignore
31 changes: 31 additions & 0 deletions docs/performance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ What AgentMemory costs you per agent turn, how that cost is measured, and how to
| Doc | What it covers |
|---|---|
| **README.md** (this file) | The two phases, what is and isn't measured, how to run it yourself |
| [`hermetic-S.json`](../../eng/perf/baselines/hermetic-S.json) | Machine-readable counter + quality baseline enforced on pull requests |
| [baseline-1.3.0.md](baseline-1.3.0.md) | The measured cost model for release 1.3.0 |

---
Expand Down Expand Up @@ -111,6 +112,15 @@ dotnet run --project tools/AgentMemory.Cli -- perf ab \
--control default \
--candidate Recall.MaxEntities=2 \
--iterations 30

# Check one completed run against the committed counter + quality baseline
dotnet run --project tools/AgentMemory.Cli -- perf gate \
--baseline eng/perf/baselines/hermetic-S.json \
--report <path-to-summary.json>

# Deliberately refresh the reviewable baseline from a completed run
dotnet run --project tools/AgentMemory.Cli -- perf baseline --update \
--report <path-to-summary.json>
```

Each run writes a dated directory containing a manifest with the full environment fingerprint, an
Expand Down Expand Up @@ -147,6 +157,27 @@ Both measured scenarios also **self-assert**: the recall scenario fails the run
items than the configured limits, and the ingestion scenario fails if extraction never ran. Those
failures are otherwise silent and would produce a confident, wrong number.

### Pull-request regression gate

A dedicated GitHub Actions job runs the hermetic scale-S profile with both zero and remote-like
provider latency. Each report is compared with
[`eng/perf/baselines/hermetic-S.json`](../../eng/perf/baselines/hermetic-S.json). The gate rejects:

- any increase in Neo4j transactions or queries, embedding requests, or model calls;
- an estimated payload-byte increase above 5%, once that counter is available; and
- any retrieval or extraction quality regression beyond the committed tolerance (currently zero).

A deliberate structural-counter increase needs both the `perf-counter-change` pull-request label and
a non-empty pull-request-body line in this exact form:

```text
Perf counter change justification: <why the extra work is intentional>
```

That acknowledgement cannot override payload or quality failures. Hermetic elapsed milliseconds are
reported for diagnosis but are deliberately excluded from the CI decision because runner timings are
not portable.

---

## Tuning starting points
Expand Down
7 changes: 5 additions & 2 deletions docs/performance/baseline-1.3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,11 @@ The cost counters are only accepted when deterministic quality remains at this c

Every value above was identical across five fresh-container runs: maximum observed variance **0.000**.
The derived tolerance is therefore **zero**, recorded in
`eng/perf/baselines/quality.json`. The `perf` command gates by default and exits non-zero on a drop or
forbidden retrieval. This guard is about deterministic pipeline behavior; it does not claim to score
`eng/perf/baselines/quality.json`. The combined reviewable counter + quality snapshot used by pull
request CI is [`eng/perf/baselines/hermetic-S.json`](../../eng/perf/baselines/hermetic-S.json).
The `perf` command gates quality by default, while `perf gate` also rejects structural-counter
regressions against that snapshot. Neither command grades hermetic elapsed milliseconds. This guard is
about deterministic pipeline behavior; it does not claim to score
the quality of a live model's prose.

---
Expand Down
59 changes: 59 additions & 0 deletions eng/perf/baselines/hermetic-S.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"schemaVersion": 1,
"profile": "hermetic-S",
"qualityTolerance": 0,
"scenarios": {
"PERF-R-04": {
"counters": {
"access_tracking.items": 25,
"context.chars": 3906,
"context.messages": 14,
"embed.chars": 95,
"embed.items": 1,
"embed.requests": 1,
"items.entities": 10,
"items.facts": 10,
"items.preferences": 5,
"items.recent": 10,
"items.relevant": 5,
"items.retrieved": 43,
"items.traces": 3,
"neo4j.queries": 9,
"neo4j.tx.read": 6,
"neo4j.tx.write": 1,
"recall.chars": 3823
}
},
"PERF-W-02": {
"counters": {
"embed.chars": 201,
"embed.items": 4,
"embed.requests": 4,
"extract.candidate_entities": 2,
"llm.calls": 4,
"llm.tokens_in": 947,
"llm.tokens_out": 668,
"neo4j.queries": 43,
"neo4j.tx.read": 4,
"neo4j.tx.write": 18,
"persist.entities": 2,
"persist.facts": 2,
"persist.preferences": 1,
"persist.relationships": 0,
"store.messages": 1
}
}
},
"quality": {
"recallAtK": 1,
"mrr": 1,
"casesWithViolations": 0,
"entityPrecision": 1,
"entityRecall": 1,
"factPrecision": 1,
"factRecall": 1,
"preferencePrecision": 1,
"preferenceRecall": 1,
"extractionFalsePositiveRate": 0
}
}
Loading
Loading