blob: d5a7510ddd31a1e0b460fab74fe03b2321a2673f [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 profiler
import (
"bufio"
"context"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/fsutil"
"chromiumos/tast/shutil"
)
// perf represents the perf profiler.
//
// perf supports gathering profiler data using the
// command "perf" with the perfType ("record", "stat record", or "stat") specified.
type perf struct {
cmd *testexec.Cmd
opts *PerfOpts
outDir string
}
// perfType represents the type of perf that the users
// want to use.
type perfType int
// Type of perf
const (
// perfRecord runs "perf record -e cycles -g" on the DUT.
perfRecord perfType = iota
// perfStatRecord runs "perf stat record -a" on the DUT.
perfStatRecord
// perfStat runs "perf stat -a" on the DUT.
perfStat
// perfSched runs "perf sched record" on the DUT.
perfSched
perfRecordFileName = "perf_record.data"
perfStatRecordFileName = "perf_stat_record.data"
perfStatFileName = "perf_stat.data"
perfSchedFileName = "perf_sched.data"
// Used in perfStat to get CPU cycle count on all processes.
PerfAllProcs = 0
)
var (
noCyclesRegexp = regexp.MustCompile(`(?s)\s+\<not counted\>\s+cycles`)
cyclesRegexp = regexp.MustCompile(`(?s)\s+(\d+)\s+cycles`)
secondsRegexp = regexp.MustCompile(`(?s)\s+(\d+\.?[\d+]*)\s+seconds time elapsed`)
)
// PerfStatOutput holds output of perf stat.
type PerfStatOutput struct {
CyclesPerSecond float64
}
// PerfSchedOutput holds output metrics of perf sched.
type PerfSchedOutput struct {
// Maximum latency from wake up to switch
MaxLatencyMs float64
}
// PerfOpts represents options for running perf.
type PerfOpts struct {
// perfType indicates the type of profiler running ("record", "stat record", or "stat").
perfType perfType
// Used in perfStat.
// Indicate the target process.
pid int
// Used in perfStat.
// A pointer to the output of perfStat.
perfStatOutput *PerfStatOutput
// Used in perf sched to get stats of process.
procName string
// Used in perf sched to provide output.
perfSchedOutput *PerfSchedOutput
}
// PerfStatOpts creates a PerfOpts for running "perf stat -a" on the DUT.
// out is a pointer to PerfStatOutput, which will hold CPU cycle count per second spent
// on pid process after End() is called on RunningProf.
// Set pid to PerfAllProcs to get cycle count for the whole system.
func PerfStatOpts(out *PerfStatOutput, pid int) *PerfOpts {
return &PerfOpts{perfType: perfStat, pid: pid, perfStatOutput: out}
}
// PerfRecordOpts creates a PerfOpts for running "perf record -e cycles -g" on the DUT.
func PerfRecordOpts() *PerfOpts {
return &PerfOpts{perfType: perfRecord}
}
// PerfStatRecordOpts creates a PerfOpts for running "perf stat record -a" on the DUT.
func PerfStatRecordOpts() *PerfOpts {
return &PerfOpts{perfType: perfStatRecord}
}
// PerfSchedOpts creates a PerfOpts for running "perf sched record" on the DUT.
func PerfSchedOpts(out *PerfSchedOutput, procName string) *PerfOpts {
return &PerfOpts{perfType: perfSched, procName: procName, perfSchedOutput: out}
}
// Perf creates a Profiler instance that constructs the profiler.
// For opts parameter, nil is treated as the zero value of PerfOpts.
func Perf(opts *PerfOpts) Profiler {
// Set default options if needed.
if opts == nil {
opts = PerfRecordOpts()
}
return func(ctx context.Context, outDir string) (instance, error) {
return newPerf(ctx, outDir, opts)
}
}
// newPerf creates and runs perf command to start recording perf.data with the options specified.
func newPerf(ctx context.Context, outDir string, opts *PerfOpts) (instance, error) {
if opts.perfType == perfStat && opts.pid < 0 {
return nil, errors.Errorf("invalid pid %d for perfStat", opts.pid)
}
cmd, err := getCmd(ctx, outDir, opts)
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
cmd.DumpLog(ctx)
return nil, errors.Wrapf(err, "failed running %s", shutil.EscapeSlice(cmd.Args))
}
success := false
defer func() {
if !success {
cmd.Kill()
cmd.Wait()
}
}()
// KASLR makes looking up the symbols from the binary impossible, save
// the running symbols from DUT to outDir.
kallsymsPath := filepath.Join(outDir, "kallsyms")
if err := fsutil.CopyFile("/proc/kallsyms", kallsymsPath); err != nil {
return nil, errors.Wrap(err, "failed copying /proc/kallsyms to output directory")
}
success = true
return &perf{
cmd: cmd,
opts: opts,
outDir: outDir,
}, nil
}
func getCmd(ctx context.Context, outDir string, opts *PerfOpts) (*testexec.Cmd, error) {
switch opts.perfType {
case perfRecord:
outputPath := filepath.Join(outDir, perfRecordFileName)
return testexec.CommandContext(ctx, "perf", "record", "-e", "cycles", "-g", "--output", outputPath), nil
case perfStatRecord:
outputPath := filepath.Join(outDir, perfStatRecordFileName)
return testexec.CommandContext(ctx, "perf", "stat", "record", "-a", "--output", outputPath), nil
case perfStat:
outputPath := filepath.Join(outDir, perfStatFileName)
if (*opts).pid == PerfAllProcs {
return testexec.CommandContext(ctx, "perf", "stat", "-a", "-e", "cycles", "--output", outputPath), nil
}
return testexec.CommandContext(ctx, "perf", "stat", "-a", "-p", strconv.Itoa(opts.pid), "-e", "cycles", "--output", outputPath), nil
case perfSched:
outputPath := filepath.Join(outDir, perfSchedFileName)
return testexec.CommandContext(ctx, "perf", "sched", "record", "--output", outputPath), nil
default:
return nil, errors.Errorf("invalid perf type: %v", opts.perfType)
}
}
// getMaxLatencyMs is for perf sched latency and parses Maximum latency from wake up to switch.
func getMaxLatencyMs(ctx context.Context, perfSchedFile, procName string) (float64, error) {
cmd := testexec.CommandContext(ctx, "perf", "sched", "latency", "-i", perfSchedFile)
output, err := cmd.Output()
if err != nil {
return 0, errors.Wrap(err, "failed to get output of perf sched latency")
}
perfSchedLatencyFile := filepath.Join(filepath.Dir(perfSchedFile), "perf_sched_latency.out")
if err := ioutil.WriteFile(perfSchedLatencyFile, output, 0644); err != nil {
return 0, errors.Wrap(err, "failed to write latency file")
}
file, err := os.Open(perfSchedLatencyFile)
if err != nil {
return 0, errors.Wrap(err, "failed to open perf sched latency file")
}
defer file.Close()
re := regexp.MustCompile(`max:\s*(.+?)\s*ms`)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
if strings.Contains(scanner.Text(), procName+":") {
res := re.FindAllStringSubmatch(scanner.Text(), -1)
if res == nil {
return 0, errors.New("failed to parse max latency")
}
f, err := strconv.ParseFloat(res[0][1], 64)
if err != nil {
return 0, errors.Wrap(err, "failed to parse max latency")
}
return f, nil
}
}
return 0, errors.New("failed to read perf sched file")
}
// parseStatFile parses the output file of perf stat command to get CPU cycles per second
// spent in a process. The file should contain cycles and seconds elapsed.
// The return value is a float64 for cycles per second.
func parseStatFile(path string) (float64, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return 0, errors.Wrapf(err, "failed to read %q", path)
}
s := string(b)
if noCyclesRegexp.FindString(s) != "" {
return 0, errors.New("got 0 cycle")
}
m := cyclesRegexp.FindStringSubmatch(s)
if m == nil {
return 0, errors.New("no cycles in perf stat output")
}
cycles, err := strconv.ParseInt(m[1], 0, 64)
if err != nil {
return 0, errors.Wrap(err, "failed to parse cycles")
}
m = secondsRegexp.FindStringSubmatch(s)
if m == nil {
return 0, errors.New("no seconds in perf stat output")
}
seconds, err := strconv.ParseFloat(m[1], 64)
if err != nil {
return 0, errors.Wrap(err, "failed to parse seconds")
}
cyclesPerSecond := float64(cycles) / seconds
return cyclesPerSecond, nil
}
func (p *perf) handleStat() error {
perfPath := filepath.Join(p.outDir, perfStatFileName)
cyclesPerSecond, err := parseStatFile(perfPath)
if err != nil {
return errors.Wrap(err, "failed to parse stat file")
}
p.opts.perfStatOutput.CyclesPerSecond = cyclesPerSecond
return nil
}
func (p *perf) handleSched(ctx context.Context) error {
perfPath := filepath.Join(p.outDir, perfSchedFileName)
maxLatencyMs, err := getMaxLatencyMs(ctx, perfPath, p.opts.procName)
if err != nil {
return errors.Wrap(err, "failed to parse sched file")
}
p.opts.perfSchedOutput.MaxLatencyMs = maxLatencyMs
return nil
}
func (p *perf) handleOutput(ctx context.Context) error {
switch p.opts.perfType {
case perfStat:
if err := p.handleStat(); err != nil {
return errors.Wrap(err, "failed to handle perf stat result")
}
case perfSched:
if err := p.handleSched(ctx); err != nil {
return errors.Wrap(err, "failed to handle perf sched result")
}
}
return nil
}
// end interrupts the perf command and ends the recording of perf.data.
func (p *perf) end(ctx context.Context) error {
// Interrupt the cmd to stop recording perf.
p.cmd.Signal(syscall.SIGINT)
err := p.cmd.Wait()
// The signal is interrupt intentionally, so we check the wait status
// instead of refusing the error.
if ws, ok := testexec.GetWaitStatus(err); !ok || !ws.Signaled() || ws.Signal() != syscall.SIGINT {
return errors.Wrap(err, "failed waiting for the command to exit")
}
return p.handleOutput(ctx)
}