Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for 'podman stats' on FreeBSD #15769

Merged
merged 4 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}