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
31 changes: 31 additions & 0 deletions benchmarking/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,37 @@ nothing. Durations are in milliseconds.
are canceled: interrupted tasks log no task record, and startup failures logged after it
belong to the teardown, not the run.

### ate-env Benchmark

The `ate-env` benchmark (`--user-class ate_env`) exercises the
[`ate-env-guest`](https://github.com/agent-substrate/env/tree/main/cmd/ate-env-guest)
sandbox image from `agent-substrate/env`. `workloads/deploy.sh` deploys and
deletes the `ate-env` `ActorTemplate` by default using the pinned release guest
image (see
[`workloads/manifests/ate-env-template.yaml.tmpl`](workloads/manifests/ate-env-template.yaml.tmpl)
to override the guest image with a custom build). It follows `glutton`'s
wake/hold/hibernate lifecycle, but replaces synthetic HTTP `/ping` calls with a
real `"hello world"` process execution inside the guest (`StartProcess(["echo",
"hello world"])` + `StreamProcessOutput(follow: true)`) over gRPC directly
through `atenet-router` (`1 dns:///` gRPC channel per VU).

#### ate-env Configuration Knobs

Reuses `glutton`'s runtime and startup knobs:
`--min-wait-time`, `--max-wait-time`, `--min-live-time`, `--max-live-time`,
`--max-pings-per-wake`, `--lifecycle-mode` (`suspend` | `pause` | `none`),
`--trace-probability`, and `--actors-per-user`.

#### ate-env Reported Metrics

* `CreateAtespace`, `CreateActor`, `ResumeActorFirstResume`, `ResumeActor`,
`SuspendActor`, `SuspendActorAsync`, `DeleteActor`: Control-plane lifecycle
RPC latencies.
* `AteEnvExec`: Combined end-to-end latency of `"hello world"` (`StartProcess` +
`StreamProcessOutput` to exit with `exit_code == 0` and
`stdout == "hello world\n"`).
* `CrashCount`: Actors that entered `ACTOR_STATE_CRASHED` during resume.

### Agent-Session Benchmark

The agent-session benchmark (`--user-class agentsession`) emulates a fleet of
Expand Down
37 changes: 37 additions & 0 deletions benchmarking/locust/tests/ate_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2026 Google LLC
#
# 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.

"""Stub AteEnvUser declaration.

The real load implementation lives in the boomer-Go worker at
cmd/benchmarking/boomer-worker/; this Python class is declared only so the master's
--class-picker enumerates the name. The Python worker container sets
LOCUST_NO_ATE_ENV_USER=1 to skip loading this file, leaving boomer as the
sole owner of AteEnvUser load.
"""

import os

if os.environ.get("LOCUST_NO_ATE_ENV_USER") != "1":
from locust import User, task
from common.boomer_config import init_boomer_config

init_boomer_config()

class AteEnvUser(User):
host = "api.ate-system.svc.cluster.local:443"

@task
def noop(self) -> None:
pass
11 changes: 10 additions & 1 deletion benchmarking/workloads/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ SWEPERF_TEMPLATE_DIR="${MANIFEST_DIR}/sweperf/generated"
# matching locust tests) are not deployed by default. The glutton-storage
# template is added when a StorageClass is set, because it needs a CSI driver
# that not every cluster has.
read -r -a TEMPLATES <<<"${WORKLOAD_TEMPLATES:-sleep glutton glutton-durdir-data glutton-durdir-full}"
read -r -a TEMPLATES <<<"${WORKLOAD_TEMPLATES:-sleep glutton glutton-durdir-data glutton-durdir-full ate-env}"

# template_manifest prints the manifest path for ActorTemplate NAME.
template_manifest() {
Expand Down Expand Up @@ -73,6 +73,9 @@ WORKER_MEMORY=""
# The StorageClass for the glutton-storage external volume. Empty means the
# template is not deployed.
STORAGE_CLASS_NAME="${STORAGE_CLASS_NAME:-}"
# The image for the ate-env benchmark ActorTemplate. Defaults to the pinned
# release guest image; set ATE_ENV_IMAGE to override with a custom build.
ATE_ENV_IMAGE="${ATE_ENV_IMAGE:-us-docker.pkg.dev/gke-substrate-release/benchmark/ate-env-guest@sha256:1d38934ef1dac71fcb1f1286de734e06ab2e19747279bccbd37bfe69e0d5bc72}"
# The address to which an instrumented actor container sends its telemetry.
# --otlp-endpoint sets it. Without the flag, resolve_otlp_endpoint reads the
# address that the control plane uses.
Expand Down Expand Up @@ -164,6 +167,7 @@ substitute() {
-e "s|\${ACTOR_MEMORY}|${ACTOR_MEMORY}|g" \
-e "s|\${ACTOR_CPU}|${ACTOR_CPU}|g" \
-e "s|\${WORKER_RESOURCES}|${worker_resources}|g" \
-e "s|\${ATE_ENV_IMAGE}|${ATE_ENV_IMAGE:-}|g" \
-e "s|\${STORAGE_CLASS_NAME}|${STORAGE_CLASS_NAME}|g" \
"${manifest}"
}
Expand Down Expand Up @@ -441,6 +445,11 @@ if [[ "${action}" == "deploy" && -z "${STORAGE_CLASS_NAME}" && " ${TEMPLATES[*]}
exit 1
fi

if [[ "${action}" == "deploy" && -z "${ATE_ENV_IMAGE:-}" && " ${TEMPLATES[*]} " == *" ate-env "* ]]; then
echo "Error: the ate-env template needs ATE_ENV_IMAGE" >&2
exit 1
fi

for entry in "${FAKE_ATELET_ENV[@]}"; do
if ! [[ "${entry}" =~ ^FAKE_ATELET_[A-Z_]+=.+$ ]]; then
echo "Error: --fake-atelet-env must be FAKE_ATELET_<FLAG>=VALUE, such as FAKE_ATELET_DELAY=500ms, got '${entry}'" >&2
Expand Down
51 changes: 51 additions & 0 deletions benchmarking/workloads/manifests/ate-env-template.yaml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2026 Google LLC
#
# 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.

# ActorTemplate (protojson-shaped ateapipb.ActorTemplate) for the ate-env
# benchmark workload, created through the ate API by workloads/deploy.sh.
metadata:
atespace: benchmark-workloads
name: ate-env
workerSelector:
matchLabels:
workload: benchmark-worker
containers:
- name: guest
# The guest image is built from the agent-substrate/env repo
# https://github.com/agent-substrate/env/tree/1c08a7a578afd896373511a83beb242aec1425c0/cmd/ate-env-guest
# (2026-10-14, which pins base bash:5.3@sha256:61962062d969cb46dfc2bad061d36342406fa485f64f246aa7e95693ca07df1f
# in .ko.yaml):
#
# KO_DOCKER_REPO="gcr.io/<PROJECT_ID>/ate-env-guest" ko build --bare --platform=linux/amd64 ./cmd/ate-env-guest
#
# Substituted by benchmarking/workloads/deploy.sh (which defaults ATE_ENV_IMAGE
# to us-docker.pkg.dev/gke-substrate-release/benchmark/ate-env-guest@sha256:1d38934ef1dac71fcb1f1286de734e06ab2e19747279bccbd37bfe69e0d5bc72;
# set ATE_ENV_IMAGE to override with a custom digest).
image: "${ATE_ENV_IMAGE}"
command:
- "/ko-app/ate-env-guest"
- "--listen=:80"
wakeupProbe:
httpGet:
path: /readyz
port: 80
resources:
limits:
- name: cpu
quantity: "${ACTOR_CPU}"
- name: memory
quantity: "${ACTOR_MEMORY}"
snapshotConfig:
preferredFidelity: SNAPSHOT_FIDELITY_MEMORY
storageLocation: gs://${BUCKET_NAME}/benchmark-workloads/ate-env/
6 changes: 6 additions & 0 deletions cmd/ate-setup/internal/steps/substrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func TestActorTemplateFromManifestDemos(t *testing.T) {
benchmarkValues := map[string]string{
"${ACTOR_CPU}": "1",
"${ACTOR_MEMORY}": "256Mi",
"${ATE_ENV_IMAGE}": "gcr.io/example/ate-env-guest@sha256:1d38934ef1dac71fcb1f1286de734e06ab2e19747279bccbd37bfe69e0d5bc72",
"${OTLP_ENDPOINT}": "http://otel-collector.otel-system:4317",
}

Expand All @@ -65,6 +66,11 @@ func TestActorTemplateFromManifestDemos(t *testing.T) {
relPath: "demos/sandbox/manual-test-multi-template.yaml",
want: resources.ActorTemplateRef{Atespace: "ate-manual-test-multi", Name: "sandbox-template"},
},
{
relPath: "benchmarking/workloads/manifests/ate-env-template.yaml.tmpl",
values: benchmarkValues,
want: resources.ActorTemplateRef{Atespace: "benchmark-workloads", Name: "ate-env"},
},
{
relPath: "benchmarking/workloads/manifests/sleep-template.yaml.tmpl",
values: benchmarkValues,
Expand Down
1 change: 1 addition & 0 deletions cmd/benchmarking/boomer-worker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (

// Register user classes via init():
"go.ate.dev/substrate/internal/benchmarking/boomer/agentsession"
_ "go.ate.dev/substrate/internal/benchmarking/boomer/ateenv"
_ "go.ate.dev/substrate/internal/benchmarking/boomer/glutton"
_ "go.ate.dev/substrate/internal/benchmarking/boomer/resumepaused"
_ "go.ate.dev/substrate/internal/benchmarking/boomer/sweperf"
Expand Down
74 changes: 74 additions & 0 deletions internal/benchmarking/boomer/ateenv/knobs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2026 Google LLC
//
// 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.

package ateenv

import (
"reflect"
"testing"
"time"

"go.ate.dev/substrate/internal/benchmarking/boomer/dynconfig"
)

func TestAteEnvCodec(t *testing.T) {
h := dynconfig.NewHolder(ateEnvCodec)
initial := ateEnvKnobs{
WaitTime: dynconfig.WaitTime{MaxWait: dynconfig.Seconds(500 * time.Millisecond)},
MaxPingsPerWake: 1,
}
if got := h.Load(); !reflect.DeepEqual(got, initial) {
t.Fatalf("fresh holder = %+v, want %+v", got, initial)
}

valid := `{
"min_wait_time": 0.2, "max_wait_time": 1.0,
"min_live_time": 9, "max_live_time": 14,
"resume_mode": "explicit", "lifecycle_mode": "pause",
"max_pings_per_wake": 2
}`
if _, _, err := h.Apply([]byte(valid)); err != nil {
t.Fatalf("Apply(valid): %v", err)
}
want := ateEnvKnobs{
WaitTime: dynconfig.WaitTime{MinWait: dynconfig.Seconds(200 * time.Millisecond), MaxWait: dynconfig.Seconds(time.Second)},
Lifecycle: dynconfig.Lifecycle{ResumeMode: dynconfig.ResumeModeExplicit, LifecycleMode: dynconfig.LifecycleModePause},
MinLive: dynconfig.Seconds(9 * time.Second),
MaxLive: dynconfig.Seconds(14 * time.Second),
MaxPingsPerWake: 2,
}
if got := h.Load(); !reflect.DeepEqual(got, want) {
t.Errorf("decoded =\n %+v, want\n %+v", got, want)
}

for name, payload := range map[string]string{
"negative min wait": `{"min_wait_time": -1}`,
"negative max wait": `{"max_wait_time": -1}`,
"max wait below min wait": `{"min_wait_time": 2, "max_wait_time": 1}`,
"negative min live": `{"min_live_time": -1}`,
"negative max live": `{"max_live_time": -1}`,
"max live below min live": `{"min_live_time": 14, "max_live_time": 9}`,
"invalid resume mode": `{"resume_mode": "invalid_mode"}`,
"invalid lifecycle mode": `{"lifecycle_mode": "invalid_lifecycle"}`,
} {
t.Run(name, func(t *testing.T) {
if _, _, err := h.Apply([]byte(payload)); err == nil {
t.Errorf("Apply(%s) took the payload, want a refusal", payload)
}
if got := h.Load(); !reflect.DeepEqual(got, want) {
t.Errorf("a refused payload changed the config to %+v", got)
}
})
}
}
Loading
Loading