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 ¶
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.
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 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 (*Snapshot) Flatten ¶
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 ¶
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.