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 ◦ 0:02.0 ╎ fetching the base image ◉◉◌◌◌◌ 0.8/2.0 GiB ◦ 39% ◦ 307 MiB/s 2.0s
Report work#
Give the unit and the size when you start the span, with
progress.Work. Then move the amount:
progress.Span.Advanceadds to the amount.progress.Span.SetAmountsets it.progress.AdvanceWriterandprogress.AdvanceReaderwrap a writer or a reader, and advance the span of the context by each byte that goes through.
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 bytesprogress/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.
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.
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 0progress/example_test.go. CI runs it and checks its output.What to use when#
| You have | Use |
|---|---|
| A step over many targets | Total and FlagFold; no work needed |
| A copy of bytes with a known size | Work(UnitBytes, size) and AdvanceWriter |
| A tool that prints its own percent | Work(UnitPercent, 100) and SetAmount |
| A loop over items of one target | Work(UnitItems, n) and Advance(1) |