Help us shape the future of Go! Take the 2026 Go Developer Survey

bench

package
v0.1.0-rc.10 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

Documentation

Overview

Package bench drives a reproducible agent workload against the mesh.

The chaos agent that ships with the repo is the realism story: a real model deciding what to call next. That makes it useless as an instrument, because two runs never issue the same requests and a difference between them cannot be attributed to the thing under test. This package is the opposite trade: it does exactly what it is told, so a difference between two runs is a difference in the mesh.

It reaches the mesh the way an agent does, through the sandbox boundary's Unix socket, opening each flow as a named HTTP CONNECT tunnel, so what it measures is what an agent would experience rather than what an operator with host access would.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Distribution

type Distribution struct {
	Count int     `json:"count"`
	Min   float64 `json:"min"`
	Mean  float64 `json:"mean"`
	P50   float64 `json:"p50"`
	P90   float64 `json:"p90"`
	P95   float64 `json:"p95"`
	P99   float64 `json:"p99"`
	Max   float64 `json:"max"`
	// StdDev is reported so a reader can see whether a mean is describing
	// anything, rather than averaging a bimodal distribution into fiction.
	StdDev float64 `json:"stddev"`
}

Distribution summarises a set of latencies in milliseconds. Percentiles are computed from the full sorted sample rather than from bucketed counts, so they are exact for the run rather than an interpolation.

type HoldReport

type HoldReport struct {
	Seconds float64 `json:"seconds"`
	Samples int     `json:"samples"`

	// Resident is how many members were ready at the first sample and
	// MinReady the fewest at any sample.
	Resident int `json:"resident"`
	MinReady int `json:"min_ready"`

	// Flaps counts ready-to-not-ready transitions across the fleet, Flapped
	// the members that had at least one, NotReadyAtEnd those still down when
	// the hold ended and Exited the processes that ended by themselves.
	Flaps         int `json:"flaps"`
	Flapped       int `json:"members_flapped"`
	NotReadyAtEnd int `json:"members_not_ready_at_end"`
	Exited        int `json:"members_exited"`

	// OutageMs is the length of every not-ready window that ended during
	// the hold. A window still open at the end is in NotReadyAtEnd instead.
	OutageMs Distribution `json:"outage_ms"`

	// Series is the ready count at each sample, for lining up against the
	// events an operator caused while the fleet was resident.
	Series []ReadySample `json:"ready_series"`
}

HoldReport describes a resident fleet over time.

type JoinOptions

type JoinOptions struct {
	// NodeBin is the agentmesh-node binary; one process of it is started per member.
	NodeBin string

	// Count is how many members to start.
	Count int

	// Dir is the directory under which each member gets its own state,
	// socket and log. It must be empty: a member directory that already holds
	// an identity would resume rather than join.
	Dir string

	// ControlPlane is the URL every member enrolls with.
	ControlPlane string

	// BootstrapTokenPath is the file holding the bootstrap token the members
	// enroll with. It needs at least Count usages left. Empty means the
	// credential comes from NodeArgs instead, such as a --jwt-path.
	BootstrapTokenPath string

	// Service is the MCP service each member looks up and calls once it is
	// ready. Empty ends the journey at readiness.
	Service string

	// Ramp is how many members to start per second. Zero starts them all at
	// once.
	Ramp float64

	// ReadyTimeout bounds enrollment plus router authentication for one
	// member.
	ReadyTimeout time.Duration

	// CallTimeout bounds discovery plus the first successful call for one
	// member, counted from when it became ready.
	CallTimeout time.Duration

	// Hold keeps the fleet resident after the last journey has ended and
	// samples every member's readiness until it elapses or the context is
	// cancelled. Zero tears the fleet down as soon as the journeys end.
	Hold time.Duration

	// SampleInterval is how often each resident member is read during Hold.
	SampleInterval time.Duration

	// MetricsBasePort is the first of Count consecutive loopback ports;
	// member i serves /readyz on MetricsBasePort+i.
	MetricsBasePort int

	// NodeArgs are appended to every member's command line as given.
	NodeArgs []string

	// OnResident, if set, is called with the report once the journeys have
	// ended and the hold has begun. The report is complete except for Hold,
	// so a caller can publish the join numbers before a long hold ends.
	OnResident func(*JoinReport)

	// Log receives one line per event: a member's journey ending, a resident
	// member losing or regaining its router. Nil discards them.
	Log io.Writer
}

JoinOptions describe one fleet to take through the join journey.

type JoinReport

type JoinReport struct {
	Count   int     `json:"count"`
	Ramp    float64 `json:"ramp_per_second"`
	Service string  `json:"service,omitempty"`

	// Ready counts members that reached /readyz 200. Completed counts those
	// that finished the whole journey, which is readiness alone when there
	// is no Service. Failed is the rest.
	Ready     int `json:"ready"`
	Completed int `json:"completed"`
	Failed    int `json:"failed"`

	// Elapsed runs from the first member starting to the last journey ending.
	Elapsed float64 `json:"elapsed_seconds"`

	// ReadyMs is process start to /readyz 200, FirstCallMs is readiness to
	// the first successful call, and JourneyMs is their sum, each over the
	// members that got that far.
	ReadyMs     Distribution `json:"ready_ms"`
	FirstCallMs Distribution `json:"first_call_ms"`
	JourneyMs   Distribution `json:"journey_ms"`

	// CallAttempts is the number of provider calls made across the fleet.
	// More than one per completed member means provider records that named
	// a peer which did not answer; CallErrors says how each of those
	// attempts failed, including on members that then succeeded elsewhere.
	CallAttempts int            `json:"call_attempts"`
	CallErrors   map[string]int `json:"call_errors,omitempty"`

	// Errors counts failures by stage, so a fleet that mostly failed cannot
	// be mistaken for a fast one.
	Errors map[string]int `json:"errors,omitempty"`

	Hold *HoldReport `json:"hold,omitempty"`

	Members []MemberResult `json:"members"`
}

JoinReport is the outcome of one fleet's journey.

func RunJoin

func RunJoin(ctx context.Context, opts JoinOptions) (*JoinReport, error)

RunJoin starts the fleet, takes every member through the journey, holds the fleet if asked, tears it down and reports. Cancelling the context ends whatever phase is running; the report still describes what happened.

type MemberResult

type MemberResult struct {
	Index       int     `json:"index"`
	Dir         string  `json:"dir"`
	ReadyMs     float64 `json:"ready_ms,omitempty"`
	FirstCallMs float64 `json:"first_call_ms,omitempty"`
	Provider    string  `json:"provider,omitempty"`
	Attempts    int     `json:"call_attempts,omitempty"`
	Error       string  `json:"error,omitempty"`
}

MemberResult is one member's journey.

type Options

type Options struct {
	// Socket is the sandbox boundary's Unix socket, which speaks HTTP CONNECT.
	// Empty measures the same workload without the boundary, which is the
	// baseline the mesh numbers are only meaningful against.
	Socket string

	// UnixTarget dials the target over this Unix socket instead of resolving
	// its host. It is how the baseline reaches the node's own API without a
	// relay in between: putting one there, socat or otherwise, would add a
	// userspace hop to the baseline and quietly flatter everything measured
	// against it.
	UnixTarget string

	// Target is the URL to request, e.g. http://mesh.alt/v1/models.
	Target string

	// Method and Body are the request to issue. An empty Method means GET.
	Method string
	Body   []byte

	// Header carries anything the target needs, such as a content type.
	Header http.Header

	// Requests is how many to issue in total, after warmup.
	Requests int

	// Concurrency is how many are in flight at once.
	Concurrency int

	// Warmup requests are issued and discarded before measurement starts, so
	// first-call costs (DHT lookup, provider discovery, connection setup) do
	// not contaminate the steady-state distribution. They are reported
	// separately rather than thrown away.
	Warmup int

	// NewFlowPerRequest opens a fresh boundary flow for every request instead
	// of reusing connections. This separates the cost of admitting a flow from
	// the cost of carrying one, which are different questions.
	NewFlowPerRequest bool

	// Timeout bounds a single request.
	Timeout time.Duration
}

Options describe one measurement run. The zero value is not useful: at minimum a Target and a positive Requests count are required.

type ReadySample

type ReadySample struct {
	At    time.Time `json:"at"`
	Ready int       `json:"ready"`
}

ReadySample is the fleet's ready count at one instant.

type Report

type Report struct {
	Target            string        `json:"target"`
	ThroughBoundary   bool          `json:"through_boundary"`
	NewFlowPerRequest bool          `json:"new_flow_per_request"`
	Concurrency       int           `json:"concurrency"`
	Requests          int           `json:"requests"`
	Warmup            int           `json:"warmup"`
	Succeeded         int           `json:"succeeded"`
	Failed            int           `json:"failed"`
	Elapsed           float64       `json:"elapsed_seconds"`
	Throughput        float64       `json:"requests_per_second"`
	TTFB              Distribution  `json:"ttfb_ms"`
	Total             Distribution  `json:"total_ms"`
	WarmupTTFB        *Distribution `json:"warmup_ttfb_ms,omitempty"`

	// Errors counts failures by their message, so a run that mostly failed
	// cannot be mistaken for a fast one.
	Errors map[string]int `json:"errors,omitempty"`
}

Report is the outcome of a run, in a shape that serialises to JSON for later analysis without needing the raw samples.

func Run

func Run(ctx context.Context, opts Options) (*Report, error)

Run issues the workload and returns its report.

type STSOptions

type STSOptions struct {
	Requests          int
	Concurrency       int
	Warmup            int
	Workloads         int
	RequestsPerMinute int
}

STSOptions configures a Control Plane and Node STS benchmark run.

type STSPhaseReport

type STSPhaseReport struct {
	Requests          int          `json:"requests"`
	Succeeded         int          `json:"succeeded"`
	Failed            int          `json:"failed"`
	Elapsed           float64      `json:"elapsed_seconds"`
	Throughput        float64      `json:"requests_per_second"`
	Latency           Distribution `json:"latency_ms"`
	ControlPlaneCalls int          `json:"control_plane_calls"`
	CacheHits         int          `json:"cache_hits"`
	CacheHitRate      float64      `json:"cache_hit_rate"`
}

STSPhaseReport records throughput, latency distribution, and cache behavior for one STS benchmark phase.

type STSReport

type STSReport struct {
	Concurrency           int            `json:"concurrency"`
	Requests              int            `json:"requests"`
	Workloads             int            `json:"workloads"`
	RequestsPerMinute     int            `json:"requests_per_minute"`
	TokenExchangeUncached STSPhaseReport `json:"token_exchange_uncached"`
	STSTokenUncached      STSPhaseReport `json:"sts_token_uncached"`
	TokenExchangeSVID5m   STSPhaseReport `json:"token_exchange_svid_5m"`
	TokenExchangeK8s1h    STSPhaseReport `json:"token_exchange_k8s_1h"`
	STSTokenCached        STSPhaseReport `json:"sts_token_cached"`
}

STSReport summarizes all STS benchmark phases (uncached Control Plane endpoints and cached AgentMeshNode paths under 5-minute JWT-SVID and 1-hour projected token rotation schedules).

func RunSTS

func RunSTS(ctx context.Context, opts STSOptions) (*STSReport, error)

RunSTS executes the STS benchmark suite against an in-process Control Plane and enrolled AgentMeshNode, measuring uncached mint throughput/latency as well as node-cached throughput and hit rates for 5-minute JWT-SVIDs and 1-hour Kubernetes projected tokens.

type Sample

type Sample struct {
	// TTFB is the time until the response headers were available, which is
	// what an agent waiting on a first token actually experiences.
	TTFB time.Duration

	// Total additionally includes reading the body to completion.
	Total time.Duration

	// Status is the HTTP status, or 0 if the request never got one.
	Status int

	// Err is set when the request did not complete.
	Err error
}

Sample is one completed request.

type Series

type Series struct {
	Name   string
	Labels map[string]string
	Value  float64
}

Series is one exported sample: a metric name, its labels, and its value.

func (Series) Key

func (s Series) Key() string

Key renders a series as a stable, sorted identifier.

type Snapshot

type Snapshot struct {
	Source string    `json:"source"`
	Taken  time.Time `json:"taken"`
	Series []Series  `json:"-"`
}

Snapshot is everything one process exported at one instant.

func Scrape

func Scrape(ctx context.Context, url string) (*Snapshot, error)

Scrape reads one process's metrics endpoint.

func (*Snapshot) Flatten

func (s *Snapshot) Flatten() map[string]float64

Flatten renders the snapshot as canonical "name{k=v,...}" keys, which is what gets recorded in a result file. Keeping every series rather than a chosen few means a question nobody thought to ask before the run can still be answered from the record afterwards.

func (*Snapshot) Sum

func (s *Snapshot) Sum(name string) float64

Sum adds every sample of name, ignoring labels. It is how a counter split across label sets, such as flows by route, becomes one total.

func (*Snapshot) Value

func (s *Snapshot) Value(name string, match map[string]string) (float64, bool)

Value returns the first sample of name whose labels include match, and whether one existed. A missing series and a zero one mean different things: a counter that was never incremented is absent from the exposition entirely.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL