Skip to content

Commit 2daa2c3

Browse files
authored
Merge pull request #6876 from thaJeztah/stats_optimize
docker stats: assorted fixes and optimizations in rendering
2 parents d8a85fb + e7cbaaf commit 2daa2c3

File tree

3 files changed

+69
-48
lines changed

3 files changed

+69
-48
lines changed

cli/command/container/formatter_stats.go

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -124,24 +124,15 @@ func NewStats(idOrName string) *Stats {
124124

125125
// statsFormatWrite renders the context for a list of containers statistics
126126
func statsFormatWrite(ctx formatter.Context, stats []StatsEntry, osType string, trunc bool) error {
127-
render := func(format func(subContext formatter.SubContext) error) error {
128-
for _, cstats := range stats {
129-
statsCtx := &statsContext{
130-
s: cstats,
131-
os: osType,
132-
trunc: trunc,
133-
}
134-
if err := format(statsCtx); err != nil {
135-
return err
136-
}
137-
}
138-
return nil
139-
}
127+
// TODO(thaJeztah): this should be taken from the (first) StatsEntry instead.
128+
// also, assuming all stats are for the same platform (and basing the
129+
// column headers on that) won't allow aggregated results, which could
130+
// be mixed platform.
140131
memUsage := memUseHeader
141132
if osType == winOSType {
142133
memUsage = winMemUseHeader
143134
}
144-
statsCtx := statsContext{}
135+
statsCtx := statsContext{os: osType}
145136
statsCtx.Header = formatter.SubHeaderContext{
146137
"Container": containerHeader,
147138
"Name": formatter.NameHeader,
@@ -153,8 +144,18 @@ func statsFormatWrite(ctx formatter.Context, stats []StatsEntry, osType string,
153144
"BlockIO": blockIOHeader,
154145
"PIDs": pidsHeader,
155146
}
156-
statsCtx.os = osType
157-
return ctx.Write(&statsCtx, render)
147+
return ctx.Write(&statsCtx, func(format func(subContext formatter.SubContext) error) error {
148+
for _, cstats := range stats {
149+
if err := format(&statsContext{
150+
s: cstats,
151+
os: osType,
152+
trunc: trunc,
153+
}); err != nil {
154+
return err
155+
}
156+
}
157+
return nil
158+
})
158159
}
159160

160161
type statsContext struct {

cli/command/container/stats.go

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import (
77
"bytes"
88
"context"
99
"errors"
10-
"fmt"
1110
"io"
12-
"strings"
1311
"sync"
1412
"time"
1513

@@ -287,30 +285,32 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
287285
}
288286
}
289287

290-
// Buffer to store formatted stats text.
291-
// Once formatted, it will be printed in one write to avoid screen flickering.
292-
var statsTextBuffer bytes.Buffer
288+
// renderBuf holds the formatted stats output produced by statsFormatWrite.
289+
// It does not include any terminal control sequences.
290+
var renderBuf bytes.Buffer
291+
292+
// frameBuf holds the final terminal frame, including cursor movement and
293+
// line-clearing escape sequences, written in a single pass to avoid flicker.
294+
var frameBuf bytes.Buffer
293295

294296
statsCtx := formatter.Context{
295-
Output: &statsTextBuffer,
297+
Output: &renderBuf,
296298
Format: NewStatsFormat(format, daemonOSType),
297299
}
298300

299301
if options.NoStream {
300-
cStats.mu.RLock()
301-
ccStats := make([]StatsEntry, 0, len(cStats.cs))
302-
for _, c := range cStats.cs {
303-
ccStats = append(ccStats, c.GetStatistics())
304-
}
305-
cStats.mu.RUnlock()
306-
307-
if len(ccStats) == 0 {
302+
statsList := cStats.snapshot()
303+
if len(statsList) == 0 {
308304
return nil
309305
}
306+
ccStats := make([]StatsEntry, 0, len(statsList))
307+
for _, c := range statsList {
308+
ccStats = append(ccStats, c.GetStatistics())
309+
}
310310
if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
311311
return err
312312
}
313-
_, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String())
313+
_, _ = dockerCLI.Out().Write(renderBuf.Bytes())
314314
return nil
315315
}
316316

@@ -319,34 +319,38 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
319319
for {
320320
select {
321321
case <-ticker.C:
322-
cStats.mu.RLock()
323-
ccStats := make([]StatsEntry, 0, len(cStats.cs))
324-
for _, c := range cStats.cs {
322+
renderBuf.Reset()
323+
frameBuf.Reset()
324+
statsList := cStats.snapshot()
325+
if len(statsList) == 0 && !showAll {
326+
// Clear screen
327+
_, _ = io.WriteString(dockerCLI.Out(), "\033[H\033[J")
328+
return nil
329+
}
330+
ccStats := make([]StatsEntry, 0, len(statsList))
331+
for _, c := range statsList {
325332
ccStats = append(ccStats, c.GetStatistics())
326333
}
327-
cStats.mu.RUnlock()
328-
329-
// Start by moving the cursor to the top-left
330-
_, _ = fmt.Fprint(&statsTextBuffer, "\033[H")
331334

332335
if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
333336
return err
334337
}
335338

336-
for line := range strings.SplitSeq(statsTextBuffer.String(), "\n") {
339+
// Start by moving the cursor to the top-left
340+
_, _ = io.WriteString(&frameBuf, "\033[H")
341+
342+
// TODO(thaJeztah): consider wrapping the writer to inject ANSI (line-clearing) during formatting.
343+
// instead of post-processing the results.
344+
for line := range bytes.SplitSeq(renderBuf.Bytes(), []byte{'\n'}) {
337345
// In case the new text is shorter than the one we are writing over,
338346
// we'll append the "erase line" escape sequence to clear the remaining text.
339-
_, _ = fmt.Fprintln(&statsTextBuffer, line, "\033[K")
347+
_, _ = frameBuf.Write(line)
348+
_, _ = io.WriteString(&frameBuf, "\033[K")
349+
_ = frameBuf.WriteByte('\n')
340350
}
341351
// We might have fewer containers than before, so let's clear the remaining text
342-
_, _ = fmt.Fprint(&statsTextBuffer, "\033[J")
343-
344-
_, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String())
345-
statsTextBuffer.Reset()
346-
347-
if len(ccStats) == 0 && !showAll {
348-
return nil
349-
}
352+
_, _ = io.WriteString(&frameBuf, "\033[J")
353+
_, _ = dockerCLI.Out().Write(frameBuf.Bytes())
350354
case err, ok := <-closeChan:
351355
if !ok || err == nil || errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
352356
// Suppress "unexpected EOF" errors in the CLI so that

cli/command/container/stats_helpers.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,22 @@ func (s *stats) isKnownContainer(cid string) (int, bool) {
4949
return -1, false
5050
}
5151

52+
// snapshot returns a point-in-time copy of the tracked container list
53+
// (the slice of *Stats pointers). The returned slice is safe for use
54+
// without holding the stats lock, but the underlying Stats values may
55+
// continue to change concurrently.
56+
func (s *stats) snapshot() []*Stats {
57+
s.mu.RLock()
58+
defer s.mu.RUnlock()
59+
if len(s.cs) == 0 {
60+
return nil
61+
}
62+
// https://github.com/golang/go/issues/53643
63+
cp := make([]*Stats, len(s.cs))
64+
copy(cp, s.cs)
65+
return cp
66+
}
67+
5268
func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, streamStats bool, waitFirst *sync.WaitGroup) { //nolint:gocyclo
5369
var getFirst bool
5470

0 commit comments

Comments
 (0)