فهرست منبع

Hide the mutex in formatter.ContainerStats

The formatter.ContainerStats struct exposes its Mutex.
This is a bad design and should be fixed.

To fix that, I separated the statistics
attributes from ContainerStats to StatsEntry and
hid the mutex. Notice that the mutex protects both
the `err` field and the statistics attributes.

Then, implemented SetStatistics, SetError, GetStatistics
and GetError to avoid races.

Moreover, to make this less granular, I decided to
replace the read-write mutex with the regular mutex and
to pass a StatsEntry slice to formatter.ContainerStatsWrite

Signed-off-by: Boaz Shuster <ripcurld.github@gmail.com>
Boaz Shuster 8 سال پیش
والد
کامیت
929a77b814
3فایلهای تغییر یافته به همراه132 افزوده شده و 81 حذف شده
  1. 13 11
      cli/command/container/stats.go
  2. 23 34
      cli/command/container/stats_helpers.go
  3. 96 36
      cli/command/formatter/stats.go

+ 13 - 11
cli/command/container/stats.go

@@ -166,11 +166,10 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
 		var errs []string
 		var errs []string
 		cStats.mu.Lock()
 		cStats.mu.Lock()
 		for _, c := range cStats.cs {
 		for _, c := range cStats.cs {
-			c.Mu.Lock()
-			if c.Err != nil {
-				errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.Err))
+			cErr := c.GetError()
+			if cErr != nil {
+				errs = append(errs, fmt.Sprintf("%s: %v", c.Name, cErr))
 			}
 			}
-			c.Mu.Unlock()
 		}
 		}
 		cStats.mu.Unlock()
 		cStats.mu.Unlock()
 		if len(errs) > 0 {
 		if len(errs) > 0 {
@@ -189,7 +188,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
 		Format: formatter.NewStatsFormat(f, daemonOSType),
 		Format: formatter.NewStatsFormat(f, daemonOSType),
 	}
 	}
 
 
-	cleanHeader := func() {
+	cleanScreen := func() {
 		if !opts.noStream {
 		if !opts.noStream {
 			fmt.Fprint(dockerCli.Out(), "\033[2J")
 			fmt.Fprint(dockerCli.Out(), "\033[2J")
 			fmt.Fprint(dockerCli.Out(), "\033[H")
 			fmt.Fprint(dockerCli.Out(), "\033[H")
@@ -198,14 +197,17 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
 
 
 	var err error
 	var err error
 	for range time.Tick(500 * time.Millisecond) {
 	for range time.Tick(500 * time.Millisecond) {
-		cleanHeader()
-		cStats.mu.RLock()
-		csLen := len(cStats.cs)
-		if err = formatter.ContainerStatsWrite(statsCtx, cStats.cs); err != nil {
+		cleanScreen()
+		ccstats := []formatter.StatsEntry{}
+		cStats.mu.Lock()
+		for _, c := range cStats.cs {
+			ccstats = append(ccstats, c.GetStatistics())
+		}
+		cStats.mu.Unlock()
+		if err = formatter.ContainerStatsWrite(statsCtx, ccstats); err != nil {
 			break
 			break
 		}
 		}
-		cStats.mu.RUnlock()
-		if csLen == 0 && !showAll {
+		if len(cStats.cs) == 0 && !showAll {
 			break
 			break
 		}
 		}
 		if opts.noStream {
 		if opts.noStream {

+ 23 - 34
cli/command/container/stats_helpers.go

@@ -17,7 +17,7 @@ import (
 
 
 type stats struct {
 type stats struct {
 	ostype string
 	ostype string
-	mu     sync.RWMutex
+	mu     sync.Mutex
 	cs     []*formatter.ContainerStats
 	cs     []*formatter.ContainerStats
 }
 }
 
 
@@ -72,9 +72,7 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli
 
 
 	response, err := cli.ContainerStats(ctx, s.Name, streamStats)
 	response, err := cli.ContainerStats(ctx, s.Name, streamStats)
 	if err != nil {
 	if err != nil {
-		s.Mu.Lock()
-		s.Err = err
-		s.Mu.Unlock()
+		s.SetError(err)
 		return
 		return
 	}
 	}
 	defer response.Body.Close()
 	defer response.Body.Close()
@@ -88,6 +86,9 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli
 				cpuPercent        = 0.0
 				cpuPercent        = 0.0
 				blkRead, blkWrite uint64 // Only used on Linux
 				blkRead, blkWrite uint64 // Only used on Linux
 				mem               = 0.0
 				mem               = 0.0
+				memLimit          = 0.0
+				memPerc           = 0.0
+				pidsStatsCurrent  uint64
 			)
 			)
 
 
 			if err := dec.Decode(&v); err != nil {
 			if err := dec.Decode(&v); err != nil {
@@ -113,26 +114,27 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli
 				cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v)
 				cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v)
 				blkRead, blkWrite = calculateBlockIO(v.BlkioStats)
 				blkRead, blkWrite = calculateBlockIO(v.BlkioStats)
 				mem = float64(v.MemoryStats.Usage)
 				mem = float64(v.MemoryStats.Usage)
-
+				memLimit = float64(v.MemoryStats.Limit)
+				memPerc = memPercent
+				pidsStatsCurrent = v.PidsStats.Current
 			} else {
 			} else {
 				cpuPercent = calculateCPUPercentWindows(v)
 				cpuPercent = calculateCPUPercentWindows(v)
 				blkRead = v.StorageStats.ReadSizeBytes
 				blkRead = v.StorageStats.ReadSizeBytes
 				blkWrite = v.StorageStats.WriteSizeBytes
 				blkWrite = v.StorageStats.WriteSizeBytes
 				mem = float64(v.MemoryStats.PrivateWorkingSet)
 				mem = float64(v.MemoryStats.PrivateWorkingSet)
 			}
 			}
-
-			s.Mu.Lock()
-			s.CPUPercentage = cpuPercent
-			s.Memory = mem
-			s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks)
-			s.BlockRead = float64(blkRead)
-			s.BlockWrite = float64(blkWrite)
-			if daemonOSType != "windows" {
-				s.MemoryLimit = float64(v.MemoryStats.Limit)
-				s.MemoryPercentage = memPercent
-				s.PidsCurrent = v.PidsStats.Current
-			}
-			s.Mu.Unlock()
+			netRx, netTx := calculateNetwork(v.Networks)
+			s.SetStatistics(formatter.StatsEntry{
+				CPUPercentage:    cpuPercent,
+				Memory:           mem,
+				MemoryPercentage: memPerc,
+				MemoryLimit:      memLimit,
+				NetworkRx:        netRx,
+				NetworkTx:        netTx,
+				BlockRead:        float64(blkRead),
+				BlockWrite:       float64(blkWrite),
+				PidsCurrent:      pidsStatsCurrent,
+			})
 			u <- nil
 			u <- nil
 			if !streamStats {
 			if !streamStats {
 				return
 				return
@@ -144,18 +146,7 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli
 		case <-time.After(2 * time.Second):
 		case <-time.After(2 * time.Second):
 			// zero out the values if we have not received an update within
 			// zero out the values if we have not received an update within
 			// the specified duration.
 			// the specified duration.
-			s.Mu.Lock()
-			s.CPUPercentage = 0
-			s.Memory = 0
-			s.MemoryPercentage = 0
-			s.MemoryLimit = 0
-			s.NetworkRx = 0
-			s.NetworkTx = 0
-			s.BlockRead = 0
-			s.BlockWrite = 0
-			s.PidsCurrent = 0
-			s.Err = errors.New("timeout waiting for stats")
-			s.Mu.Unlock()
+			s.SetErrorAndReset(errors.New("timeout waiting for stats"))
 			// if this is the first stat you get, release WaitGroup
 			// if this is the first stat you get, release WaitGroup
 			if !getFirst {
 			if !getFirst {
 				getFirst = true
 				getFirst = true
@@ -163,12 +154,10 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli
 			}
 			}
 		case err := <-u:
 		case err := <-u:
 			if err != nil {
 			if err != nil {
-				s.Mu.Lock()
-				s.Err = err
-				s.Mu.Unlock()
+				s.SetError(err)
 				continue
 				continue
 			}
 			}
-			s.Err = nil
+			s.SetError(nil)
 			// if this is the first stat you get, release WaitGroup
 			// if this is the first stat you get, release WaitGroup
 			if !getFirst {
 			if !getFirst {
 				getFirst = true
 				getFirst = true

+ 96 - 36
cli/command/formatter/stats.go

@@ -4,27 +4,27 @@ import (
 	"fmt"
 	"fmt"
 	"sync"
 	"sync"
 
 
-	"github.com/docker/go-units"
+	units "src/github.com/docker/go-units"
 )
 )
 
 
 const (
 const (
-	defaultStatsTableFormat    = "table {{.Container}}\t{{.CPUPrec}}\t{{.MemUsage}}\t{{.MemPrec}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
-	winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPrec}}\t{{{.MemUsage}}\t{.NetIO}}\t{{.BlockIO}}"
+	winOSType                  = "windows"
+	defaultStatsTableFormat    = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
+	winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{{.MemUsage}}\t{.NetIO}}\t{{.BlockIO}}"
 	emptyStatsTableFormat      = "Waiting for statistics..."
 	emptyStatsTableFormat      = "Waiting for statistics..."
 
 
 	containerHeader  = "CONTAINER"
 	containerHeader  = "CONTAINER"
-	cpuPrecHeader    = "CPU %"
+	cpuPercHeader    = "CPU %"
 	netIOHeader      = "NET I/O"
 	netIOHeader      = "NET I/O"
 	blockIOHeader    = "BLOCK I/O"
 	blockIOHeader    = "BLOCK I/O"
-	winMemPrecHeader = "PRIV WORKING SET"  // Used only on Window
-	memPrecHeader    = "MEM %"             // Used only on Linux
+	winMemPercHeader = "PRIV WORKING SET"  // Used only on Window
+	memPercHeader    = "MEM %"             // Used only on Linux
 	memUseHeader     = "MEM USAGE / LIMIT" // Used only on Linux
 	memUseHeader     = "MEM USAGE / LIMIT" // Used only on Linux
 	pidsHeader       = "PIDS"              // Used only on Linux
 	pidsHeader       = "PIDS"              // Used only on Linux
 )
 )
 
 
-// ContainerStatsAttrs represents the statistics data collected from a container.
-type ContainerStatsAttrs struct {
-	Windows          bool
+// StatsEntry represents represents the statistics data collected from a container
+type StatsEntry struct {
 	Name             string
 	Name             string
 	CPUPercentage    float64
 	CPUPercentage    float64
 	Memory           float64 // On Windows this is the private working set
 	Memory           float64 // On Windows this is the private working set
@@ -35,19 +35,73 @@ type ContainerStatsAttrs struct {
 	BlockRead        float64
 	BlockRead        float64
 	BlockWrite       float64
 	BlockWrite       float64
 	PidsCurrent      uint64 // Not used on Windows
 	PidsCurrent      uint64 // Not used on Windows
+	IsInvalid        bool
+	OSType           string
 }
 }
 
 
-// ContainerStats represents the containers statistics data.
+// ContainerStats represents an entity to store containers statistics synchronously
 type ContainerStats struct {
 type ContainerStats struct {
-	Mu sync.RWMutex
-	ContainerStatsAttrs
-	Err error
+	mutex sync.Mutex
+	StatsEntry
+	err error
+}
+
+// GetError returns the container statistics error.
+// This is used to determine whether the statistics are valid or not
+func (cs *ContainerStats) GetError() error {
+	cs.mutex.Lock()
+	defer cs.mutex.Unlock()
+	return cs.err
+}
+
+// SetErrorAndReset zeroes all the container statistics and store the error.
+// It is used when receiving time out error during statistics collecting to reduce lock overhead
+func (cs *ContainerStats) SetErrorAndReset(err error) {
+	cs.mutex.Lock()
+	defer cs.mutex.Unlock()
+	cs.CPUPercentage = 0
+	cs.Memory = 0
+	cs.MemoryPercentage = 0
+	cs.MemoryLimit = 0
+	cs.NetworkRx = 0
+	cs.NetworkTx = 0
+	cs.BlockRead = 0
+	cs.BlockWrite = 0
+	cs.PidsCurrent = 0
+	cs.err = err
+	cs.IsInvalid = true
+}
+
+// SetError sets container statistics error
+func (cs *ContainerStats) SetError(err error) {
+	cs.mutex.Lock()
+	defer cs.mutex.Unlock()
+	cs.err = err
+	if err != nil {
+		cs.IsInvalid = true
+	}
+}
+
+// SetStatistics set the container statistics
+func (cs *ContainerStats) SetStatistics(s StatsEntry) {
+	cs.mutex.Lock()
+	defer cs.mutex.Unlock()
+	s.Name = cs.Name
+	s.OSType = cs.OSType
+	cs.StatsEntry = s
+}
+
+// GetStatistics returns container statistics with other meta data such as the container name
+func (cs *ContainerStats) GetStatistics() StatsEntry {
+	cs.mutex.Lock()
+	defer cs.mutex.Unlock()
+	return cs.StatsEntry
 }
 }
 
 
 // NewStatsFormat returns a format for rendering an CStatsContext
 // NewStatsFormat returns a format for rendering an CStatsContext
 func NewStatsFormat(source, osType string) Format {
 func NewStatsFormat(source, osType string) Format {
 	if source == TableFormatKey {
 	if source == TableFormatKey {
-		if osType == "windows" {
+		if osType == winOSType {
 			return Format(winDefaultStatsTableFormat)
 			return Format(winDefaultStatsTableFormat)
 		}
 		}
 		return Format(defaultStatsTableFormat)
 		return Format(defaultStatsTableFormat)
@@ -58,22 +112,16 @@ func NewStatsFormat(source, osType string) Format {
 // NewContainerStats returns a new ContainerStats entity and sets in it the given name
 // NewContainerStats returns a new ContainerStats entity and sets in it the given name
 func NewContainerStats(name, osType string) *ContainerStats {
 func NewContainerStats(name, osType string) *ContainerStats {
 	return &ContainerStats{
 	return &ContainerStats{
-		ContainerStatsAttrs: ContainerStatsAttrs{
-			Name:    name,
-			Windows: (osType == "windows"),
-		},
+		StatsEntry: StatsEntry{Name: name, OSType: osType},
 	}
 	}
 }
 }
 
 
 // ContainerStatsWrite renders the context for a list of containers statistics
 // ContainerStatsWrite renders the context for a list of containers statistics
-func ContainerStatsWrite(ctx Context, containerStats []*ContainerStats) error {
+func ContainerStatsWrite(ctx Context, containerStats []StatsEntry) error {
 	render := func(format func(subContext subContext) error) error {
 	render := func(format func(subContext subContext) error) error {
 		for _, cstats := range containerStats {
 		for _, cstats := range containerStats {
-			cstats.Mu.RLock()
-			cstatsAttrs := cstats.ContainerStatsAttrs
-			cstats.Mu.RUnlock()
 			containerStatsCtx := &containerStatsContext{
 			containerStatsCtx := &containerStatsContext{
-				s: cstatsAttrs,
+				s: cstats,
 			}
 			}
 			if err := format(containerStatsCtx); err != nil {
 			if err := format(containerStatsCtx); err != nil {
 				return err
 				return err
@@ -86,7 +134,7 @@ func ContainerStatsWrite(ctx Context, containerStats []*ContainerStats) error {
 
 
 type containerStatsContext struct {
 type containerStatsContext struct {
 	HeaderContext
 	HeaderContext
-	s ContainerStatsAttrs
+	s StatsEntry
 }
 }
 
 
 func (c *containerStatsContext) Container() string {
 func (c *containerStatsContext) Container() string {
@@ -94,42 +142,54 @@ func (c *containerStatsContext) Container() string {
 	return c.s.Name
 	return c.s.Name
 }
 }
 
 
-func (c *containerStatsContext) CPUPrec() string {
-	c.AddHeader(cpuPrecHeader)
+func (c *containerStatsContext) CPUPerc() string {
+	c.AddHeader(cpuPercHeader)
+	if c.s.IsInvalid {
+		return fmt.Sprintf("--")
+	}
 	return fmt.Sprintf("%.2f%%", c.s.CPUPercentage)
 	return fmt.Sprintf("%.2f%%", c.s.CPUPercentage)
 }
 }
 
 
 func (c *containerStatsContext) MemUsage() string {
 func (c *containerStatsContext) MemUsage() string {
 	c.AddHeader(memUseHeader)
 	c.AddHeader(memUseHeader)
-	if !c.s.Windows {
-		return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit))
+	if c.s.IsInvalid || c.s.OSType == winOSType {
+		return fmt.Sprintf("-- / --")
 	}
 	}
-	return fmt.Sprintf("-- / --")
+	return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit))
 }
 }
 
 
-func (c *containerStatsContext) MemPrec() string {
-	header := memPrecHeader
-	if c.s.Windows {
-		header = winMemPrecHeader
+func (c *containerStatsContext) MemPerc() string {
+	header := memPercHeader
+	if c.s.OSType == winOSType {
+		header = winMemPercHeader
 	}
 	}
 	c.AddHeader(header)
 	c.AddHeader(header)
+	if c.s.IsInvalid {
+		return fmt.Sprintf("--")
+	}
 	return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage)
 	return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage)
 }
 }
 
 
 func (c *containerStatsContext) NetIO() string {
 func (c *containerStatsContext) NetIO() string {
 	c.AddHeader(netIOHeader)
 	c.AddHeader(netIOHeader)
+	if c.s.IsInvalid {
+		return fmt.Sprintf("--")
+	}
 	return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3))
 	return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3))
 }
 }
 
 
 func (c *containerStatsContext) BlockIO() string {
 func (c *containerStatsContext) BlockIO() string {
 	c.AddHeader(blockIOHeader)
 	c.AddHeader(blockIOHeader)
+	if c.s.IsInvalid {
+		return fmt.Sprintf("--")
+	}
 	return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3))
 	return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3))
 }
 }
 
 
 func (c *containerStatsContext) PIDs() string {
 func (c *containerStatsContext) PIDs() string {
 	c.AddHeader(pidsHeader)
 	c.AddHeader(pidsHeader)
-	if !c.s.Windows {
-		return fmt.Sprintf("%d", c.s.PidsCurrent)
+	if c.s.IsInvalid || c.s.OSType == winOSType {
+		return fmt.Sprintf("--")
 	}
 	}
-	return fmt.Sprintf("-")
+	return fmt.Sprintf("%d", c.s.PidsCurrent)
 }
 }