From 7bcc03d97219f07f96ce84888dec0b50a4e9e0a9 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 28 Mar 2022 11:00:29 +0200 Subject: [PATCH 1/3] cli/command/container: add BenchmarkStatsFormat() To test: GO111MODULE=off go test -test.v -test.bench '^BenchmarkStatsFormat' -test.run '^$' ./cli/command/container/ goos: darwin goarch: amd64 pkg: github.com/docker/cli/cli/command/container cpu: Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz BenchmarkStatsFormat BenchmarkStatsFormat-8 2482 522721 ns/op 62439 B/op 5600 allocs/op PASS ok github.com/docker/cli/cli/command/container 1.369s Signed-off-by: Sebastiaan van Stijn --- cli/command/container/formatter_stats_test.go | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/cli/command/container/formatter_stats_test.go b/cli/command/container/formatter_stats_test.go index 844679654b..ad395e6f74 100644 --- a/cli/command/container/formatter_stats_test.go +++ b/cli/command/container/formatter_stats_test.go @@ -308,3 +308,38 @@ func TestContainerStatsContextWriteTrunc(t *testing.T) { out.Reset() } } + +func BenchmarkStatsFormat(b *testing.B) { + b.ReportAllocs() + stats := genStats() + + for i := 0; i < b.N; i++ { + for _, s := range stats { + _ = s.CPUPerc() + _ = s.MemUsage() + _ = s.MemPerc() + _ = s.NetIO() + _ = s.BlockIO() + _ = s.PIDs() + } + } +} + +func genStats() []statsContext { + entry := statsContext{s: StatsEntry{ + CPUPercentage: 12.3456789, + Memory: 123.456789, + MemoryLimit: 987.654321, + MemoryPercentage: 12.3456789, + BlockRead: 123.456789, + BlockWrite: 987.654321, + NetworkRx: 123.456789, + NetworkTx: 987.654321, + PidsCurrent: 123456789, + }} + stats := make([]statsContext, 100) + for i := 0; i < 100; i++ { + stats = append(stats, entry) + } + return stats +} From 34dd43bf1ba890add8b1de191f886dd443d783b6 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 28 Mar 2022 11:26:39 +0200 Subject: [PATCH 2/3] cli/command/container: some small performance optimizations for formatting stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Formatting stats runs in a loop to refresh the stats for each container. This patch makes some small performance improvments by reducing the use of Sprintf in favor of concatenating strings, and using strconv directly where possible. Benchmark can be run with: GO111MODULE=off go test -test.v -test.bench '^BenchmarkStatsFormat' -test.run '^$' ./cli/command/container/ Before/after: BenchmarkStatsFormatOld-8 2655 428064 ns/op 62432 B/op 5600 allocs/op BenchmarkStatsFormat-8 3338 335822 ns/op 52832 B/op 4700 allocs/op Average of 5 runs; benchstat old.txt new.txt name old time/op new time/op delta StatsFormat-8 432µs ± 1% 344µs ± 5% -20.42% (p=0.008 n=5+5) name old alloc/op new alloc/op delta StatsFormat-8 62.4kB ± 0% 52.8kB ± 0% -15.38% (p=0.000 n=5+4) name old allocs/op new allocs/op delta StatsFormat-8 5.60k ± 0% 4.70k ± 0% -16.07% (p=0.008 n=5+5) Signed-off-by: Sebastiaan van Stijn --- cli/command/container/formatter_stats.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cli/command/container/formatter_stats.go b/cli/command/container/formatter_stats.go index 4909405cf3..1b5055ea17 100644 --- a/cli/command/container/formatter_stats.go +++ b/cli/command/container/formatter_stats.go @@ -1,7 +1,7 @@ package container import ( - "fmt" + "strconv" "sync" "github.com/docker/cli/cli/command/formatter" @@ -183,7 +183,7 @@ func (c *statsContext) CPUPerc() string { if c.s.IsInvalid { return "--" } - return fmt.Sprintf("%.2f%%", c.s.CPUPercentage) + return formatPercentage(c.s.CPUPercentage) } func (c *statsContext) MemUsage() string { @@ -193,33 +193,37 @@ func (c *statsContext) MemUsage() string { if c.os == winOSType { return units.BytesSize(c.s.Memory) } - return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit)) + return units.BytesSize(c.s.Memory) + " / " + units.BytesSize(c.s.MemoryLimit) } func (c *statsContext) MemPerc() string { if c.s.IsInvalid || c.os == winOSType { return "--" } - return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage) + return formatPercentage(c.s.MemoryPercentage) } func (c *statsContext) NetIO() string { if c.s.IsInvalid { return "--" } - return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3)) + return units.HumanSizeWithPrecision(c.s.NetworkRx, 3) + " / " + units.HumanSizeWithPrecision(c.s.NetworkTx, 3) } func (c *statsContext) BlockIO() string { if c.s.IsInvalid { return "--" } - return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3)) + return units.HumanSizeWithPrecision(c.s.BlockRead, 3) + " / " + units.HumanSizeWithPrecision(c.s.BlockWrite, 3) } func (c *statsContext) PIDs() string { if c.s.IsInvalid || c.os == winOSType { return "--" } - return fmt.Sprintf("%d", c.s.PidsCurrent) + return strconv.FormatUint(c.s.PidsCurrent, 10) +} + +func formatPercentage(val float64) string { + return strconv.FormatFloat(val, 'f', 2, 64) + "%" } From a2e9ed3b874fccc177b9349f3b0277612403934f Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 28 Mar 2022 11:29:06 +0200 Subject: [PATCH 3/3] cli/command/container: use RWMutex for stats to allow concurrent reads Signed-off-by: Sebastiaan van Stijn --- cli/command/container/formatter_stats.go | 10 +++++----- cli/command/container/stats.go | 8 ++++---- cli/command/container/stats_helpers.go | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli/command/container/formatter_stats.go b/cli/command/container/formatter_stats.go index 1b5055ea17..c079c913ff 100644 --- a/cli/command/container/formatter_stats.go +++ b/cli/command/container/formatter_stats.go @@ -43,7 +43,7 @@ type StatsEntry struct { // Stats represents an entity to store containers statistics synchronously type Stats struct { - mutex sync.Mutex + mutex sync.RWMutex StatsEntry err error } @@ -51,8 +51,8 @@ type Stats struct { // GetError returns the container statistics error. // This is used to determine whether the statistics are valid or not func (cs *Stats) GetError() error { - cs.mutex.Lock() - defer cs.mutex.Unlock() + cs.mutex.RLock() + defer cs.mutex.RUnlock() return cs.err } @@ -94,8 +94,8 @@ func (cs *Stats) SetStatistics(s StatsEntry) { // GetStatistics returns container statistics with other meta data such as the container name func (cs *Stats) GetStatistics() StatsEntry { - cs.mutex.Lock() - defer cs.mutex.Unlock() + cs.mutex.RLock() + defer cs.mutex.RUnlock() return cs.StatsEntry } diff --git a/cli/command/container/stats.go b/cli/command/container/stats.go index 38d89450c4..0e938f3fad 100644 --- a/cli/command/container/stats.go +++ b/cli/command/container/stats.go @@ -184,13 +184,13 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error { waitFirst.Wait() var errs []string - cStats.mu.Lock() + cStats.mu.RLock() for _, c := range cStats.cs { if err := c.GetError(); err != nil { errs = append(errs, err.Error()) } } - cStats.mu.Unlock() + cStats.mu.RUnlock() if len(errs) > 0 { return errors.New(strings.Join(errs, "\n")) } @@ -221,11 +221,11 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error { for range ticker.C { cleanScreen() ccstats := []StatsEntry{} - cStats.mu.Lock() + cStats.mu.RLock() for _, c := range cStats.cs { ccstats = append(ccstats, c.GetStatistics()) } - cStats.mu.Unlock() + cStats.mu.RUnlock() if err = statsFormatWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil { break } diff --git a/cli/command/container/stats_helpers.go b/cli/command/container/stats_helpers.go index 299bd26baf..5ea7f665dc 100644 --- a/cli/command/container/stats_helpers.go +++ b/cli/command/container/stats_helpers.go @@ -14,7 +14,7 @@ import ( ) type stats struct { - mu sync.Mutex + mu sync.RWMutex cs []*Stats }