blob: a013fe0095f8de5b14a8d6cd3bc93a26eb45ca39 [file] [log] [blame]
// Copyright 2024 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"fmt"
"log"
"path/filepath"
"strings"
"time"
"go.chromium.org/chromiumos/config/go/test/api"
)
const (
pgrep = "pgrep"
ps = "ps"
perf = "perf"
mkdir = "mkdir"
samplingRate = "8000"
kill = "kill"
outputPerfDir = "/tmp/perf"
perfData = "perf.data"
nohup = "nohup"
perfInitWait = 1 * time.Second
exitStatus1 = "status:1"
)
var (
perfArgs = []string{
"perf",
"record",
"-e", // the Performance Monitoring Unit event
"cycles",
"--timestamp", // record the sample timestamps
"--switch-output=60s", // emit a perf.data file every 1 minute
"--all-cpus",
"-F",
samplingRate,
"-o", // will generate /tmp/perf/perf.data.<timestamp> files.
filepath.Join(outputPerfDir, perfData),
}
mkdirArgs = []string{
"-p",
outputPerfDir,
}
)
type execUtil interface {
RunCmd(ctx context.Context, cmd string, args []string, dut api.DutServiceClient) (string, error)
RunCmdAsync(ctx context.Context, cmd string, args []string, dut api.DutServiceClient) error
}
type perfCmd struct {
dut api.DutServiceClient
exec execUtil
perfInitWaitTimeout time.Duration
perfInitPollInterval time.Duration
}
func (p *perfCmd) perfId(ctx context.Context) (string, error) {
// It takes few milliseconds for RunCmdAsync to start the perf process on dut.
// Need to wait little bit before quering for the perf processID.
timeout := time.After(p.perfInitWaitTimeout)
for {
select {
case <-time.After(p.perfInitPollInterval):
pids, err := p.perfPids(ctx)
if err != nil {
return "", err
}
if len(pids) != 1 {
return "", fmt.Errorf("expect one perf to be present on dut but got %d pids: %v", len(pids), pids)
}
return pids[0], nil
case <-timeout:
return "", fmt.Errorf("did not find any perf process on DUT")
}
}
}
func (p *perfCmd) startPerf(ctx context.Context) (string, error) {
// Create a dir where the perf.data will be saved.
if _, err := p.exec.RunCmd(ctx, mkdir, mkdirArgs, p.dut); err != nil {
return "", err
}
// perf-record is an non-terminating cmd. It blocks the session/terminal
// and will continue to run until it is signalled to stop.
// Due to this property we use RunCmdAsync, which is a non blocking way to run the command.
// When the session used to start perf-record gets terminated(perhaps due to network glich),
// the corresponding perf process will also get terminated.
// Using nohup provides robustness as the perf process will continue to run
// in background even after the session is closed.
if err := p.exec.RunCmdAsync(ctx, nohup, perfArgs, p.dut); err != nil {
return "", err
}
return p.perfId(ctx)
}
func (p *perfCmd) kill(ctx context.Context, pids ...string) error {
for _, pid := range pids {
args := []string{
"-SIGTERM",
pid,
}
if _, err := p.exec.RunCmd(ctx, kill, args, p.dut); err != nil {
return err
}
log.Printf("Successfully stopped process: %s\n", pid)
}
return nil
}
func (p *perfCmd) perfPids(ctx context.Context) ([]string, error) {
var result []string
output, err := p.exec.RunCmd(ctx, pgrep, []string{perf, "||", "true"}, p.dut)
if err != nil && !strings.Contains(err.Error(), exitStatus1) {
return nil, err
}
output = strings.TrimSpace(output)
for _, pid := range strings.Split(output, "\n") {
if pid = strings.TrimSpace(pid); pid != "" {
result = append(result, pid)
}
}
return result, nil
}
func (p *perfCmd) killAll(ctx context.Context) error {
log.Println("Killing all perf processes on dut.")
pids, err := p.perfPids(ctx)
if err != nil {
return err
}
return p.kill(ctx, pids...)
}