Skip to content
Open
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
2 changes: 1 addition & 1 deletion test/e2e/01-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ echo "Fetching access tokens..."
mkdir -p "${SA_TOKEN_PATH}"
service_accounts=(all-namespaces-read-access single-namespace-read-access all-namespaces-admin-access all-namespaces-impersonate-access)
for service_account in "${service_accounts[@]}"; do
kubectl create token "$service_account" > "${SA_TOKEN_PATH}"/"$service_account"
kubectl create token "$service_account" > "${SA_TOKEN_PATH}"/"$service_account" --duration=24h
echo "Created ${SA_TOKEN_PATH}/$service_account"
done

Expand Down
154 changes: 154 additions & 0 deletions test/performance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Tekton Results — Performance Benchmark Framework

A repeatable harness for measuring **store** (write/ingest) and **query**
(read/list) performance of Tekton Results — separately and in parallel — always
starting from an identical, versioned seed dataset. It drives the **real deployed
API** only (gRPC for writes, REST/gRPC for reads); it never touches the database
directly.

Use it to prove improvement and catch regressions across schema changes (metadata
columns, label tables, indexes) and new code paths.

```
generator/ deterministic PipelineRun/TaskRun generator + golden DatasetDefinition
metrics/ latency percentiles, error-by-code counters, throughput (importable)
report/ JSON report schema + metadata capture (importable)
harness/ cobra "bench" CLI + store/query/mixed/load drivers (package main)
datasets/ dataset specs (see seed-small.md)
environments/ kind cluster + install automation (local & external DB)
baselines/ committed baseline reports (follow-up)
```

`generator`, `metrics`, and `report` are importable packages — the compare tool
(Story 02) and compliance suite (Story 03) reuse the generator, datasets, and
report schema.

## Quick start (tier-1 kind, local DB)

```bash
# 1. Cluster + install with fixed Postgres tuning
./test/performance/environments/kind/00-kind-up.sh
./test/performance/environments/kind/01-install-localdb.sh

# The install prints the cert/token paths; export them for convenience:
export SSL_CERT_PATH=/tmp/tekton-results/ssl
export SA_TOKEN_PATH=/tmp/tekton-results/tokens
export API_SERVER_ADDR=https://localhost:8080

CERT="${SSL_CERT_PATH}/tekton-results-cert.pem"
TOKEN="${SA_TOKEN_PATH}/all-namespaces-admin-access"

# 2. Load + verify the seed dataset through the API
go run ./test/performance/harness load --verify --cert "$CERT" --token "$TOKEN"

# 3. Benchmark
go run ./test/performance/harness store --cert "$CERT" --token "$TOKEN" --output store.json
go run ./test/performance/harness query --cert "$CERT" --token "$TOKEN" --output query.json --transport both
go run ./test/performance/harness mixed --cert "$CERT" --token "$TOKEN" --output mixed.json --duration 60
```

## Subcommands

| Command | What it measures |
| --------- | ---------------------------------------------------------------------- |
| `load` | Loads the seed dataset via the API; `--verify` checks row counts against the golden definition. |
| `store` | Write/ingest — replays the watcher lifecycle (CreateResult → CreateRecord(pending) → 1–3 UpdateRecord → UpdateResult → 2–15 child records) over the **live** UID range. |
| `query` | Read/list — a fixed CEL query mix (by status, recent window, by type, by label) against the **seed** range, paginated. `--transport grpc\|rest\|both`. |
| `mixed` | Writers (live range) and readers (seed range) in parallel; sized by `--read-ratio`/`--write-ratio`. |
| `dataset` | Emits the golden `DatasetDefinition` JSON — no cluster required. |

## Common flags

Defaults come from the same environment variables the e2e suite uses.

| Flag | Default | Env |
| ----------------- | ------------------------------------ | ----------------- |
| `--server-addr` | `https://localhost:8080` | `API_SERVER_ADDR` |
| `--server-name` | `tekton-results-api-service…svc…` | `API_SERVER_NAME` |
| `--cert` | — | `SSL_CERT_PATH` |
| `--token` | — | `SA_TOKEN_PATH` |
| `--concurrency` | `8` | |
| `--count` | `1000` (count-bounded runs) | |
| `--duration` | `0` → 30s default for time-bounded | |
| `--seed` | `42` | |
| `--namespaces` | `50` | |
| `--child-min/max` | `2` / `15` | |
| `--db-backend` | `local` | `BENCH_DB_BACKEND`|
| `--output` | stdout | |

## Dataset determinism

The generator is seeded (`math/rand/v2` PCG per index) and uses a fixed absolute
time window — never `time.Now()` — so a given `(seed, count, namespaces)` always
produces byte-identical objects and a stable `content_hash`. Seed data and live
(store/mixed) data draw UIDs from **disjoint** ranges so writes never collide with
loaded reads. See [`datasets/seed-small.md`](datasets/seed-small.md) for the
tier-1 spec and golden fingerprint.

Templates are anonymized real PipelineRun/TaskRun manifests dropped into
[`generator/templates/`](generator/templates/) per the contract documented there;
a sample ships so the framework builds and tests run before real manifests arrive.

## Report schema

Every run emits a JSON `Report` (`report.SchemaVersion`) — the comparison input
for Story 02:

```jsonc
{
"schema_version": "1",
"meta": {
"git_commit": "…", "git_dirty": false,
"dataset_version": "seed-small-v1", "dataset_hash": "…",
"tier": "tier1", "mode": "store",
"started_at": "…", "duration_ms": 1234,
"hostname": "…", "api_server_addr": "…",
"db_backend": "local", "server_image": "…"
},
"config": { "count": 1000, "concurrency": 8, "transport": "grpc", "seed": 42, … },
"metrics": {
"throughput_per_sec": 812.3,
"total_ops": 5000, "total_errors": 0,
"by_op": {
"create_record": { "count": 1000, "errors": 0, "error_codes": {},
"p50_ms": 3.1, "p90_ms": 7.4, "p99_ms": 19.0,
"min_ms": 1.2, "max_ms": 41.0, "mean_ms": 4.0 }
}
}
}
```

Percentiles are exact (nearest-rank over sorted samples); errors are classified by
gRPC status code. Map keys serialize in stable order.

## External database

To benchmark against a managed/external Postgres instead of the in-cluster one:

```bash
export BENCH_DB_URL="postgres://user:pass@host:5432/results?sslmode=require"
./test/performance/environments/kind/02-install-externaldb.sh
```

This rewires only configuration (`DB_*` config + credentials secret) — no code or
image change — and sets `db_backend=external` in reports. The harness is unchanged
because it only talks to the API.

## Comparing runs

Reports are self-describing (git commit, dataset hash, tier, DB backend). To
compare two runs, diff `metrics.by_op[*].p99_ms` and `throughput_per_sec` for the
same `mode` and `dataset_hash`. Guideline variance for a pass/fail gate is ±10% on
p99. Committed reference runs live in [`baselines/`](baselines/) (populated as a
follow-up).

## Tests

```bash
go test ./test/performance/...
```

Covers generator determinism and distributions, UID non-overlap, metrics
percentile math, report round-trip, and harness workload wiring. The CI smoke run
stands up local-db kind, loads `seed-small`, then runs `store|query|mixed` capped
at 1k records and asserts each emits valid JSON.
3 changes: 3 additions & 0 deletions test/performance/baselines/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Committed baseline reports land here (one JSON per mode/tier/dataset version).
# Populated as a follow-up: run store|query|mixed against main and commit the
# reports as tier1-<dataset-version>-<mode>-baseline.json.
82 changes: 82 additions & 0 deletions test/performance/datasets/seed-small.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Dataset: `seed-small` (tier-1)

The tier-1 seed dataset used for CI smoke runs and the committed tier-1 baseline.
It is **deterministic**: the same generator seed always produces byte-identical
objects, so the content hash below is a versioned fingerprint. If any value in
this file changes, treat it as a dataset-version bump and refresh the committed
baseline.

## Parameters

| Parameter | Value | Flag |
| ---------------- | ------ | -------------- |
| PipelineRuns | 1000 | `--count 1000` |
| RNG seed | 42 | `--seed 42` |
| Namespaces | 50 | `--namespaces 50` |
| Child TaskRuns | 2–15 | `--child-min 2 --child-max 15` |
| Dataset version | `seed-small` | `--dataset-version seed-small` |

Reproduce the golden definition with no cluster:

```bash
go run ./test/performance/harness dataset \
--count 1000 --seed 42 --namespaces 50 > /tmp/seed-small.json
```

## Golden fingerprint

| Field | Expected value |
| -------------------- | ----------------------------------------------------------------- |
| `content_hash` | `4ab4cbcfbdbfe4e89e455eaf13971edef720d6fa1e24a8fbedf7e53d3dc60fa5` |
| PipelineRun records | 1000 |
| TaskRun records | 3028 |
| Total records | 4028 |

The content hash covers only the generated objects (UIDs, namespaces, labels,
outcomes, timestamps), never wall-clock metadata, so it is stable across runs and
machines. `bench load --verify` recomputes it and asserts the loaded record
counts match this definition.

## Distributions

Outcomes (target 85% / 10% / 5%):

| Outcome | Count | Share |
| ---------- | ----- | ------ |
| succeeded | 845 | 84.5% |
| failed | 101 | 10.1% |
| cancelled | 54 | 5.4% |

Namespaces: 1000 PipelineRuns spread across `ns-00`..`ns-49` (~20 each).

Labels — a small set of hot values plus a long tail:

`appstudio.openshift.io/component`

| Value | Count |
| -------- | ----- |
| backend | 413 |
| frontend | 401 |
| db | 33 |
| api | 24 |
| auth | 23 |
| cache | 23 |
| ingest | 22 |
| worker | 22 |
| reporting| 20 |
| gateway | 19 |

`pipelinesascode.tekton.dev/event-type`

| Value | Count |
| ------------ | ----- |
| push | 780 |
| retest | 85 |
| pull_request | 71 |
| incoming | 64 |

## UID ranges

Seed and live (store/mixed) streams draw UIDs from **disjoint** UUIDv5 ranges, so
a store or mixed run never collides with the loaded seed data. The ranges are
recorded in the definition's `uid_range` and in every report's metadata.
24 changes: 24 additions & 0 deletions test/performance/environments/kind/00-kind-up.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Copyright 2026 The Tekton Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Stand up the tier-1 kind cluster. Delegates to the e2e cluster setup so the
# benchmark runs against the exact node topology the conformance suite uses.

set -euo pipefail

ROOT="$(git rev-parse --show-toplevel)"

echo "Creating kind cluster for performance benchmarks..."
"${ROOT}/test/e2e/00-setup.sh"
64 changes: 64 additions & 0 deletions test/performance/environments/kind/01-install-localdb.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Copyright 2026 The Tekton Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Install Tekton Results with the bundled local Postgres, then apply the fixed
# performance tuning (pinned resources + postgresql.conf) for reproducible runs.
#
# This delegates the full app install (Pipelines, certs, tokens, ko deploy) to
# the e2e installer, then layers Postgres tuning on top imperatively so no ko
# rebuild is needed for the database.

set -euo pipefail

ROOT="$(git rev-parse --show-toplevel)"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NAMESPACE="tekton-pipelines"

# Expose the API on localhost so the harness can reach it without port-forward
# gymnastics; the e2e installer honours this when generating the TLS cert.
export SSL_INCLUDE_LOCALHOST=${SSL_INCLUDE_LOCALHOST:-"true"}

echo "Installing Tekton Results (standard e2e deployment)..."
"${ROOT}/test/e2e/01-install.sh"

echo "Applying Postgres performance tuning..."
kubectl create configmap postgres-tuning \
--namespace="${NAMESPACE}" \
--from-file=postgresql.conf="${HERE}/postgresql.conf" \
--dry-run=client -o yaml | kubectl apply -f -

kubectl patch statefulset tekton-results-postgres \
--namespace="${NAMESPACE}" \
--type=strategic \
--patch-file "${HERE}/postgres-patch.yaml"

echo "Waiting for Postgres to roll out with tuning applied..."
kubectl rollout status statefulset/tekton-results-postgres --namespace="${NAMESPACE}" --timeout=180s
kubectl wait deployment tekton-results-api --namespace="${NAMESPACE}" --for=condition=available --timeout=120s

cat <<EOF

Tier-1 environment ready.

API_SERVER_ADDR=https://localhost:8080
SSL_CERT_PATH=${SSL_CERT_PATH:-/tmp/tekton-results/ssl}
SA_TOKEN_PATH=${SA_TOKEN_PATH:-/tmp/tekton-results/tokens}

Run a benchmark, e.g.:

go run ./test/performance/harness load --verify \\
--cert "\${SSL_CERT_PATH}/tekton-results-cert.pem" \\
--token "\${SA_TOKEN_PATH}/all-namespaces-admin-access"
EOF
Loading
Loading