gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/test/dockerutil/profile.go (about)

     1  // Copyright 2020 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package dockerutil
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"os/exec"
    22  	"path/filepath"
    23  	"time"
    24  
    25  	"golang.org/x/sys/unix"
    26  )
    27  
    28  // profile represents profile-like operations on a container.
    29  //
    30  // It is meant to be added to containers such that the container type calls
    31  // the profile during its lifecycle. Standard implementations are below.
    32  
    33  // profile is for running profiles with 'runsc debug'.
    34  type profile struct {
    35  	BasePath string
    36  	Types    []string
    37  	Duration time.Duration
    38  	cmd      *exec.Cmd
    39  }
    40  
    41  // profileInit initializes a profile object, if required.
    42  //
    43  // N.B. The profiling filename initialized here will use the *image*
    44  // name, and not the unique container name. This is intentional. Most
    45  // of the time, profiling will be used for benchmarks. Benchmarks will
    46  // be run iteratively until a sufficiently large N is reached. It is
    47  // useful in this context to overwrite previous runs, and generate a
    48  // single profile result for the final test.
    49  func (c *Container) profileInit(image string) {
    50  	if !*pprofBlock && !*pprofCPU && !*pprofMutex && !*pprofHeap && !*trace {
    51  		return // Nothing to do.
    52  	}
    53  	c.profile = &profile{
    54  		BasePath: filepath.Join(*pprofBaseDir, c.runtime, c.logger.Name(), image),
    55  		Duration: *pprofDuration,
    56  	}
    57  	if *pprofCPU {
    58  		c.profile.Types = append(c.profile.Types, "cpu")
    59  	}
    60  	if *pprofHeap {
    61  		c.profile.Types = append(c.profile.Types, "heap")
    62  	}
    63  	if *pprofMutex {
    64  		c.profile.Types = append(c.profile.Types, "mutex")
    65  	}
    66  	if *pprofBlock {
    67  		c.profile.Types = append(c.profile.Types, "block")
    68  	}
    69  }
    70  
    71  // createProcess creates the collection process.
    72  func (p *profile) createProcess(c *Container) error {
    73  	// Ensure our directory exists.
    74  	if err := os.MkdirAll(p.BasePath, 0755); err != nil {
    75  		return err
    76  	}
    77  
    78  	// Find the runtime to invoke.
    79  	path, err := RuntimePath()
    80  	if err != nil {
    81  		return fmt.Errorf("failed to get runtime path: %v", err)
    82  	}
    83  
    84  	rootDir, err := c.RootDirectory()
    85  	if err != nil {
    86  		return fmt.Errorf("failed to get root directory: %v", err)
    87  	}
    88  
    89  	// Format is `runsc --root=rootDir debug --profile-*=file --duration=24h containerID`.
    90  	args := []string{fmt.Sprintf("--root=%s", rootDir), "debug"}
    91  	for _, profileArg := range p.Types {
    92  		outputPath := filepath.Join(p.BasePath, fmt.Sprintf("%s.pprof", profileArg))
    93  		args = append(args, fmt.Sprintf("--profile-%s=%s", profileArg, outputPath))
    94  	}
    95  	if *trace {
    96  		args = append(args, fmt.Sprintf("--trace=%s", filepath.Join(p.BasePath, "sentry.trace")))
    97  	}
    98  	args = append(args, fmt.Sprintf("--duration=%s", p.Duration)) // Or until container exits.
    99  	args = append(args, fmt.Sprintf("--delay=%s", p.Duration))    // Ditto.
   100  	args = append(args, c.ID())
   101  
   102  	// Best effort wait until container is running.
   103  	for now := time.Now(); time.Since(now) < 5*time.Second; {
   104  		if status, err := c.Status(context.Background()); err != nil {
   105  			return fmt.Errorf("failed to get status with: %v", err)
   106  		} else if status.Running {
   107  			break
   108  		}
   109  		time.Sleep(100 * time.Millisecond)
   110  	}
   111  	p.cmd = exec.Command(path, args...)
   112  	p.cmd.Stderr = os.Stderr // Pass through errors.
   113  	if err := p.cmd.Start(); err != nil {
   114  		return fmt.Errorf("start process failed: %v", err)
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  // killProcess kills the process, if running.
   121  func (p *profile) killProcess() error {
   122  	if p.cmd != nil && p.cmd.Process != nil {
   123  		return p.cmd.Process.Signal(unix.SIGTERM)
   124  	}
   125  	return nil
   126  }
   127  
   128  // waitProcess waits for the process, if running.
   129  func (p *profile) waitProcess() error {
   130  	defer func() { p.cmd = nil }()
   131  	if p.cmd != nil {
   132  		return p.cmd.Wait()
   133  	}
   134  	return nil
   135  }
   136  
   137  // Start is called when profiling is started.
   138  func (p *profile) Start(c *Container) error {
   139  	return p.createProcess(c)
   140  }
   141  
   142  // Stop is called when profiling is started.
   143  func (p *profile) Stop(c *Container) error {
   144  	killErr := p.killProcess()
   145  	waitErr := p.waitProcess()
   146  	if waitErr != nil && killErr != nil {
   147  		return killErr
   148  	}
   149  	return waitErr // Ignore okay wait, err kill.
   150  }