Documentation
¶
Overview ¶
Package exprof gives every program under examples/ one identical profiling contract: a -profile-dir flag that writes cpu.pprof and heap.pprof, a -trace flag that writes a runtime/trace, and a -contention flag that adds mutex.pprof, block.pprof and goroutine.pprof.
Responsibility ¶
exprof owns the *instrumentation* of an example: binding the three flags, starting and stopping the profilers in the correct order, setting and restoring the contention sampling rates, and reporting the artefact paths as telemetry. It owns nothing else. It never touches GoGraph, never interprets an example's workload, and never writes anything to the example's output when no flag is set.
The reason it exists as one package rather than as a copy inside each example is that the contract must be *identical* across all of them: a reader who learns -profile-dir on one example knows it on every other. Thirty-seven hand-maintained copies would be free to drift apart silently; one implementation cannot.
Inert by default ¶
With no flag set every operation is a no-op: no directory is created, no profiler runs, no sampling rate is touched, and not one byte reaches the example's writer. This is what lets each example's regression test pin its deterministic output unedited.
Why contention is opt-in, and separate ¶
The mutex and block profilers are not free the way the CPU profiler is. Both accumulate for as long as their rate is non-zero, and at the resolution worth attributing — rate 1, every event recorded — the block profiler charges a stack walk to every blocking operation the workload performs. That cost lands on the very workload a CPU profile is trying to attribute, so it must never be paid by default or implicitly: -profile-dir alone buys CPU and heap and nothing else, and a CPU-attribution run is unchanged by this package's extension.
-contention therefore requires -profile-dir rather than implying it. Asking for contention profiles with nowhere to write them is a usage error, and it is reported as one at setup rather than silently ignored.
Ordering, and why it matters ¶
Four orderings here are load-bearing and are the reason this is a package and not a dozen lines at each call site:
- The CPU profile is stopped *before* the heap profile is written, so the profiler's own teardown allocations are not attributed to the workload.
- runtime.GC runs before the heap profile, because a heap profile reports what was live as of the last collection. Without it the profile counts garbage that is merely unswept, which reads as a leak that is not there.
- The contention rates are raised *before* the CPU profiler starts and dropped only *after* the profiles that depend on them are written, since both profilers accumulate for exactly as long as their rate is set. A narrower window would under-count the workload's own contention.
- The mutex and block profiles are written BEFORE the CPU profile is stopped, and this one is not a nicety. pprof.StopCPUProfile blocks on a channel receive while it drains the profiler's buffer, and with the block rate still live that wait is recorded AS BLOCKING. Measured on this host with the writes ordered the other way round, examples/09_leiden — a single-goroutine program — reported 200.27 ms of block delay of which 100% was runtime.chanrecv1 under stopCPU. The instrument was reporting itself. Writing the two contention profiles first costs only that their own small write appears in the CPU profile, where it is visible and attributable instead of swamping the measurement.
- The goroutine profile is written from the same Finish, after the workload and after the forced collection, which means it is a POST-workload snapshot. That makes it evidence of goroutine *retention* — what the workload failed to shut down — and not of peak concurrency. Measured, its single sample is usually runtime.goroutineProfileWithLabels itself, which is the correct reading of "nothing was left running". An example that needs the peak must capture it at its own peak; examples/23_bolt_server does exactly that.
Usage ¶
The common shape is Config.Run, which guarantees the profilers are stopped even when the workload fails — a plain defer would not, because examples end in log.Fatal, and os.Exit does not run deferred calls:
func main() {
cfg := defaultConfig()
flag.IntVar(&cfg.nodes, "nodes", cfg.nodes, "number of nodes")
prof := exprof.Bind(flag.CommandLine)
flag.Parse()
if err := prof.Run(os.Stdout, func() error {
return run(context.Background(), os.Stdout, cfg)
}); err != nil {
log.Fatal(err)
}
}
Examples that drive several batteries, or that must stop the CPU profile at a point of their own choosing, use Config.Start and Session.Finish directly.
Concurrency ¶
A Config is bound and read on one goroutine before the workload starts and is not safe for concurrent mutation. A Session's Finish is safe to call from any goroutine and is idempotent, so an error path and a success path may both call it. The underlying profilers are process-global: at most one Session may be active at a time, which is the natural shape for a single-purpose example binary. The two contention sampling rates are process-global as well; a Session restores the mutex fraction it found and resets the block rate, which is all the runtime allows (see Session.Finish).
Index ¶
Constants ¶
const ( CPUProfileName = "cpu.pprof" HeapProfileName = "heap.pprof" // The three below are written only when -contention is set. MutexProfileName = "mutex.pprof" BlockProfileName = "block.pprof" GoroutineProfileName = "goroutine.pprof" )
Artefact basenames written into the -profile-dir. They are fixed rather than configurable so that a reader, a script, or a later cycle finds the profile of any example at the same path without consulting that example's flags.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Dir is -profile-dir: the directory to write cpu.pprof and heap.pprof
// into. Empty disables both CPU and heap profiling.
Dir string
// Trace is -trace: the file to write a runtime/trace into. Empty disables
// tracing.
Trace string
// Contention is -contention: the sampling rate for the mutex and block
// profilers. Zero — the default — disables both and writes no goroutine
// profile either. A positive value additionally requires Dir, because there
// would otherwise be nowhere to write the three artefacts; Start reports
// that combination as a setup error rather than ignoring it.
//
// The value is passed to BOTH runtime.SetMutexProfileFraction and
// runtime.SetBlockProfileRate. The two units differ — a fraction of
// contention events for the first, nanoseconds spent blocked per sample for
// the second — but they share the direction that matters to an operator: 1
// records every event, and a larger value trades resolution for overhead.
Contention int
}
Config is the profiling destination set an example binds from its flags. The zero Config is valid and inert.
func Bind ¶
Bind registers -profile-dir and -trace on fs and returns the Config they fill. Call it before fs.Parse.
The help text is defined here, once, so it reads identically in every example's -h output.
func (*Config) Run ¶
Run starts the profilers, calls fn, and stops them — whether fn succeeds or fails. It is the shape every example should use unless it needs to control where the CPU profile stops.
It exists because the alternative is a hazard rather than a preference: examples end a failed run with log.Fatal, os.Exit does not run deferred calls, and so a deferred stop would silently truncate the profile on exactly the runs worth profiling.
Errors follow the two-phase rule:
- A SETUP failure is fail-fast: if the profilers cannot be started, fn is not called at all and the error is returned. The operator asked for evidence, so spending the workload's whole runtime only to report at the end that no profile exists would be fail-silent in the way that matters.
- A TEARDOWN failure is subordinate: once fn has run, its error takes precedence, so a problem writing a profile can never mask a workload failure.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session is an active profiling run. Finish must be called before the process exits, or the CPU profile is truncated and the trace is unreadable.
func (*Session) Finish ¶
Finish stops the CPU profile and the trace, writes the heap profile — and, when -contention was set, the mutex, block and goroutine profiles — restores the contention sampling rates, and reports the artefact paths to w as telemetry lines (prefixed with "# ", so an example's regression test ignores them).
The goroutine profile it writes is a POST-workload snapshot: it answers which goroutines the workload left behind, not how many ran at its peak.
It is idempotent: the profilers are stopped and the heap profile written on the first call only, and every later call returns that same result. This lets an error path and a success path both call it without coordinating.
Finish writes nothing and returns nil when the session is inert.