gvisor/pkg/test/dockerutil/profile.go

151 lines
4.5 KiB
Go

// Copyright 2020 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dockerutil
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"golang.org/x/sys/unix"
)
// profile represents profile-like operations on a container.
//
// It is meant to be added to containers such that the container type calls
// the profile during its lifecycle. Standard implementations are below.
// profile is for running profiles with 'runsc debug'.
type profile struct {
BasePath string
Types []string
Duration time.Duration
cmd *exec.Cmd
}
// profileInit initializes a profile object, if required.
//
// N.B. The profiling filename initialized here will use the *image*
// name, and not the unique container name. This is intentional. Most
// of the time, profiling will be used for benchmarks. Benchmarks will
// be run iteratively until a sufficiently large N is reached. It is
// useful in this context to overwrite previous runs, and generate a
// single profile result for the final test.
func (c *Container) profileInit(image string) {
if !*pprofBlock && !*pprofCPU && !*pprofMutex && !*pprofHeap {
return // Nothing to do.
}
c.profile = &profile{
BasePath: filepath.Join(*pprofBaseDir, c.runtime, c.logger.Name(), image),
Duration: *pprofDuration,
}
if *pprofCPU {
c.profile.Types = append(c.profile.Types, "cpu")
}
if *pprofHeap {
c.profile.Types = append(c.profile.Types, "heap")
}
if *pprofMutex {
c.profile.Types = append(c.profile.Types, "mutex")
}
if *pprofBlock {
c.profile.Types = append(c.profile.Types, "block")
}
}
// createProcess creates the collection process.
func (p *profile) createProcess(c *Container) error {
// Ensure our directory exists.
if err := os.MkdirAll(p.BasePath, 0755); err != nil {
return err
}
// Find the runtime to invoke.
path, err := RuntimePath()
if err != nil {
return fmt.Errorf("failed to get runtime path: %v", err)
}
// The root directory of this container's runtime.
rootDir := fmt.Sprintf("/var/run/docker/runtime-%s/moby", c.runtime)
if _, err := os.Stat(rootDir); os.IsNotExist(err) {
// In docker v20+, due to https://github.com/moby/moby/issues/42345 the
// rootDir seems to always be the following.
rootDir = "/var/run/docker/runtime-runc/moby"
}
// Format is `runsc --root=rootDir debug --profile-*=file --duration=24h containerID`.
args := []string{fmt.Sprintf("--root=%s", rootDir), "debug"}
for _, profileArg := range p.Types {
outputPath := filepath.Join(p.BasePath, fmt.Sprintf("%s.pprof", profileArg))
args = append(args, fmt.Sprintf("--profile-%s=%s", profileArg, outputPath))
}
args = append(args, fmt.Sprintf("--duration=%s", p.Duration)) // Or until container exits.
args = append(args, fmt.Sprintf("--delay=%s", p.Duration)) // Ditto.
args = append(args, c.ID())
// Best effort wait until container is running.
for now := time.Now(); time.Since(now) < 5*time.Second; {
if status, err := c.Status(context.Background()); err != nil {
return fmt.Errorf("failed to get status with: %v", err)
} else if status.Running {
break
}
time.Sleep(100 * time.Millisecond)
}
p.cmd = exec.Command(path, args...)
p.cmd.Stderr = os.Stderr // Pass through errors.
if err := p.cmd.Start(); err != nil {
return fmt.Errorf("start process failed: %v", err)
}
return nil
}
// killProcess kills the process, if running.
func (p *profile) killProcess() error {
if p.cmd != nil && p.cmd.Process != nil {
return p.cmd.Process.Signal(unix.SIGTERM)
}
return nil
}
// waitProcess waits for the process, if running.
func (p *profile) waitProcess() error {
defer func() { p.cmd = nil }()
if p.cmd != nil {
return p.cmd.Wait()
}
return nil
}
// Start is called when profiling is started.
func (p *profile) Start(c *Container) error {
return p.createProcess(c)
}
// Stop is called when profiling is started.
func (p *profile) Stop(c *Container) error {
killErr := p.killProcess()
waitErr := p.waitProcess()
if waitErr != nil && killErr != nil {
return killErr
}
return waitErr // Ignore okay wait, err kill.
}