blob: 483b5165f8d5e33e7214d80177976f3e9fed78b1 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package perfutil contains utilities needed for VM performance testing.
package perfutil
import (
"bytes"
"context"
"fmt"
"io"
"path/filepath"
"regexp"
"strings"
"time"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/local/vm"
"chromiumos/tast/testing"
)
// ContainerHomeDir is the default home directory for test user in container.
const ContainerHomeDir = "/home/testuser"
// WriteError output errors to an io.Writer.
func WriteError(ctx context.Context, w io.Writer, title string, content []byte) {
const logTemplate = "========== START %s ==========\n%s\n========== END ==========\n"
if _, err := fmt.Fprintf(w, logTemplate, title, content); err != nil {
testing.ContextLog(ctx, "Failed to write error to log file: ", err)
}
}
// RunCmd runs cmd and returns its combined stdout and stderr.
// If an error is encountered, details are written to errWriter.
func RunCmd(ctx context.Context, cmd *testexec.Cmd, errWriter io.Writer) (out []byte, err error) {
out, err = cmd.CombinedOutput()
if err == nil {
return out, nil
}
cmdString := strings.Join(append(cmd.Cmd.Env, cmd.Cmd.Args...), " ")
// Write complete stdout and stderr to a log file.
WriteError(ctx, errWriter, cmdString, out)
// Only append the first and last line of the output to the error.
out = bytes.TrimSpace(out)
var errSnippet string
if idx := bytes.IndexAny(out, "\r\n"); idx != -1 {
lastIdx := bytes.LastIndexAny(out, "\r\n")
errSnippet = fmt.Sprintf("%s ... %s", out[:idx], out[lastIdx+1:])
} else {
errSnippet = string(out)
}
return nil, errors.Wrap(err, errSnippet)
}
// ToTimeUnit returns time.Duration in unit as float64 numbers.
func ToTimeUnit(unit time.Duration, ts ...time.Duration) (out []float64) {
for _, t := range ts {
out = append(out, float64(t)/float64(unit))
}
return out
}
// parseLddOutput parses the output of a "ldd" command. Example "ldd" output:
// libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f96da2cf000)
// libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f96d9f30000)
// libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f96d9cbd000)
// libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f96d9ab9000)
// /lib64/ld-linux-x86-64.so.2 (0x00007f96da71a000)
// libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f96d989c000)
func parseLddOutput(ctx context.Context, out string) (dynLibs map[string]string, dynLinker string) {
dynLibPattern := regexp.MustCompile(`^(\S+) => (\S+)`)
dynLinkerPattern := regexp.MustCompile(`^\S*lib\S*ld-linux\S*\.so\S*`)
dynLibs = map[string]string{}
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
matched := dynLibPattern.FindStringSubmatch(line)
if matched != nil {
testing.ContextLogf(ctx, "Found dynamic lib: %s", matched[0])
dynLibs[matched[1]] = matched[2]
} else {
// Not a dynamic library line, checks if it's a dynamic linker line.
matched := dynLinkerPattern.FindString(line)
if matched != "" {
testing.ContextLogf(ctx, "Found dynamic linker: %s", matched)
if dynLinker != "" {
testing.ContextLogf(ctx, "Already have %s, %s is ignored", dynLinker, matched)
continue
}
dynLinker = matched
}
}
}
return dynLibs, dynLinker
}
// HostBinaryRunner runs a binary compiled for host in container. It is useful to ensure the same
// binary is used when the performance of the binary itself matters. This avoids performance gap
// introduced by different compiler/compiler flags/optimization level etc.
// It copies needed dynamic library and dynamic linker from host to container so the binary
// can be executed.
type HostBinaryRunner struct {
binary string // Full path of the binary.
cont *vm.Container // A container instance to run in.
contBinaryPath, contDynLinkerPath, contLibsPath string // Used internally to specify file locations in container.
}
// NewHostBinaryRunner creates a HostBinaryRunner object.
func NewHostBinaryRunner(ctx context.Context, binary string, cont *vm.Container, errWriter io.Writer) (*HostBinaryRunner, error) {
h := &HostBinaryRunner{binary: binary, cont: cont}
baseName := filepath.Base(h.binary)
// Copy the binary itself.
testing.ContextLogf(ctx, "Copying %s to container", h.binary)
h.contBinaryPath = filepath.Join(ContainerHomeDir, baseName)
err := cont.PushFile(ctx, h.binary, h.contBinaryPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to copy %q to container", h.binary)
}
h.contLibsPath = filepath.Join(ContainerHomeDir, baseName+"_libs")
_, err = RunCmd(ctx, cont.Command(ctx, "mkdir", "-p", h.contLibsPath), errWriter)
if err != nil {
return nil, errors.Wrapf(err, "failed to create %q in container", h.contLibsPath)
}
// Parse ldd output to get a list of dynamic libraries and dynamic linker.
out, err := RunCmd(ctx, testexec.CommandContext(ctx, "ldd", h.binary), errWriter)
if err != nil {
return nil, errors.Wrapf(err, "failed to run ldd on %q", h.binary)
}
dynLibs, dynLinker := parseLddOutput(ctx, string(out))
if dynLinker == "" {
return nil, errors.Errorf("could not find dynamic linker for %q", h.binary)
}
// Copy dynamic linker.
testing.ContextLogf(ctx, "Copying dynamic linker %s to container", dynLinker)
h.contDynLinkerPath = filepath.Join(ContainerHomeDir, filepath.Base(dynLinker))
err = cont.PushFile(ctx, dynLinker, h.contDynLinkerPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to copy dynamic linker %q to container", dynLinker)
}
// Dynmic linker needs to be executable.
_, err = RunCmd(ctx, cont.Command(ctx, "chmod", "755", h.contDynLinkerPath), errWriter)
if err != nil {
return nil, errors.Wrapf(err, "failed to chmod %q", h.contDynLinkerPath)
}
// Copy dynamic libraries.
for libName, libPath := range dynLibs {
containerPath := filepath.Join(h.contLibsPath, libName)
testing.ContextLogf(ctx, "Copying %s to %s", libPath, containerPath)
err = cont.PushFile(ctx, libPath, containerPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to copy %q to %q", libPath, containerPath)
}
}
return h, nil
}
// Command returns a command for executing the binary in the container
// with the supplied args.
func (h *HostBinaryRunner) Command(ctx context.Context, args ...string) *testexec.Cmd {
cmdArgs := append([]string{h.contDynLinkerPath, "--library-path", h.contLibsPath, h.contBinaryPath}, args...)
return h.cont.Command(ctx, cmdArgs...)
}