❯ go-clikit
v0.10.0 pkg.go.dev ↗

concepts › work and the meter

Work and the Meter

A span can report an amount of work, such as bytes, out of a size. The Bus samples it, and a sink rolls it up the tree.

A count of targets shows how far a step got. A download of 2 GiB is one target for a long time, so it needs another measure: an amount of work, in a unit, out of a size when the size is known (decision 23).

reinstall --progress=tty: the work of a step
reinstall ◦ 0:02.0
╎ fetching the base image  ◉◉◌◌◌◌ 0.8/2.0 GiB ◦ 39% ◦ 307 MiB/s  2.0s  
During the first five seconds, the step reports bytes. The tree shows the bar, the amount, the share, the rate and the time left. The frame at rest is the third.

Report work#

Give the unit and the size when you start the span, with progress.Work. Then move the amount:

The units are items, bytes and percent. A span in percent has a size of 100. A span that was given no Work counts items. Advance does nothing to a span that does not run, and nothing to a nil span.

now := time.Date(2026, 10, 10, 12, 0, 0, 0, time.UTC)
bus := progress.NewBus(progress.BusOptions{
	Sinks: []progress.Sink{worker{}},
	Now:   func() time.Time { return now },
})
ctx := progress.WithBus(context.Background(), bus)
_, call := progress.Start(ctx, progress.KindCall, "download", progress.HTTPRequest("GET", "/images/rocky-9.4.qcow2"))
call.Update(progress.Work(progress.UnitBytes, 2<<30))
for range 3 {
	now = now.Add(100 * time.Millisecond)
	call.Advance(8 << 20)
}
now = now.Add(50 * time.Millisecond)
call.Advance(8 << 20)
call.End(nil)
bus.Close()
start "download" 0/0 unit(0)
update "download" 0/2147483648 bytes
advance "download" 8388608/2147483648 bytes
advance "download" 16777216/2147483648 bytes
advance "download" 25165824/2147483648 bytes
end "download" 33554432/2147483648 bytes
ExampleSpan_Advance in progress/example_test.go. CI runs it and checks its output.

The Bus samples, nobody waits#

A copy can write a hundred thousand times a second. So Advance adds to an atomic amount and takes no lock. The Bus sends the amount as an event at most every 100 ms for each span, by its own clock, and always with the span’s end, so that the newest amount is never lost. An update of the span carries the newest amount too, so that work which learns its amount and its size sets the amount first, and a new size never meets an old amount. The event log writes an advance at most once a second by default, and counts the advances it leaves out in leftOut on the next advance it writes. It holds the newest one it would leave out, and writes it once its second has passed, so that a reader of the log sees where the work stopped while it pauses.

NOTE

Advance and SetAmount read the clock of the Bus, BusOptions.Now, on the goroutine that calls them, outside the lock of the Bus. A clock that you give the Bus must be safe for concurrent use.

The Meter rolls the work up#

An event carries only what its span said. progress.Meter is the half on the side of the sink: it is given every event in order, as a sink is, and keeps the work of each span, rolled up the tree. Each display keeps a Meter and reads it when it draws a frame. So the rows move between events.

1 BoundSizeThe span has a size.Share = amount / size.1.1/2.0 GiB · 56%2 BoundTargetsA step that folds, or a batchwith a Total: ended targetsand running shares / Total.3.5 of 6 targets3 BoundChildrenAny other span: the sum ofthe amounts below, over thesum of their sizes.(80 + 30) / 200 = 55%
Where the share done of a span comes from. The first rule that applies wins.

A progress.Reading also has the rate over the last five seconds, the time left once the span has run for two seconds, and how long the amount has not grown, once that is five seconds. A display shows this as “stalled”.

now := time.Date(2026, 10, 10, 12, 0, 0, 0, time.UTC)
capture := &progresstest.Capture{}
bus := progress.NewBus(progress.BusOptions{
	Sinks: []progress.Sink{capture},
	Now:   func() time.Time { return now },
})
ctx, _ := progress.Start(progress.WithBus(context.Background(), bus), progress.KindCommand, "deploy")

ctx, _ = progress.Start(ctx, progress.KindStep, "copying the image",
	progress.Flagged(progress.FlagFold), progress.Total(4))
targets := make([]*progress.Span, 4)
for i, node := range []string{"exe01", "exe02", "exe03", "exe04"} {
	_, targets[i] = progress.Start(ctx, progress.KindTarget, node, progress.Queued(),
		progress.Node(node), progress.Work(progress.UnitBytes, 1<<30))
}
for _, target := range targets[:2] {
	target.Run()
	target.Advance(1 << 30)
	target.End(nil)
}
targets[2].Run()
now = now.Add(5 * time.Second)
targets[2].Advance(1 << 29)

var meter progress.Meter
var counted progress.SpanID
for _, e := range capture.Events() {
	meter.Add(e)
	if e.Kind == progress.KindStep && e.Type == progress.TypeStart {
		counted = e.Span
	}
}
r, _ := meter.Read(counted, now)
fmt.Println(r.Bound, r.Fraction, r.Amount, r.Size)
bus.Close()
targets 0.625 2684354560 0
ExampleMeter in progress/example_test.go. CI runs it and checks its output.

What to use when#

You haveUse
A step over many targetsTotal and FlagFold; no work needed
A copy of bytes with a known sizeWork(UnitBytes, size) and AdvanceWriter
A tool that prints its own percentWork(UnitPercent, 100) and SetAmount
A loop over items of one targetWork(UnitItems, n) and Advance(1)