wcprof

package
v1.0.0-beta.27 Latest Latest
Warning

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

Go to latest
Published: Nov 10, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

wcprof: engine wall-clock profiling

wcprof is an experimental, cheap wall-clock profiler for the Dagger engine, built to find latency bottlenecks that CPU/memory profiles can't see (e.g. a 300ms serial tax before every container start). It is intentionally separate from OTel: events are fixed-size structs appended to sharded in-memory buffers with interned strings, and all heavy analysis happens offline.

What it records

Four event types, written by hooks at the engine's blocking choke points:

  • ops: timed intervals with a kind (call, call_exec, lazy, exec, exec_phase, service_start, session_phase, ...), a class (e.g. Container.withExec, exec.setupNetwork), an instance ident (recipe digest, exec ID), a structural parent (from context), and an outcome (hit/executed/joined/...).
  • waits: exact blocked-on intervals — who waited, on which op (or named resource, e.g. a cache-volume lock), why, from when to when. These are recorded at the choke points themselves (dagql singleflight, lazy-eval waiters, service starts, exec completion), not inferred from span nesting.
  • links: non-blocking correlations, most importantly "exec op X hosts nested client Y" so module-function calls back into the API are stitched under the exec that made them (wcprof-report puts them under the exec's exec.processRun, or its exec.workload when the run is split). A fact link says something was true of its op (its ident, e.g. hit.remote), optionally about a result; a part link says a result has a part (its ident) of b bytes.
  • spans: the OTel span an op owns, when telemetry is on and the engine emits a span for the same work: a call with its own span, call_exec, dagql.publishResult, lazy, exec, its exec.containerStart, exec.runtimeStart and exec.processRun phases, and service_start. Dumped as {"e":"span","id":<op>,"tr":<trace ID>,"sp":<span ID>} in hex, so offline analysis can join an op to its span's attributes exactly.

Current hook points:

  • dagql/cache.go getOrInitCall: one call op per caller (cache hits and do-not-cache calls included — this deliberately records what OTel suppresses), one shared call_exec op per actual resolver execution, wait edges from every caller to it, and a dagql.publishResult op for publication cost. A publication that waits for the dependency attachment another publication, or a merge, holds open on its recipe's entry records an attach wait on that publication's op (in a trace, a wait link to its span; a merge has no span to link to).
  • dagql/cache.go evaluateOne: one lazy op per lazy-evaluation run, classed by the call that created the lazy value, with wait edges from the triggering and joining ops.
  • engine/engineutil/executor.go: one exec op per container run, one exec_phase op per setup phase (exec.setupNetwork, exec.setupRootfs, ..., exec.runContainer), plus a split of the container run into exec.containerStart (engine overhead up to launching runc), exec.runtimeStart (runc creating the container, up to the pid file it writes once the workload is released) and exec.processRun (user work). When the container runs under the injected /.init, /.init reports its own timing on a pipe and exec.processRun is split further into exec.initStart (exec of /.init up to its main), exec.processSpawn (its setup and the fork/exec of the command), exec.workload (the command, user work) and exec.processExit (/.init exiting, runc reaping it and exiting, and the stdio drain). The exec op is linked to every nested client the container connects.
  • core/lazy_state.go, dagql/cache.go syncResultSnapshotLeases: lock waits on a lazy group's body (core.lazyGroupOnce.mu), the whole-op latch (core.LazyState.LazyMu) and a result's snapshot lease sync (dagql.sharedResult.leaseSyncMu), recorded only when the lock is contended (wcprof.Lock).
  • core/container_exec.go: cache-volume lock waits, mount preparation and output-commit phases, and the wait on the executor.
  • core/services.go: service_start ops with singleflight wait edges.
  • engine/server/session.go serveQuery: per-query session_phase ops (attachables wait, workspace load, module load, schema build, query).
  • dagql/cache.go getOrInitCall, lookup facts: fact links from the call op saying why its lookup decided what it did, named as its span's dagger.io/cache.* attributes without the prefix: hit.remote (about the result hit), miss.saw_expired, miss.expired_remote, miss.incompatible_candidates, miss.incompatible_remote, miss.unavailable_parts, miss.chosen_remote, miss.producer_released, miss.requirements_grew, miss.unknown_input, miss.fresh_inputs, adopted and adopted.remote. They read the call's CacheDecision; when no recording span armed one, the profile arms it for the call and disarms it after (the carrier's own fact gathering, such as the new-inputs check, is then the profile's only added cost). Lookups and publications record contended waits for the cache's graph lock as lock waits on dagql.egraph, as an export's walk does.
  • engine/server/resolver, registry I/O: an io op per image pull (image.pull), resolve (image.resolve) and push (image.push), with the ref as ident and external work type. Under a pull, an image.fetch op per blob downloaded (its digest and the bytes read); under a push, an image.push.blob op per blob (its digest, and its size when it was sent rather than already in the registry).
  • engine/snapshots layer imports (remote-cache installs and image pulls): per layer, an io op snapshot.import.fetch[<source>] (download, builtin or present) and snapshot.import.apply, with the layer's digest as ident and its size in bytes, and a lock wait on snapshots.importLayer. A layer whose blob the import awaits from another source (a chain's Await) is an io wait on snapshots.awaitLayer:<digest> for as long as it waits. Each metadata write transaction the snapshot manager makes, as when it attaches a lease to a snapshot and its parents, is an internal op snapshot.metadata.write, like a containerd snapshotter call's, from asking for the database's single writer to the commit's end.
  • engine/snapshots owner leases: AttachLease and RemoveLease record a lock wait on snapshots.snapshotManager.mu when another holder blocks the manager's lock (wcprof.Lock), as do a chain import's index lookups, its layer reuse and record, GetBySnapshotID, New and a mutable snapshot's Commit. An install's prefetcher bookkeeping records a lock wait on dagql.partPrefetcher.mu.
  • dagql/cache.go runLazyEvalBody: the bookkeeping after a lazy body, each step that has work an internal op of the lazy op's: lazyBody.cleanup, lazyBody.ownerSync (the owner lease sync), lazyBody.releaseProtections, lazyBody.afterOwnerSync, lazyBody.settle (the parts installed), lazyBody.releaseOperationLease and lazyBody.teachContentIdentity, and a lazy wait on dagql.lazyBody.ownerSyncReady when the owner sync isn't ready yet.
  • Remote-cache installs (dagql/cache_part_content.go): one remote_cache op remotecache.install per attempt to install an offered part, with the part key as ident, the receiving result, and the bytes downloaded; a part link with the part's size (its whole chain's); the layers' import ops, remotecache.renew (fresh content addresses) and remotecache.install.commit as children. The commit's steps are internal ops under it: remotecache.install.prepare (with .probe, .pin and .store under it) and remotecache.install.publish, and its graph lock acquisitions are dagql.egraph lock waits. A part demand's acquire task (acquire:…) records each step of its passes as an internal op: part.demand.probe, .select (its source scan's graph lock acquisitions dagql.egraph lock waits), .decide, .call (running again the call of a value whose part nothing supplies and no operation makes), .join and .open. Outcomes are ok, refused, superseded, canceled or error (with an install.content fact when the source's bytes were bad). A part that no source could supply and that is computed locally gets a part.no_source fact. Lazy ops name their task (lazy:…, acquire:…, obtain:…) as ident. These read the same measurement points as the install's log line and span attributes.
  • Remote-cache exports (engine/remotecache/session.go, engine/server/remote_cache_protocol.go, dagql/cache_value_capture.go): one root remote_cache op remotecache.export per export the service asks for, named by its export ID, with the bytes uploaded, an export.root fact about each root the service asked for, and an export.no_bundle or export.empty fact when it sent nothing or the service asked for nothing. It waits on remotecache.exportSlots and on remotecache:service-upload (the service's upload request). Its children are remotecache.export.hold, remotecache.export.capture (a graph-lock wait on dagql.egraph, .walk, .copy, .bundle (checking the copied records and building the bundle of the roots it keeps) and .chains, with one remotecache.export.chain per exported part: the part key, its result and the bytes of the blobs it computed, over its layers' snapshot.export.diff ops and snapshots.exportLayer lock waits), and remotecache.upload, with a remotecache.upload.blob per PUT after a wait on remotecache.uploadSlots, then the export's releases: remotecache.export.release.chains, remotecache.export.release.capture (the capture's records) and remotecache.export.release.holds (the entries held for it). The capture links each output's part to its result with the part's size (its chain's). Merges are a remotecache.merge op, and each offered part a remotecache.offer op on the row its address resolved to, with an offer.<outcome> fact. Exports, merges and offers run on the service's connection, not a client's, so they are recorded when the engine records everything, not under --profile. Image exports get the snapshot.export.diff ops too.

Enabling and dumping

Two scopes:

  • Per-session: dagger --profile <anything> (hidden flag) sets ClientMetadata.Profile. Only that session's work is recorded — including its nested module/SDK clients — via a context mark applied by the session server and propagated to derived contexts. Caveat: if a profiled call joins in-flight identical work spawned by an unprofiled session, the join shows up as an unresolved wait (modeled by the analyzer as a fixed delay) since the other session's execution wasn't recorded.

  • Engine-global: record everything. Enable at startup with the engine's --wcprof flag (hidden), or at runtime by POSTing to the debug endpoint:

    curl -X POST -d on  http://<engine-debug-addr>/debug/wcprof/enabled
    curl -X POST -d off http://<engine-debug-addr>/debug/wcprof/enabled
    curl http://<engine-debug-addr>/debug/wcprof/enabled   # report state
    

    POSTing off stops engine-global recording but keeps buffered events dumpable, lets --profile sessions keep recording, and lets in-flight recorded ops finish; on re-enables on the same recorder (dumps from before and after merge cleanly since the epoch is unchanged).

The recorder keeps up to ~12.6M events (about 1 GB); events past that are dropped and counted in the dump header's dropped_events.

eBPF programs

A recording can also ask for eBPF programs compiled into the engine (engine/ebpf/wcbpf), by name. Each is loaded into the kernel and attached only while some recording asks for it, and nothing is loaded otherwise. It measures each profiled exec that starts while it's attached, and records what it measured on the exec's exec.run op as the exec ends. An exec that started before the program was attached isn't measured by it.

  • Per-session: dagger --profile --profile-ebpf exec_sched ... (hidden flags); the programs stay attached until the session ends.
  • Engine-global: the engine's --wcprof-ebpf exec_sched with --wcprof, or curl -X POST -d on 'http://<engine-debug-addr>/debug/wcprof/enabled?ebpf=exec_sched'. Each POST replaces the set; off releases it. A GET reports it as ebpf.

An unknown name is an error. A program that can't be loaded (a kernel without BTF, say) is skipped, and the dump header's ebpf list names every program asked for, with the error for any that couldn't be attached.

The programs:

  • exec_sched (engine/ebpf/execsched): the parts of each exec's container life as the kernel saw them, as exec_phase ops under its exec.run op: exec_sched.containerSetup (crun's child in the container's cgroup, up to its exec of /.init), exec_sched.initStart (up to the workload's exec), exec_sched.workload (up to the workload process's exit) and exec_sched.initExit (up to /.init's exit). And an exec_sched reading: the time the exec's threads, workload and mount helpers, waited runnable for a CPU (rqw, µs), how many such waits ended (rqn), and the time they ran (oncpu, µs).

  • Dump: GET http://<engine-debug-addr>/debug/wcprof/dump streams a JSON header line (string tables, open ops) followed by one JSON event per line, and flushes the buffer (pass ?flush=0 to keep it).

  • Dump at stop: with engine-global recording on, a stopping engine writes the whole buffer, unflushed, to <engine root>/wcprof/stop.ndjson.gz (wcprof.StopDumpPath): on the stop signal (engine.stop.signal), when each stop phase starts and ends (engine.stop.<phase> ops under engine.stop, waiting for a running garbage collection included), when the server's stop ends, and when each telemetry drain after it starts and ends (engine.exit.<step>). Each write replaces the file atomically and is itself a wcprof.stopDump op, so an engine killed mid-stop leaves the last complete dump, with the phase it was in as an open op. Writes run in the background, one at a time, so the stop doesn't wait for them (a large recording's dump takes seconds to write): requests made during a write become one more write, from the state at its start. The dump on disk lags the stop by at most the write in progress and one more; as it exits, the engine waits for one last write, so a stop that finishes leaves its whole record. The write that produced a dump is open in it too, as a wcprof.stopDump op, since it snapshots the recorder before it ends. So after a fully drained stop, the dump an engine leaves has that write as its only open op; a phase still running when the stop gave up on it (its deadline passed) stays open beside it. In a dump taken mid-stop, the work running at the snapshot is open as well: the stop and its current phase. An open op alone doesn't prove the work was cut off.

Typical dev-engine workflow:

./hack/dev   # build + start dagger-engine.dev (publishes debug port 6060)
./hack/with-dev ./bin/dagger --profile call engine-dev container sync
curl -s http://localhost:6060/debug/wcprof/dump > /tmp/wcprof.dump

Dumps are NDJSON: a DumpHeader line (with the interned strings table that class/ident/client indexes point into), then one DumpEvent per line. An event's b is a byte count: what an op moved, or a part link's size. Read them with wcprof.ReadDump.

Capturing a run that goes far too long

A session watch captures what the engine was doing when a session runs far longer than it should, whether or not anything is profiled, so a timed run with recording off still leaves evidence of a stall:

curl -X POST -d 60s http://<engine-debug-addr>/debug/wcprof/watch   # on, with a threshold
curl -X POST -d off http://<engine-debug-addr>/debug/wcprof/watch   # off
curl http://<engine-debug-addr>/debug/wcprof/captures               # the last 4 captures, JSON

While it's on, every session that starts gets a timer. A session that runs past the threshold gets one capture: every goroutine's stack, a 15 s CPU profile, and every goroutine's stack again, with the sessions running at the time and how long each had run. One capture runs at a time; when Go's CPU profiler is already in use (an armed phase profile or a /debug/pprof/profile pull), the capture keeps the stacks and records why it has no profile. A session keeps the threshold it started under when the threshold changes; switching the watch off stops every capture that hasn't started. With the watch off, a session's start costs one atomic load.

Analysis

This package is only the recorder; analysis tooling lives outside this repo. The offline what-if analyzer (cmd/wcprof-analyze, engine/wcprof/wcanalyze: replay-based what-if rankings, blocking chains, dead air) was moved out in #13588.

For agent-driven debugging, the engine-lab dev module (.dagger/modules/engine-lab) has wcprofEnable, wcprofCapture and wcprofReport tools: capture a dump from a from-source engine, then report per-class self time, per-parent child breakdowns, per-client activity, op subtrees, or name-resolved events through jq. Its wcprof-report helper is built against this package at tool-call time, so it tracks the dump format. See the engine-lab skill for the workflow.

Status / caveats

This is a prototype for validating the approach:

  • there is one engine-wide recorder and event buffer; per-session enablement scopes what gets recorded, but all enabled sessions share the buffer and dumps (the analyzer can group by client/root, but the dump endpoint has no per-session filter).
  • events are kept until dumped; long sessions can hit the event cap (the dump and the analyzer both surface the drop count loudly).
  • some leaf I/O (filesync, HTTP fetches, module loading) is not yet instrumented; it shows up as self-time of its calling op, and the "dead air" / unexplained-self-time reports are the guide for where to add hooks next.

Documentation

Overview

Package wcprof implements cheap wall-clock profiling for the engine.

It records operation intervals, wait (blocked-on) intervals, and link events into an in-memory buffer, for offline analysis of where wall-clock time goes and which operation classes are bottlenecks. It is intentionally not OTel: events are fixed-size structs appended to sharded in-memory buffers, with all strings interned. The expensive work (graph reconstruction, counterfactual simulation) happens offline after dumping the buffer via the engine debug endpoint.

Recording can be enabled two ways:

  • Engine-global: the engine's --wcprof flag, or POSTing "on" to the /debug/wcprof/enabled endpoint. All work on the engine is recorded until "off" is POSTed to the same endpoint.
  • Per-session: a client connecting with ClientMetadata.Profile set (the hidden --profile CLI flag). Only work attributable to that session (including its nested module/SDK clients) is recorded: the session server marks the session's contexts via ContextWithProfiling, and the mark propagates to derived contexts.

When profiling has never been enabled, all recording calls are a single atomic load + nil check.

Index

Constants

View Source
const (
	DumpTypeResource    = "resource"
	DumpTypeAcquisition = "acquisition"
)

Dump event types for reported resources and acquisitions.

View Source
const (
	// HolderUnknown: the lock was taken directly (Lock or TryLock), with no
	// context to name the op that took it.
	HolderUnknown uint64 = math.MaxUint64
	// HolderReaders: the lock was held for reading.
	HolderReaders uint64 = math.MaxUint64 - 1
)

Holders a recorded wait on an RWMutex can name in place of an op.

View Source
const (
	// UsageScopeExecCgroup is the exec's resource cgroup: the workload and the
	// mount helpers that run beside it, read after mount teardown.
	UsageScopeExecCgroup = "exec_cgroup"
	// UsageScopeMountHelpers is the mount helpers' own cgroup inside the
	// exec's resource cgroup, read with it, and only when a helper ran: the
	// workload's usage is the exec cgroup's less this.
	UsageScopeMountHelpers = "mount_helpers"
	// UsageScopeExecSched is the exec's resource cgroup's threads as the
	// exec_sched eBPF program saw them (engine/ebpf/execsched): the workload
	// and its mount helpers, from the exec's start, when a recording asked for
	// the program.
	UsageScopeExecSched = "exec_sched"
)

Usage scopes of an exec.

View Source
const DumpSchemaVersion = 1

DumpSchemaVersion identifies the dump wire format.

View Source
const DumpTypeTally = "tally"

DumpTypeTally is the DumpEvent.Type of an op's tallied calls (Tally).

View Source
const DumpTypeUsage = "usage"

DumpTypeUsage is the DumpEvent.Type of a resource reading (RecordUsage).

Variables

This section is empty.

Functions

func ContextWithOpID

func ContextWithOpID(ctx context.Context, opID uint64) context.Context

ContextWithOpID returns a context carrying the given op ID as the current op. Useful when handing work to a goroutine that should be attributed to an existing op.

func ContextWithProfiling

func ContextWithProfiling(ctx context.Context) context.Context

ContextWithProfiling marks ctx (and any context derived from it) for recording even when engine-global recording is off. The session server applies this to contexts of sessions that opted into profiling.

func ContextWithRequestArrival

func ContextWithRequestArrival(ctx context.Context, t time.Time) context.Context

ContextWithRequestArrival marks a request's ctx with when the engine received it, so the op for serving it can start there and show the routing before it (RequestArrival).

func CurrentOpID

func CurrentOpID(ctx context.Context) uint64

CurrentOpID returns the profiling op ID carried by ctx, or 0.

func DisableGlobal

func DisableGlobal()

DisableGlobal turns off engine-global recording. The recorder and its buffered events are retained for dumping, sessions that explicitly enabled profiling keep recording, and already-recording flows (contexts carrying a recorded op) run to completion.

func EnableGlobal

func EnableGlobal()

EnableGlobal turns on recording of all work on the engine.

func Enabled

func Enabled(ctx context.Context) bool

Enabled reports whether work running under ctx should be recorded. Call sites use it to skip computing op metadata (class/ident strings) when profiling is off. When profiling has never been enabled this is a single atomic load + nil check.

func EnsureRecorder

func EnsureRecorder()

EnsureRecorder creates the recorder if it does not exist yet, without turning on engine-global recording. Used when a session opts into profiling.

func Fact

func Fact(ctx context.Context, fromOpID uint64, fact string, resultID uint64)

Fact records that fact was true of the op fromOpID (0 means the current op in ctx), about the dagql result resultID when non-zero.

func FlushStopDump

func FlushStopDump(path string) error

FlushStopDump asks for the stop dump at path to be rewritten and waits for a write that began after the call, returning its error: the engine's last state before it exits is then on disk.

func GloballyEnabled

func GloballyEnabled() bool

GloballyEnabled reports whether engine-global recording is on.

func Link(ctx context.Context, kind LinkKind, fromOpID, targetOpID uint64, ident string, resultID uint64)

Link records a non-blocking correlation from fromOpID (0 means the current op in ctx) to an op, an interned ident, and/or a dagql result ID.

func LinkOTelParent

func LinkOTelParent(ctx context.Context, opID uint64, traceID, spanID string)

LinkOTelParent records that the engine op opID was caused by the span (traceID, spanID), e.g. a request a reported task made: in the dump the link resolves to that span's reported op, if it was recorded. A flushing dump drops the links it couldn't resolve, as most callers aren't reported spans: take it after the clients have exported their spans.

func Lock

func Lock(ctx context.Context, mu *sync.Mutex, ident string)

Lock acquires mu. When mu is held by someone else and work under ctx is being recorded, the time spent blocked is recorded as a lock wait of the current op on the resource named ident; otherwise it is mu.Lock with a TryLock in front. Use it for engine-internal mutexes held across slow work, whose waits would otherwise show up as the waiter's own time.

func MapOTelSpan

func MapOTelSpan(ctx context.Context, opID uint64, traceID, spanID string)

MapOTelSpan records that the engine op opID was begun under the span (traceID, spanID), e.g. a dagql call under its call span: a reported op whose causal parent is that span (a client inside the call's exec, or the module the call runs) resolves to opID in the dump. The first op mapped to a span keeps it, since calls under a telemetry-suppressed caller share the caller's span and the outermost of them is the one it stands for.

func NoteEBPFProgram

func NoteEBPFProgram(name string, err error)

NoteEBPFProgram records that a recording asked for the eBPF program name, with the error that kept it from being attached, if any. Each dump's header lists the programs asked for.

func NowNS

func NowNS() int64

NowNS returns the current recorder-relative timestamp, or 0 when profiling is disabled.

func Part

func Part(ctx context.Context, fromOpID uint64, part string, resultID uint64, size int64)

Part records, from the op fromOpID (0 means the current op in ctx), that the dagql result resultID has the part named part, of size bytes.

func ReadDump

func ReadDump(rd io.Reader) (*DumpHeader, []DumpEvent, error)

ReadDump parses a dump produced by WriteDump.

func ReadLock

func ReadLock(ctx context.Context, mu RWLocker, ident string)

ReadLock acquires mu for reading, recording a blocked acquisition as Lock does. On an RWMutex the wait names the lock's holder.

func RecordOp

func RecordOp(ctx context.Context, kind OpKind, class string, opts OpOpts, startNS, endNS int64, outcome Outcome) uint64

RecordOp records an already-completed op interval with explicit timestamps (from NowNS). Useful when an op's boundaries are observed from callbacks where holding an *Op handle would race. Returns the op ID, or 0 when profiling is disabled.

func RecordOpAt

func RecordOpAt(ctx context.Context, kind OpKind, class string, opts OpOpts, start, end time.Time, outcome Outcome) uint64

RecordOpAt is RecordOp for an interval stamped with time.Now, so a measurement point that already keeps time.Time stamps (for logs or span attributes) can feed the profile too. A zero start records nothing.

func RecordProducerComplete

func RecordProducerComplete(ctx context.Context, producer string, spanCount, dropped int64)

RecordProducerComplete records a producer's declaration of how many spans it reported and how many it failed to export.

func RecordReported

func RecordReported(ctx context.Context, op ReportedOp)

RecordReported records one reported op. A span already recorded is not recorded again (live and final exports of one span), and only ended spans are recorded. A no-op when profiling is disabled for ctx.

func RecordSpan

func RecordSpan(ctx context.Context, opID uint64, traceID [16]byte, spanID [8]byte)

RecordSpan is SetSpan for an op recorded by ID (RecordOp).

func RecordUsage

func RecordUsage(ctx context.Context, opID uint64, scope string, u Usage)

RecordUsage attaches a resource reading for scope to the op opID. It is a no-op when profiling is disabled for ctx or opID is 0. Readings count against the recorder's event cap like other events.

func RecordWaitAt

func RecordWaitAt(ctx context.Context, ident string, reason WaitReason, start, end time.Time)

RecordWaitAt records that the current op (from ctx) was blocked on the resource named ident from start to end, stamped with time.Now. A zero start records nothing.

func RequestArrival

func RequestArrival(ctx context.Context) time.Time

RequestArrival is when the engine received ctx's request (ContextWithRequestArrival), or the zero time.

func RequestStopDump

func RequestStopDump(path string)

RequestStopDump asks for the engine's stop dump at path to be rewritten, while the engine records everything (GloballyEnabled), and returns without waiting for the write. Each write is an op of its own, wcprof.stopDump. The dump on disk lags the latest request by at most the write in progress and one more; FlushStopDump waits for it.

func SetActiveForTest

func SetActiveForTest(r *Recorder, on bool) func()

SetActiveForTest installs r as the recorder, recording globally when on, and returns a function restoring the previous state. For tests of code that records through the package-level API.

func StopDumpPath

func StopDumpPath(root string) string

StopDumpPath is where an engine with state directory root keeps the dump of its stop (see WriteDumpFile).

func TimeNS

func TimeNS(t time.Time) int64

TimeNS returns t as a recorder-relative timestamp, like NowNS, for an op whose boundaries another library timed. 0 when profiling is disabled.

func WriteDumpFile

func WriteDumpFile(path string) (err error)

WriteDumpFile writes the recorder's dump to path, gzipped, without flushing the recorder. It writes a temporary file beside path and renames it over path, so a reader, or an engine killed mid-write, never leaves a partial dump. It does nothing when profiling was never enabled.

The engine writes its stop dump this way (RequestStopDump) when it receives SIGTERM, and as each graceful-stop phase and final drain begins and ends: an engine killed before it exits leaves the dump of its last finished write, with the work still running then as open ops. It compresses at gzip's fastest level, since it runs during the stop it records.

func WriteLock

func WriteLock(ctx context.Context, mu RWLocker, ident string)

WriteLock acquires mu for writing, recording a blocked acquisition as Lock does. On an RWMutex the wait names the lock's holder, and the op of ctx is its holder from then.

Types

type DumpEBPFProgram

type DumpEBPFProgram struct {
	Name string `json:"name"`
	// Error is why the program couldn't be attached the last time it was
	// asked for; empty when it was.
	Error string `json:"error,omitempty"`
}

DumpEBPFProgram is an eBPF program a recording asked for (see engine/ebpf/wcbpf).

type DumpEvent

type DumpEvent struct {
	Type     string `json:"e"`
	OpKind   string `json:"k,omitempty"`
	WorkType string `json:"w,omitempty"`
	Outcome  string `json:"o,omitempty"`
	Reason   string `json:"rs,omitempty"`
	LinkKind string `json:"lk,omitempty"`

	OpID     uint64 `json:"id,omitempty"`
	ParentID uint64 `json:"p,omitempty"`
	TargetID uint64 `json:"t,omitempty"`
	ResultID uint64 `json:"r,omitempty"`

	ClassID  uint32 `json:"c,omitempty"`
	IdentID  uint32 `json:"i,omitempty"`
	ClientID uint32 `json:"cl,omitempty"`
	MetaID   uint32 `json:"m,omitempty"`

	Bytes int64 `json:"b,omitempty"`
	// TraceID and SpanID are a span event's OTel span, in lower-case hex.
	TraceID string `json:"tr,omitempty"`
	SpanID  string `json:"sp,omitempty"`

	StartNS int64 `json:"s"`
	EndNS   int64 `json:"d"`

	// Reported ops (see reported.go) also carry their label and producer.
	LabelID    uint32 `json:"lb,omitempty"`
	ProducerID uint32 `json:"pr,omitempty"`
	// Resource events ("e":"resource") describe the resource op OpID; IdentID
	// is its display name.
	Capacity int64  `json:"cap,omitempty"`
	PolicyID uint32 `json:"pol,omitempty"`
	// Acquisition events ("e":"acquisition") describe the acquire op OpID:
	// TargetID is its resource op, Priority its priority, MetaID its
	// priority inputs (a JSON object), GrantedNS when it was granted (0 if
	// never), Outcome released, cancelled or held.
	Priority  int64 `json:"pri,omitempty"`
	GrantedNS int64 `json:"gr,omitempty"`

	// Usage events ("e":"usage") carry a resource reading for the op OpID:
	// ClassID names the scope (see the UsageScope constants), StartNS and
	// EndNS are when it was read.
	CPUUserUS    int64 `json:"cu,omitempty"`
	CPUSystemUS  int64 `json:"cs,omitempty"`
	MemPeakBytes int64 `json:"mp,omitempty"`
	CPUIowaitUS  int64 `json:"ciw,omitempty"`
	CPUStealUS   int64 `json:"cst,omitempty"`
	CPUIdleUS    int64 `json:"cid,omitempty"`
	MinorFaults  int64 `json:"fmin,omitempty"`
	MajorFaults  int64 `json:"fmaj,omitempty"`
	HugeFaults   int64 `json:"fhuge,omitempty"`
	CPUStallUS   int64 `json:"stcpu,omitempty"`
	MemStallUS   int64 `json:"stmem,omitempty"`
	GoAllocBytes int64 `json:"ga,omitempty"`
	GoGCCycles   int64 `json:"gcn,omitempty"`
	GoGCCPUUS    int64 `json:"gcc,omitempty"`
	IOReadBytes  int64 `json:"iorb,omitempty"`
	IOWriteBytes int64 `json:"iowb,omitempty"`
	MemAnonBytes int64 `json:"manon,omitempty"`
	MemFileBytes int64 `json:"mfile,omitempty"`
	MemHugeBytes int64 `json:"mhuge,omitempty"`

	GCAssistUS       int64 `json:"gca,omitempty"`
	GCDedicatedUS    int64 `json:"gcd,omitempty"`
	GCIdleUS         int64 `json:"gci,omitempty"`
	GCPauseUS        int64 `json:"gcp,omitempty"`
	HeapLiveBytes    int64 `json:"hlive,omitempty"`
	HeapGoalBytes    int64 `json:"hgoal,omitempty"`
	ScanHeapBytes    int64 `json:"sheap,omitempty"`
	ScanStackBytes   int64 `json:"sstack,omitempty"`
	ScanGlobalsBytes int64 `json:"sglob,omitempty"`

	RunqWaitUS int64 `json:"rqw,omitempty"`
	RunqWaits  int64 `json:"rqn,omitempty"`
	OnCPUUS    int64 `json:"oncpu,omitempty"`

	// A wait on an RWMutex carries the lock's holder when the wait began:
	// HolderID the op that took it through WriteLock, HolderUnknown when
	// it was taken directly, with no context to name the op, or
	// HolderReaders when it was held for reading; and HolderSinceNS when
	// the holder took it, 0 for readers or when not known. Only locks of
	// type RWMutex have a holder: a wait on another lock has none. A lock
	// taken directly, or caught between a release and the next taker's
	// mark, reads as HolderUnknown, never as an earlier holder's op;
	// readers aren't named one by one.
	HolderID      uint64 `json:"h,omitempty"`
	HolderSinceNS int64  `json:"hs,omitempty"`

	// Tally events (DumpTypeTally) count the calls of one class (OpKind,
	// ClassID) made under the op ParentID, from its start to its end, or to
	// the dump for an op still open (StartNS, EndNS): how many, how many
	// failed, and their summed duration. A later dump of the same recorder
	// can have the same op's tally again, grown.
	Count   int64 `json:"n,omitempty"`
	Errors  int64 `json:"er,omitempty"`
	TotalNS int64 `json:"tn,omitempty"`
}

DumpEvent is the JSON form of an Event. Short keys keep dumps compact.

type DumpHeader

type DumpHeader struct {
	SchemaVersion  int      `json:"schema_version"`
	EpochUnixNano  int64    `json:"epoch_unix_nano"`
	DumpedUnixNano int64    `json:"dumped_unix_nano"`
	DroppedEvents  uint64   `json:"dropped_events"`
	EventCount     int      `json:"event_count"`
	Strings        []string `json:"strings"`
	// OpenOps are ops begun but not ended at dump time (e.g. in-flight or
	// hung work).
	OpenOps []DumpOpenOp `json:"open_ops,omitempty"`
	// Producers are the clients that reported their own work: per producer,
	// the spans received and, once it declared them, the count it reported
	// and how many it failed to export. A producer whose Received is below
	// its Declared, or that never declared, makes the dump's reported work
	// incomplete.
	Producers []DumpProducer `json:"producers,omitempty"`
	// Machine describes the machine the engine ran on.
	Machine *DumpMachine `json:"machine,omitempty"`
	// EBPF lists the eBPF programs recordings have asked for since the
	// engine started, and why each that couldn't be attached wasn't.
	EBPF []DumpEBPFProgram `json:"ebpf,omitempty"`
}

DumpHeader is the first line of a dump. The remaining lines are one DumpEvent JSON object per line.

type DumpMachine

type DumpMachine struct {
	// KernelRelease and KernelVersion are the kernel's release (as uname -r)
	// and its build string (as uname -v), which names the preemption models
	// it was built with (PREEMPT, PREEMPT_DYNAMIC, PREEMPT_RT).
	KernelRelease string `json:"kernel_release,omitempty"`
	KernelVersion string `json:"kernel_version,omitempty"`
	// Preempt is the preemption model in use, when the kernel can switch it
	// at boot (PREEMPT_DYNAMIC) and debugfs is readable: none, voluntary,
	// full or lazy.
	Preempt  string `json:"preempt,omitempty"`
	PageSize int    `json:"page_size"`
	// THPEnabled and THPDefrag are the transparent huge page settings
	// (always, madvise or never; and the defrag policy).
	THPEnabled string `json:"thp_enabled,omitempty"`
	THPDefrag  string `json:"thp_defrag,omitempty"`
	// CPUs are the CPUs the engine process may run on, and GOMAXPROCS how
	// many it runs Go code on at once.
	CPUs       int `json:"cpus"`
	GOMAXPROCS int `json:"gomaxprocs"`
}

DumpMachine is what a dump records about the machine its engine ran on, read at each dump, since settings such as transparent huge pages can change while an engine runs. A fact that couldn't be read is left out.

type DumpOpenOp

type DumpOpenOp struct {
	OpID     uint64 `json:"op_id"`
	ParentID uint64 `json:"parent_id,omitempty"`
	Kind     string `json:"kind"`
	WorkType string `json:"work_type,omitempty"`
	ClassID  uint32 `json:"class_id,omitempty"`
	IdentID  uint32 `json:"ident_id,omitempty"`
	ClientID uint32 `json:"client_id,omitempty"`
	MetaID   uint32 `json:"meta_id,omitempty"`
	StartNS  int64  `json:"start_ns"`
}

DumpOpenOp describes an in-progress op at dump time.

type DumpProducer

type DumpProducer struct {
	Producer string `json:"producer"`
	Received int64  `json:"received"`
	Declared int64  `json:"declared,omitempty"`
	Dropped  int64  `json:"dropped,omitempty"`
	Complete bool   `json:"complete,omitempty"`
}

DumpProducer is one producer's completeness accounting.

type Event

type Event struct {
	Type     EventType
	OpKind   OpKind
	WorkType WorkType
	Outcome  Outcome
	Reason   WaitReason
	LinkKind LinkKind

	OpID     uint64
	ParentID uint64
	TargetID uint64
	ResultID uint64

	ClassID  uint32
	IdentID  uint32
	ClientID uint32
	// MetaID is an interned op-metadata string (currently the canonical
	// scalar JSON-array encoding of a user exec's scrubbed argv), 0 when none.
	MetaID uint32

	Bytes int64

	StartNS int64
	EndNS   int64
}

Event is one fixed-size profiling record. Field meaning varies by Type:

  • Op: OpID/ParentID identify the op and its structural parent. StartNS and EndNS bound the interval. ClassID/IdentID/ClientID are interned strings. ResultID is the dagql shared result ID when known.
  • Wait: ParentID is the waiting op, TargetID the awaited op (or 0 when waiting on a named resource, in which case IdentID names it).
  • Link: ParentID is the from-op, TargetID an op (optional), IdentID an interned identifier (optional), ResultID a dagql result (optional).
  • Span: OpID is the op, TargetID and ResultID the high and low halves of its span's trace ID, ParentID the span ID, all big-endian.

Bytes is a byte count an op moved or a part link names, 0 when none.

type EventType

type EventType uint8

EventType discriminates the union in Event.

const (
	EventTypeInvalid EventType = iota
	// EventTypeOp is a completed operation interval.
	EventTypeOp
	// EventTypeWait is a completed wait interval: an op was blocked on
	// another op (or named resource) from Start to End.
	EventTypeWait
	// EventTypeLink is a non-blocking correlation between an op and another
	// op or an interned identifier.
	EventTypeLink
	// EventTypeSpan names the OTel span an op owns: the span the engine
	// emits for the same work.
	EventTypeSpan
	// EventTypeTally is an op's tallied calls of one class (Tally).
	EventTypeTally
)

type LinkKind

type LinkKind uint8

LinkKind classifies link events.

const (
	LinkKindInvalid LinkKind = iota
	// LinkKindNestedClient: op (an exec) hosts a nested client whose ID is
	// the link's interned ident. Ops recorded for that client belong under
	// this exec.
	LinkKindNestedClient
	// LinkKindResult: op produced or returned the dagql result with
	// ResultID.
	LinkKindResult
	// LinkKindReusedResult: op was satisfied by reusing the dagql result
	// with ResultID (cache hit).
	LinkKindReusedResult
	// LinkKindReportedCause: the engine op (from) was caused by the reported
	// op (target), e.g. a request a reported task made.
	LinkKindReportedCause
	// LinkKindUnresolvedSpan: a reported op (from) refers to a span (the
	// ident, "<trace>/<span>") that wasn't recorded by the time of the dump.
	LinkKindUnresolvedSpan
	// LinkKindFact: the fact named by the link's ident was true of the op
	// (e.g. "hit.remote" on a call), about the dagql result with ResultID
	// when set.
	LinkKindFact
	// LinkKindPart: the dagql result with ResultID has the part named by the
	// link's ident, of Bytes bytes (the compressed size of its chain).
	LinkKindPart
)

func (LinkKind) String

func (k LinkKind) String() string

type Op

type Op struct {
	// contains filtered or unexported fields
}

Op is a handle for an in-progress operation. A nil *Op is valid and all methods are no-ops, so call sites do not need to check whether profiling is enabled.

func BeginOp

func BeginOp(ctx context.Context, kind OpKind, class string, opts OpOpts) (context.Context, *Op)

BeginOp starts recording an operation. It returns a derived context that carries the new op as the current op (so nested ops and waits parent to it), and a handle to end it. When profiling is disabled it returns ctx unchanged and a nil handle.

func (*Op) End

func (op *Op) End(outcome Outcome)

End records the op's completion.

func (*Op) EndAt

func (op *Op) EndAt(outcome Outcome, resultID uint64, end time.Time)

EndAt is EndWithResult at end, stamped with time.Now, when a measurement point already stamped the op's end.

func (*Op) EndErr

func (op *Op) EndErr(err error)

EndErr records completion with OutcomeOK or OutcomeError based on err.

func (*Op) EndWithResult

func (op *Op) EndWithResult(outcome Outcome, resultID uint64)

EndWithResult records the op's completion, associating it with a dagql shared result ID when non-zero.

func (*Op) ID

func (op *Op) ID() uint64

ID returns the op's ID, or 0 for a nil handle.

func (*Op) OutcomeHint

func (op *Op) OutcomeHint() Outcome

OutcomeHint returns the hint set by SetOutcomeHint, or OutcomeNone.

func (*Op) SetBytes

func (op *Op) SetBytes(n int64)

SetBytes sets the byte count the op moved. Must be called from the goroutine that owns the op, before End.

func (*Op) SetIdent

func (op *Op) SetIdent(ident string)

SetIdent updates the op's instance identity (useful when it only becomes known after the op began, e.g. a recipe digest derived mid-call). Must be called from the goroutine that owns the op, before End.

func (*Op) SetOutcomeHint

func (op *Op) SetOutcomeHint(outcome Outcome)

outcomeHint carries an outcome decided mid-op (e.g. joined vs executed), read back by the code that ends the op.

func (*Op) SetSpan

func (op *Op) SetSpan(traceID [16]byte, spanID [8]byte)

SetSpan records the OTel span the op owns, the span the engine emits for the same work, so offline analysis can join the op to the span's attributes. A zero trace or span ID records nothing.

func (*Op) StartTime

func (op *Op) StartTime() time.Time

StartTime is when the op began, or the zero time for a nil handle.

type OpKind

type OpKind uint8

OpKind classifies operations.

const (
	OpKindInvalid OpKind = iota
	// OpKindCall is one dagql GetOrInitCall invocation (per caller,
	// including cache hits and singleflight joiners).
	OpKindCall
	// OpKindCallExec is the shared execution of a call's resolver function.
	// Singleflighted callers all wait on one of these.
	OpKindCallExec
	// OpKindLazy is one run of a lazy evaluation callback for a result.
	OpKindLazy
	// OpKindExecPhase is a setup/run phase of a container exec
	// (e.g. exec.setupNetwork, exec.runContainer).
	OpKindExecPhase
	// OpKindExec is the overall run of a container by the executor.
	OpKindExec
	// OpKindServiceStart is the start (incl. health check) of a service.
	OpKindServiceStart
	// OpKindSessionPhase is a per-query session serving phase
	// (e.g. session.workspaceLoad, session.query).
	OpKindSessionPhase
	// OpKindIO is a leaf I/O operation (git fetch, image pull, filesync...).
	OpKindIO
	// OpKindInternal is engine-internal background work (gc, persistence).
	OpKindInternal
	// OpKindTask is one unit of a client's own work, reported by the client
	// (e.g. one package of a Go build); see the reporting contract in
	// engine/wcprof/contract.
	OpKindTask
	// OpKindTaskGroup is a reported parent whose child tasks run concurrently,
	// ordered only by their dependency waits and resource acquisitions.
	OpKindTaskGroup
	// OpKindResource is a reported limited resource instance (capacity and
	// admission policy), for the life of the instance.
	OpKindResource
	// OpKindAcquire is a reported acquisition of one unit of a resource, from
	// the request to the release.
	OpKindAcquire
	// OpKindRemoteCache is remote-cache work: a part's install, an export
	// and its uploads, an offer or a merge.
	OpKindRemoteCache
)

func ParseOpKind

func ParseOpKind(s string) OpKind

ParseOpKind is the OpKind named s (as OpKind.String writes it), or OpKindInvalid.

func (OpKind) String

func (k OpKind) String() string

type OpOpts

type OpOpts struct {
	// Ident is an instance identity for the op (e.g. recipe digest, exec ID).
	Ident string
	// ClientID is the dagger client this op serves, when known.
	ClientID string
	// WorkType attributes the op's self time.
	WorkType WorkType
	// Argv is the user command for a container-exec op (already scrubbed and
	// bounded by the caller). The recorder marshals it to the canonical scalar
	// JSON-array string and interns it as the op's MetaID, so offline analysis
	// can rank the exec by its real command. Empty for non-exec ops.
	Argv []string
	// Bytes is a byte count the op moved (e.g. downloaded or uploaded), when
	// known at Begin time; see Op.SetBytes.
	Bytes int64
	// ResultID is the dagql shared result the op is about, for RecordOp and
	// RecordOpAt (a begun op takes it at End).
	ResultID uint64
	// Start, when set, is when the op began, for an op whose work began
	// before BeginOp (e.g. at its OTel span's start); zero is now.
	Start time.Time
}

OpOpts carries optional per-op metadata.

type Outcome

type Outcome uint8

Outcome describes how an op completed.

const (
	OutcomeNone Outcome = iota
	// OutcomeHit: dagql call satisfied from cache.
	OutcomeHit
	// OutcomeExecuted: dagql call missed cache and this caller spawned the
	// execution.
	OutcomeExecuted
	// OutcomeJoined: dagql call missed cache and joined an in-flight
	// execution started by another caller.
	OutcomeJoined
	// OutcomeDoNotCache: dagql call executed inline without caching.
	OutcomeDoNotCache
	// OutcomeOK: generic success for non-call ops.
	OutcomeOK
	// OutcomeError: op failed.
	OutcomeError
	// OutcomeCanceled: op canceled.
	OutcomeCanceled
	// OutcomeRefused: op gave way to coordination (e.g. a remote-cache
	// install refused so its demand selects again); not a failure.
	OutcomeRefused
	// OutcomeSuperseded: op's work was done by another op meanwhile (e.g. a
	// part another install installed first).
	OutcomeSuperseded
)

func (Outcome) String

func (o Outcome) String() string

type RWLocker

type RWLocker interface {
	Lock()
	TryLock() bool
	RLock()
	TryRLock() bool
}

RWLocker is a read-write lock: a sync.RWMutex, or an RWMutex.

type RWMutex

type RWMutex struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

RWMutex is a sync.RWMutex that remembers who holds it while a recorder is active, so that a wait on it recorded by WriteLock or ReadLock names its holder: the op that took it through WriteLock, HolderUnknown when it was taken directly, or HolderReaders when it was held for reading. Releasing it makes the holder unknown before the lock is free, so a waiter that reads it before the next taker marks itself reads HolderUnknown, never the holder that released it; and a waiter whose read overlaps a change of holder reads HolderUnknown too.

func (*RWMutex) Holder

func (m *RWMutex) Holder() (uint64, int64)

Holder is m's holder and when it took m, as a wait on m begun now would record them; 0 while no recorder has been active.

func (*RWMutex) Lock

func (m *RWMutex) Lock()

Lock locks m for writing, its holder unknown.

func (*RWMutex) ObserveMarks

func (m *RWMutex) ObserveMarks(f func(holder uint64)) (stop func())

ObserveMarks calls f, in the acquiring goroutine, with the holder each exclusive acquisition of m marks while a recorder is active, until the returned stop is called; for tests, which can tell from f's caller which code took m and whether it named its op.

func (*RWMutex) RLock

func (m *RWMutex) RLock()

RLock locks m for reading.

func (*RWMutex) RUnlock

func (m *RWMutex) RUnlock()

RUnlock unlocks m for reading. A reader leaving while others still read makes the holder unknown until the next reader marks it: unknown, not wrong.

func (*RWMutex) TryLock

func (m *RWMutex) TryLock() bool

TryLock tries to lock m for writing, its holder unknown.

func (*RWMutex) TryRLock

func (m *RWMutex) TryRLock() bool

TryRLock tries to lock m for reading.

func (*RWMutex) Unlock

func (m *RWMutex) Unlock()

Unlock unlocks m for writing.

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder collects profiling events. Safe for concurrent use.

func Active

func Active() *Recorder

Active returns the recorder, or nil if profiling was never enabled. The recorder outlives DisableGlobal so buffered events can still be dumped; use GloballyEnabled/Enabled to ask whether work is being recorded.

func NewRecorder

func NewRecorder(maxEvents int64) *Recorder

NewRecorder returns a recorder with the given event cap (<=0 means default).

func (*Recorder) Intern

func (r *Recorder) Intern(s string) uint32

Intern interns s and returns its table ID.

func (*Recorder) Now

func (r *Recorder) Now() int64

Now returns the current recorder-relative timestamp in nanoseconds.

func (*Recorder) WriteDump

func (r *Recorder) WriteDump(w io.Writer, flush bool) error

WriteDump streams the recorder state to w: one DumpHeader JSON line, then one DumpEvent JSON line per event. When flush is true, recorded events are removed from the buffer (the string table, open ops, and op ID counter are retained so later dumps remain consistent).

type ReportedAcquisition

type ReportedAcquisition struct {
	ResourceSpanID string
	Priority       int64
	PriorityInputs map[string]string
	// GrantedUnixNS is when the unit was granted, 0 if never.
	GrantedUnixNS int64
	Outcome       string
}

ReportedAcquisition describes an "acquire" op.

type ReportedOp

type ReportedOp struct {
	TraceID string
	SpanID  string
	// ParentSpanID is the op's causal parent span (same trace), "" for none.
	ParentSpanID string
	Kind         OpKind
	Work         WorkType
	// Name is the span's name, the op's class.
	Name        string
	Label       string
	Producer    string
	StartUnixNS int64
	EndUnixNS   int64
	Waits       []ReportedWait
	Resource    *ReportedResource
	Acquisition *ReportedAcquisition
}

ReportedOp is one reported span to record, already decoded.

type ReportedResource

type ReportedResource struct {
	// Name is the resource's display name, e.g. "gobuild.slots".
	Name     string
	Capacity int64
	Policy   string
}

ReportedResource describes a "resource" op.

type ReportedWait

type ReportedWait struct {
	TargetTraceID string
	TargetSpanID  string
	Reason        WaitReason
	Ident         string
	StartUnixNS   int64
	EndUnixNS     int64
}

ReportedWait is a wait link of a reported op.

type TallyCall

type TallyCall struct {
	// contains filtered or unexported fields
}

TallyCall is one tallied call in progress (BeginTally). Its zero value, for unprofiled work, records nothing.

func BeginTally

func BeginTally(ctx context.Context, tc *TallyClass) TallyCall

BeginTally starts a call of class tc under the op current in ctx, when ctx is profiled; otherwise it returns the zero TallyCall without reading the clock.

func (TallyCall) End

func (c TallyCall) End(err error)

End tallies the call, failed when err is set, on the op current in its context. When that op was only handed over by ID (ContextWithOpID), or has already ended, the call is recorded as an op of its own under it instead.

type TallyClass

type TallyClass struct {
	// contains filtered or unexported fields
}

A TallyClass names calls recorded as a count, a summed duration and a count of failures on the op they run under (BeginTally), rather than as an op each: calls frequent enough that recording an op apiece costs the engine time. An op's tallies are recorded with it when it ends, a tally event per class (DumpTypeTally); a dump taken while it's open has its tallies so far.

func NewTallyClass

func NewTallyClass(kind OpKind, class string) *TallyClass

NewTallyClass registers a tallied call class, of ops of kind kind. Call it from a package-level var, so it runs while packages initialize: there are at most maxTallyClasses.

type Usage

type Usage struct {
	CPUUserUS   int64
	CPUSystemUS int64
	// MemPeakBytes is the scope's peak memory, 0 when it couldn't be read.
	MemPeakBytes int64
	// CPUIowaitUS, CPUStealUS and CPUIdleUS are a whole machine's CPU time
	// waiting for I/O, taken by its hypervisor for other guests, and idle,
	// summed over its cores. Only machine scopes have them.
	CPUIowaitUS int64
	CPUStealUS  int64
	CPUIdleUS   int64
	// MinorFaults and MajorFaults are the scope's page faults: those served
	// without I/O, and those that needed it. HugeFaults are those that
	// allocated a transparent huge page. Process and exec scopes have them.
	MinorFaults int64
	MajorFaults int64
	HugeFaults  int64
	// CPUStallUS and MemStallUS are the time any of the scope's tasks
	// stalled waiting for a CPU and for memory (its cgroup's cpu.pressure and
	// memory.pressure "some" totals). Only exec scopes have them.
	CPUStallUS int64
	MemStallUS int64
	// GoAllocBytes, GoGCCycles and GoGCCPUUS are a Go process's cumulative
	// heap allocations, its completed GC cycles, and the CPU time its runtime
	// estimates the GC used. Only the engine process scope has them.
	GoAllocBytes int64
	GoGCCycles   int64
	GoGCCPUUS    int64
	// IOReadBytes and IOWriteBytes are the bytes a cgroup scope read from
	// and wrote to block devices.
	IOReadBytes  int64
	IOWriteBytes int64
	// MemAnonBytes, MemFileBytes and MemHugeBytes are a cgroup's memory at
	// the reading's time: anonymous memory, file pages, and the part of both
	// in transparent huge pages. Only sample scopes have them.
	MemAnonBytes int64
	MemFileBytes int64
	MemHugeBytes int64
	// GCAssistUS, GCDedicatedUS, GCIdleUS and GCPauseUS are the CPU time a
	// Go process's GC took, by class, as the runtime estimates it: mark
	// assists by allocating goroutines, dedicated and idle mark workers, and
	// stop-the-world pauses, counted as the pause's duration times GOMAXPROCS
	// rather than processor time used. HeapLiveBytes is the heap the last GC
	// marked live, and HeapGoalBytes the heap size the next GC aims for.
	// ScanHeapBytes and ScanGlobalsBytes are the scannable heap and globals
	// now, and ScanStackBytes the stack the last GC scanned. Only Go runtime
	// sample scopes have them; see runtime/metrics for the definitions.
	GCAssistUS       int64
	GCDedicatedUS    int64
	GCIdleUS         int64
	GCPauseUS        int64
	HeapLiveBytes    int64
	HeapGoalBytes    int64
	ScanHeapBytes    int64
	ScanStackBytes   int64
	ScanGlobalsBytes int64
	// RunqWaitUS is the time the scope's threads spent runnable but waiting
	// for a CPU, summed over its threads, and RunqWaits how many such waits
	// ended; OnCPUUS is the time its threads ran. Only exec scheduling scopes
	// have them.
	RunqWaitUS int64
	RunqWaits  int64
	OnCPUUS    int64
}

Usage is a resource reading attached to an op: the CPU time and peak memory of one scope (see the UsageScope constants), read once the scope's work is done.

type Wait

type Wait struct {
	// contains filtered or unexported fields
}

Wait is a handle for an in-progress wait interval. A nil *Wait is valid and End is a no-op.

func BeginWait

func BeginWait(ctx context.Context, targetOpID uint64, reason WaitReason) *Wait

BeginWait starts recording that the current op (from ctx) is blocked on targetOpID for the given reason. Returns nil when profiling is disabled.

func BeginWaitIdent

func BeginWaitIdent(ctx context.Context, ident string, reason WaitReason) *Wait

BeginWaitIdent is BeginWait for waits on a named resource (e.g. a lock) rather than another op.

func (*Wait) End

func (w *Wait) End()

End records the wait interval.

func (*Wait) SetTarget

func (w *Wait) SetTarget(targetOpID uint64)

SetTarget updates the awaited op (useful when the target becomes known only after the wait began).

type WaitReason

type WaitReason uint8

WaitReason describes why an op was blocked.

const (
	WaitReasonInvalid WaitReason = iota
	// WaitReasonCallExec: blocked waiting for a call's resolver execution
	// (the caller that spawned it).
	WaitReasonCallExec
	// WaitReasonSingleflight: blocked joining another caller's in-flight
	// execution of the same call.
	WaitReasonSingleflight
	// WaitReasonLazy: blocked waiting for a lazy evaluation to finish.
	WaitReasonLazy
	// WaitReasonService: blocked waiting for a service to start/be healthy.
	WaitReasonService
	// WaitReasonLock: blocked acquiring a lock (target is an interned
	// resource name, not an op).
	WaitReasonLock
	// WaitReasonExec: blocked waiting for a container exec to finish.
	WaitReasonExec
	// WaitReasonIO: blocked on external I/O.
	WaitReasonIO
	// WaitReasonDependency: a reported task waited for one of its
	// dependencies' results (zero-length when it was already done).
	WaitReasonDependency
	// WaitReasonAttach: a publication blocked on a result's dependency
	// attachment, which another publication or a merge holds open.
	WaitReasonAttach
)

func ParseWaitReason

func ParseWaitReason(s string) WaitReason

ParseWaitReason is the WaitReason named s, or WaitReasonInvalid.

func (WaitReason) String

func (r WaitReason) String() string

type WorkType

type WorkType uint8

WorkType is a coarse attribution of an op's self-time, used by analysis to separate actionable engine overhead from user workload time and external I/O time.

const (
	WorkTypeEngine WorkType = iota
	// WorkTypeUser is time running user-controlled work (e.g. the container
	// process itself).
	WorkTypeUser
	// WorkTypeExternal is time bound on external systems (registry pulls,
	// git remotes, host filesync...).
	WorkTypeExternal
)

func ParseWorkType

func ParseWorkType(s string) WorkType

ParseWorkType is the WorkType named s, or WorkTypeEngine.

func (WorkType) String

func (w WorkType) String() string

Directories

Path Synopsis
Package contract decodes the wcprof reporting contract: how a client or a program inside an exec reports its own work (tasks and task groups), what each task waited for (dependency waits), its limited resources and the acquisitions of them, and its completeness, as OTel spans and links with the wcprof.* attributes of engine/telemetryattrs.
Package contract decodes the wcprof reporting contract: how a client or a program inside an exec reports its own work (tasks and task groups), what each task waited for (dependency waits), its limited resources and the acquisitions of them, and its completeness, as OTel spans and links with the wcprof.* attributes of engine/telemetryattrs.

Jump to

Keyboard shortcuts

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