Skip to content

Commit

Permalink
Merge pull request #15769 from dfr/freebsd-stats
Browse files Browse the repository at this point in the history
Add support for 'podman stats' on FreeBSD
  • Loading branch information
openshift-merge-robot authored Sep 14, 2022
2 parents ae20f19 + b47ce9a commit 82651e5
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 38 deletions.
49 changes: 49 additions & 0 deletions libpod/stats_common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//go:build linux || freebsd
// +build linux freebsd

package libpod

import (
"fmt"

"github.com/containers/podman/v4/libpod/define"
)

// GetContainerStats gets the running stats for a given container.
// The previousStats is used to correctly calculate cpu percentages. You
// should pass nil if there is no previous stat for this container.
func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*define.ContainerStats, error) {
stats := new(define.ContainerStats)
stats.ContainerID = c.ID()
stats.Name = c.Name()

if c.config.NoCgroups {
return nil, fmt.Errorf("cannot run top on container %s as it did not create a cgroup: %w", c.ID(), define.ErrNoCgroups)
}

if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.syncContainer(); err != nil {
return stats, err
}
}

// returns stats with the fields' default values respective of their type
if c.state.State != define.ContainerStateRunning && c.state.State != define.ContainerStatePaused {
return stats, nil
}

if previousStats == nil {
previousStats = &define.ContainerStats{
// if we have no prev stats use the container start time as prev time
// otherwise we cannot correctly calculate the CPU percentage
SystemNano: uint64(c.state.StartedTime.UnixNano()),
}
}

if err := c.getPlatformContainerStats(stats, previousStats); err != nil {
return nil, err
}
return stats, nil
}
153 changes: 153 additions & 0 deletions libpod/stats_freebsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package libpod

import (
"fmt"
"math"
"strings"
"time"

"github.com/containers/common/pkg/cgroups"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/rctl"
"github.com/containers/storage/pkg/system"
"github.com/sirupsen/logrus"
)

// getPlatformContainerStats gets the platform-specific running stats
// for a given container. The previousStats is used to correctly
// calculate cpu percentages. You should pass nil if there is no
// previous stat for this container.
func (c *Container) getPlatformContainerStats(stats *define.ContainerStats, previousStats *define.ContainerStats) error {
now := uint64(time.Now().UnixNano())

jailName := c.ID()
if c.state.NetNS != nil {
jailName = c.state.NetNS.Name + "." + jailName
}
entries, err := rctl.GetRacct("jail:" + jailName)
if err != nil {
return fmt.Errorf("unable to read accounting for %s: %w", jailName, err)
}

// If the current total usage is less than what was previously
// recorded then it means the container was restarted and runs
// in a new jail
if dur, ok := entries["wallclock"]; ok {
if previousStats.Duration > dur*1000000000 {
previousStats = &define.ContainerStats{}
}
}

for key, val := range entries {
switch key {
case "cputime": // CPU time, in seconds
stats.CPUNano = val * 1000000000
stats.AvgCPU = calculateCPUPercent(stats.CPUNano, 0, now, uint64(c.state.StartedTime.UnixNano()))
case "datasize": // data size, in bytes
case "stacksize": // stack size, in bytes
case "coredumpsize": // core dump size, in bytes
case "memoryuse": // resident set size, in bytes
stats.MemUsage = val
case "memorylocked": // locked memory, in bytes
case "maxproc": // number of processes
stats.PIDs = val
case "openfiles": // file descriptor table size
case "vmemoryuse": // address space limit, in bytes
case "pseudoterminals": // number of PTYs
case "swapuse": // swap space that may be reserved or used, in bytes
case "nthr": // number of threads
case "msgqqueued": // number of queued SysV messages
case "msgqsize": // SysV message queue size, in bytes
case "nmsgq": // number of SysV message queues
case "nsem": // number of SysV semaphores
case "nsemop": // number of SysV semaphores modified in a single semop(2) call
case "nshm": // number of SysV shared memory segments
case "shmsize": // SysV shared memory size, in bytes
case "wallclock": // wallclock time, in seconds
stats.Duration = val * 1000000000
stats.UpTime = time.Duration(stats.Duration)
case "pcpu": // %CPU, in percents of a single CPU core
stats.CPU = float64(val)
case "readbps": // filesystem reads, in bytes per second
stats.BlockInput = val
case "writebps": // filesystem writes, in bytes per second
stats.BlockOutput = val
case "readiops": // filesystem reads, in operations per second
case "writeiops": // filesystem writes, in operations per second
}
}
stats.MemLimit = c.getMemLimit()
stats.SystemNano = now

netStats, err := getContainerNetIO(c)
if err != nil {
return err
}

// Handle case where the container is not in a network namespace
if netStats != nil {
stats.NetInput = netStats.TxBytes
stats.NetOutput = netStats.RxBytes
} else {
stats.NetInput = 0
stats.NetOutput = 0
}

return nil
}

// getMemory limit returns the memory limit for a container
func (c *Container) getMemLimit() uint64 {
memLimit := uint64(math.MaxUint64)

if c.config.Spec.Linux != nil && c.config.Spec.Linux.Resources != nil &&
c.config.Spec.Linux.Resources.Memory != nil && c.config.Spec.Linux.Resources.Memory.Limit != nil {
memLimit = uint64(*c.config.Spec.Linux.Resources.Memory.Limit)
}

mi, err := system.ReadMemInfo()
if err != nil {
logrus.Errorf("ReadMemInfo error: %v", err)
return 0
}

//nolint:unconvert
physicalLimit := uint64(mi.MemTotal)

if memLimit <= 0 || memLimit > physicalLimit {
return physicalLimit
}

return memLimit
}

// calculateCPUPercent calculates the cpu usage using the latest measurement in stats.
// previousCPU is the last value of stats.CPU.Usage.Total measured at the time previousSystem.
//
// (now - previousSystem) is the time delta in nanoseconds, between the measurement in previousCPU
//
// and the updated value in stats.
func calculateCPUPercent(currentCPU, previousCPU, now, previousSystem uint64) float64 {
var (
cpuPercent = 0.0
cpuDelta = float64(currentCPU - previousCPU)
systemDelta = float64(now - previousSystem)
)
if systemDelta > 0.0 && cpuDelta > 0.0 {
// gets a ratio of container cpu usage total, and multiplies that by 100 to get a percentage
cpuPercent = (cpuDelta / systemDelta) * 100
}
return cpuPercent
}

func calculateBlockIO(stats *cgroups.Metrics) (read uint64, write uint64) {
for _, blkIOEntry := range stats.Blkio.IoServiceBytesRecursive {
switch strings.ToLower(blkIOEntry.Op) {
case "read":
read += blkIOEntry.Value
case "write":
write += blkIOEntry.Value
}
}
return
}
50 changes: 14 additions & 36 deletions libpod/stats.go → libpod/stats_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,57 +16,33 @@ import (
"github.com/containers/podman/v4/libpod/define"
)

// GetContainerStats gets the running stats for a given container.
// The previousStats is used to correctly calculate cpu percentages. You
// should pass nil if there is no previous stat for this container.
func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*define.ContainerStats, error) {
stats := new(define.ContainerStats)
stats.ContainerID = c.ID()
stats.Name = c.Name()

// getPlatformContainerStats gets the platform-specific running stats
// for a given container. The previousStats is used to correctly
// calculate cpu percentages. You should pass nil if there is no
// previous stat for this container.
func (c *Container) getPlatformContainerStats(stats *define.ContainerStats, previousStats *define.ContainerStats) error {
if c.config.NoCgroups {
return nil, fmt.Errorf("cannot run top on container %s as it did not create a cgroup: %w", c.ID(), define.ErrNoCgroups)
}

if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.syncContainer(); err != nil {
return stats, err
}
}

// returns stats with the fields' default values respective of their type
if c.state.State != define.ContainerStateRunning && c.state.State != define.ContainerStatePaused {
return stats, nil
}

if previousStats == nil {
previousStats = &define.ContainerStats{
// if we have no prev stats use the container start time as prev time
// otherwise we cannot correctly calculate the CPU percentage
SystemNano: uint64(c.state.StartedTime.UnixNano()),
}
return fmt.Errorf("cannot run top on container %s as it did not create a cgroup: %w", c.ID(), define.ErrNoCgroups)
}

cgroupPath, err := c.cGroupPath()
if err != nil {
return nil, err
return err
}
cgroup, err := cgroups.Load(cgroupPath)
if err != nil {
return stats, fmt.Errorf("unable to load cgroup at %s: %w", cgroupPath, err)
return fmt.Errorf("unable to load cgroup at %s: %w", cgroupPath, err)
}

// Ubuntu does not have swap memory in cgroups because swap is often not enabled.
cgroupStats, err := cgroup.Stat()
if err != nil {
return stats, fmt.Errorf("unable to obtain cgroup stats: %w", err)
return fmt.Errorf("unable to obtain cgroup stats: %w", err)
}
conState := c.state.State
netStats, err := getContainerNetIO(c)
if err != nil {
return nil, err
return err
}

// If the current total usage in the cgroup is less than what was previously
Expand Down Expand Up @@ -103,7 +79,7 @@ func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*de
stats.NetOutput = 0
}

return stats, nil
return nil
}

// getMemory limit returns the memory limit for a container
Expand Down Expand Up @@ -133,7 +109,9 @@ func (c *Container) getMemLimit() uint64 {

// calculateCPUPercent calculates the cpu usage using the latest measurement in stats.
// previousCPU is the last value of stats.CPU.Usage.Total measured at the time previousSystem.
// (now - previousSystem) is the time delta in nanoseconds, between the measurement in previousCPU
//
// (now - previousSystem) is the time delta in nanoseconds, between the measurement in previousCPU
//
// and the updated value in stats.
func calculateCPUPercent(stats *runccgroup.Stats, previousCPU, now, previousSystem uint64) float64 {
var (
Expand Down
4 changes: 2 additions & 2 deletions libpod/stats_unsupported.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//go:build !linux
// +build !linux
//go:build !linux && !freebsd
// +build !linux,!freebsd

package libpod

Expand Down
47 changes: 47 additions & 0 deletions pkg/rctl/rctl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//go:build freebsd
// +build freebsd

package rctl

// #include <sys/rctl.h>
import "C"

import (
"bytes"
"fmt"
"strconv"
"strings"
"syscall"
"unsafe"

"github.com/sirupsen/logrus"
)

func GetRacct(filter string) (map[string]uint64, error) {
bp, err := syscall.ByteSliceFromString(filter)
if err != nil {
return nil, err
}
var buf [1024]byte
_, _, errno := syscall.Syscall6(syscall.SYS_RCTL_GET_RACCT,
uintptr(unsafe.Pointer(&bp[0])),
uintptr(len(bp)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(len(buf)), 0, 0)
if errno != 0 {
return nil, fmt.Errorf("error calling rctl_get_racct with filter %s: %v", errno)
}
len := bytes.IndexByte(buf[:], byte(0))
entries := strings.Split(string(buf[:len]), ",")
res := make(map[string]uint64)
for _, entry := range entries {
kv := strings.SplitN(entry, "=", 2)
key := kv[0]
val, err := strconv.ParseUint(kv[1], 10, 0)
if err != nil {
logrus.Warnf("unexpected rctl entry, ignoring: %s", entry)
}
res[key] = val
}
return res, nil
}

0 comments on commit 82651e5

Please sign in to comment.