Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions cmd/metrics/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"time"

"perfspect/internal/cpus"
"perfspect/internal/progress"
"perfspect/internal/report"
"perfspect/internal/script"
"perfspect/internal/target"
Expand Down Expand Up @@ -77,7 +78,7 @@ type Metadata struct {

// LoadMetadata - populates and returns a Metadata structure containing state of the
// system.
func LoadMetadata(myTarget target.Target, noRoot bool, noSystemSummary bool, perfPath string, localTempDir string) (Metadata, error) {
func LoadMetadata(myTarget target.Target, noRoot bool, noSystemSummary bool, localTempDir string, statusUpdate progress.MultiSpinnerUpdateFunc) (Metadata, error) {
uarch, err := myTarget.GetArchitecture()
if err != nil {
return Metadata{}, fmt.Errorf("failed to get target architecture: %v", err)
Expand All @@ -86,11 +87,11 @@ func LoadMetadata(myTarget target.Target, noRoot bool, noSystemSummary bool, per
if err != nil {
return Metadata{}, fmt.Errorf("failed to create metadata collector: %v", err)
}
return collector.CollectMetadata(myTarget, noRoot, noSystemSummary, perfPath, localTempDir)
return collector.CollectMetadata(myTarget, noRoot, noSystemSummary, localTempDir, statusUpdate)
}

type MetadataCollector interface {
CollectMetadata(myTarget target.Target, noRoot bool, noSystemSummary bool, perfPath string, localTempDir string) (Metadata, error)
CollectMetadata(myTarget target.Target, noRoot bool, noSystemSummary bool, localTempDir string, statusUpdate progress.MultiSpinnerUpdateFunc) (Metadata, error)
}

func NewMetadataCollector(architecture string) (MetadataCollector, error) {
Expand All @@ -112,7 +113,7 @@ type X86MetadataCollector struct {
type ARMMetadataCollector struct {
}

func (c *X86MetadataCollector) CollectMetadata(myTarget target.Target, noRoot bool, noSystemSummary bool, perfPath string, localTempDir string) (Metadata, error) {
func (c *X86MetadataCollector) CollectMetadata(myTarget target.Target, noRoot bool, noSystemSummary bool, localTempDir string, statusUpdate progress.MultiSpinnerUpdateFunc) (Metadata, error) {
var metadata Metadata
var err error
// Hostname
Expand Down Expand Up @@ -158,12 +159,12 @@ func (c *X86MetadataCollector) CollectMetadata(myTarget target.Target, noRoot bo
return Metadata{}, fmt.Errorf("failed to get number of general purpose counters: %v", err)
}
// the rest of the metadata is retrieved by running scripts in parallel
metadataScripts, err := getMetadataScripts(noRoot, perfPath, noSystemSummary, metadata.NumGeneralPurposeCounters)
metadataScripts, err := getMetadataScripts(noRoot, noSystemSummary, metadata.NumGeneralPurposeCounters)
if err != nil {
return Metadata{}, fmt.Errorf("failed to get metadata scripts: %v", err)
}
// run the scripts
scriptOutputs, err := script.RunScripts(myTarget, metadataScripts, true, localTempDir, nil, "") // nosemgrep
scriptOutputs, err := script.RunScripts(myTarget, metadataScripts, true, localTempDir, statusUpdate, "collecting metadata") // nosemgrep
if err != nil {
return Metadata{}, fmt.Errorf("failed to run metadata scripts: %v", err)
}
Expand Down Expand Up @@ -282,7 +283,8 @@ func (c *X86MetadataCollector) CollectMetadata(myTarget target.Target, noRoot bo
}
return metadata, nil
}
func (c *ARMMetadataCollector) CollectMetadata(myTarget target.Target, noRoot bool, noSystemSummary bool, perfPath string, localTempDir string) (Metadata, error) {

func (c *ARMMetadataCollector) CollectMetadata(myTarget target.Target, noRoot bool, noSystemSummary bool, localTempDir string, statusUpdate progress.MultiSpinnerUpdateFunc) (Metadata, error) {
var metadata Metadata
// Hostname
metadata.Hostname = myTarget.GetName()
Expand Down Expand Up @@ -342,7 +344,7 @@ func (c *ARMMetadataCollector) CollectMetadata(myTarget target.Target, noRoot bo
return Metadata{}, fmt.Errorf("failed to get number of general purpose counters: %v", err)
}
// the rest of the metadata is retrieved by running scripts in parallel and then parsing the output
metadataScripts, err := getMetadataScripts(noRoot, perfPath, noSystemSummary, metadata.NumGeneralPurposeCounters)
metadataScripts, err := getMetadataScripts(noRoot, noSystemSummary, metadata.NumGeneralPurposeCounters)
if err != nil {
return Metadata{}, fmt.Errorf("failed to get metadata scripts: %v", err)
}
Expand Down Expand Up @@ -397,7 +399,7 @@ func (c *ARMMetadataCollector) CollectMetadata(myTarget target.Target, noRoot bo
return metadata, nil
}

func getMetadataScripts(noRoot bool, perfPath string, noSystemSummary bool, numGPCounters int) (metadataScripts []script.ScriptDefinition, err error) {
func getMetadataScripts(noRoot bool, noSystemSummary bool, numGPCounters int) (metadataScripts []script.ScriptDefinition, err error) {
// reduce startup time by running the metadata scripts in parallel
metadataScriptDefs := []script.ScriptDefinition{
{
Expand All @@ -407,8 +409,9 @@ func getMetadataScripts(noRoot bool, perfPath string, noSystemSummary bool, numG
},
{
Name: "perf supported events",
ScriptTemplate: perfPath + " list",
ScriptTemplate: "perf list",
Superuser: !noRoot,
Depends: []string{"perf"},
},
{
Name: "list uncore devices",
Expand All @@ -418,46 +421,54 @@ func getMetadataScripts(noRoot bool, perfPath string, noSystemSummary bool, numG
},
{
Name: "perf stat instructions",
ScriptTemplate: perfPath + " stat -a -e instructions sleep 1",
ScriptTemplate: "perf stat -a -e instructions sleep 1",
Superuser: !noRoot,
Depends: []string{"perf"},
},
{
Name: "perf stat ref-cycles",
ScriptTemplate: perfPath + " stat -a -e ref-cycles sleep 1",
ScriptTemplate: "perf stat -a -e ref-cycles sleep 1",
Superuser: !noRoot,
Depends: []string{"perf"},
},
{
Name: "perf stat pebs",
ScriptTemplate: perfPath + " stat -a -e INT_MISC.UNKNOWN_BRANCH_CYCLES sleep 1",
ScriptTemplate: "perf stat -a -e INT_MISC.UNKNOWN_BRANCH_CYCLES sleep 1",
Superuser: !noRoot,
Architectures: []string{cpus.X86Architecture},
Depends: []string{"perf"},
},
{
Name: "perf stat ocr",
ScriptTemplate: perfPath + " stat -a -e OCR.READS_TO_CORE.LOCAL_DRAM sleep 1",
ScriptTemplate: "perf stat -a -e OCR.READS_TO_CORE.LOCAL_DRAM sleep 1",
Superuser: !noRoot,
Architectures: []string{cpus.X86Architecture},
Depends: []string{"perf"},
},
{
Name: "perf stat tma",
ScriptTemplate: perfPath + " stat -a -e '{topdown.slots, topdown-bad-spec}' sleep 1",
ScriptTemplate: "perf stat -a -e '{topdown.slots, topdown-bad-spec}' sleep 1",
Superuser: !noRoot,
Architectures: []string{cpus.X86Architecture},
Depends: []string{"perf"},
},
{
Name: "perf stat fixed instructions",
ScriptTemplate: perfPath + " stat -a -e '{{{.InstructionsList}}}' sleep 1",
ScriptTemplate: "perf stat -a -e '{{{.InstructionsList}}}' sleep 1",
Superuser: !noRoot,
Depends: []string{"perf"},
},
{
Name: "perf stat fixed cpu-cycles",
ScriptTemplate: perfPath + " stat -a -e '{{{.CpuCyclesList}}}' sleep 1",
ScriptTemplate: "perf stat -a -e '{{{.CpuCyclesList}}}' sleep 1",
Superuser: !noRoot,
Depends: []string{"perf"},
},
{
Name: "perf stat fixed ref-cycles",
ScriptTemplate: perfPath + " stat -a -e '{{{.RefCyclesList}}}' sleep 1",
ScriptTemplate: "perf stat -a -e '{{{.RefCyclesList}}}' sleep 1",
Superuser: !noRoot,
Depends: []string{"perf"},
},
{
Name: "pmu driver version",
Expand Down
31 changes: 5 additions & 26 deletions cmd/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,6 @@ func validateFlags(cmd *cobra.Command, args []string) error {
type targetContext struct {
target target.Target
err error
perfPath string
metadata Metadata
nmiDisabled bool
perfMuxIntervalsSet bool
Expand Down Expand Up @@ -974,22 +973,14 @@ func runCmd(cmd *cobra.Command, args []string) error {
return err
}
}
// extract perf into local temp directory (assumes all targets have the same architecture)
localPerfPath, err := extractPerf(myTargets[0], localTempDir)
if err != nil {
err = fmt.Errorf("failed to extract perf: %w", err)
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
cmd.SilenceUsage = true
return err
}
// prepare the targets
channelTargetError := make(chan targetError)
var targetContexts []targetContext
for _, myTarget := range myTargets {
targetContexts = append(targetContexts, targetContext{target: myTarget})
}
for i := range targetContexts {
go prepareTarget(&targetContexts[i], localTempDir, localPerfPath, channelTargetError, multiSpinner.Status, !cmd.Flags().Lookup(flagPerfMuxIntervalName).Changed)
go prepareTarget(&targetContexts[i], localTempDir, channelTargetError, multiSpinner.Status, !cmd.Flags().Lookup(flagPerfMuxIntervalName).Changed)
}
// wait for all targets to be prepared
numPreparedTargets := 0
Expand Down Expand Up @@ -1144,7 +1135,7 @@ func runCmd(cmd *cobra.Command, args []string) error {
return err
}

func prepareTarget(targetContext *targetContext, localTempDir string, localPerfPath string, channelError chan targetError, statusUpdate progress.MultiSpinnerUpdateFunc, useDefaultMuxInterval bool) {
func prepareTarget(targetContext *targetContext, localTempDir string, channelError chan targetError, statusUpdate progress.MultiSpinnerUpdateFunc, useDefaultMuxInterval bool) {
myTarget := targetContext.target
var err error
_ = statusUpdate(myTarget.GetName(), "configuring target")
Expand Down Expand Up @@ -1215,15 +1206,6 @@ func prepareTarget(targetContext *targetContext, localTempDir string, localPerfP
}
targetContext.perfMuxIntervalsSet = true
}
// get the full path to the perf binary
if targetContext.perfPath, err = getPerfPath(myTarget, localPerfPath); err != nil {
err = fmt.Errorf("failed to find perf: %w", err)
_ = statusUpdate(myTarget.GetName(), fmt.Sprintf("Error: %v", err))
targetContext.err = err
channelError <- targetError{target: myTarget, err: err}
return
}
slog.Debug("Using Linux perf", slog.String("target", targetContext.target.GetName()), slog.String("path", targetContext.perfPath))
channelError <- targetError{target: myTarget, err: nil}
}

Expand All @@ -1234,13 +1216,12 @@ func prepareMetrics(targetContext *targetContext, localTempDir string, channelEr
return
}
// load metadata
_ = statusUpdate(myTarget.GetName(), "collecting metadata")
var err error
skipSystemSummary := flagNoSystemSummary
if flagLive {
skipSystemSummary = true // no system summary when live, it doesn't get used/printed
}
if targetContext.metadata, err = LoadMetadata(myTarget, flagNoRoot, skipSystemSummary, targetContext.perfPath, localTempDir); err != nil {
if targetContext.metadata, err = LoadMetadata(myTarget, flagNoRoot, skipSystemSummary, localTempDir, statusUpdate); err != nil {
_ = statusUpdate(myTarget.GetName(), fmt.Sprintf("Error: %s", err.Error()))
targetContext.err = err
channelError <- targetError{target: myTarget, err: err}
Expand Down Expand Up @@ -1379,8 +1360,7 @@ func collectOnTarget(targetContext *targetContext, localTempDir string, localOut
break
}
}
var perfCommand *exec.Cmd
perfCommand, err = getPerfCommand(targetContext.perfPath, targetContext.groupDefinitions, pids, cids, flagCpuRange)
perfCommand, err := getPerfCommand(targetContext.groupDefinitions, pids, cids, flagCpuRange)
if err != nil {
err = fmt.Errorf("failed to get perf command: %w", err)
_ = statusUpdate(myTarget.GetName(), fmt.Sprintf("Error: %s", err.Error()))
Expand Down Expand Up @@ -1417,9 +1397,8 @@ func collectOnTarget(targetContext *targetContext, localTempDir string, localOut
// until perf stops. When collecting for cgroups, perf will be manually terminated if/when the
// run duration exceeds the collection time or the time when the cgroup list needs
// to be refreshed.
func runPerf(myTarget target.Target, noRoot bool, processes []Process, cmd *exec.Cmd, eventGroupDefinitions []GroupDefinition, metricDefinitions []MetricDefinition, metadata Metadata, localTempDir string, outputDir string, frameChannel chan []MetricFrame, errorChannel chan error, signalMgr *signalManager) {
func runPerf(myTarget target.Target, noRoot bool, processes []Process, perfCommand string, eventGroupDefinitions []GroupDefinition, metricDefinitions []MetricDefinition, metadata Metadata, localTempDir string, outputDir string, frameChannel chan []MetricFrame, errorChannel chan error, signalMgr *signalManager) {
// start perf
perfCommand := strings.Join(cmd.Args, " ")
stdoutChannel := make(chan []byte)
stderrChannel := make(chan []byte)
exitcodeChannel := make(chan int)
Expand Down
93 changes: 20 additions & 73 deletions cmd/metrics/perf.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,32 @@ package metrics

import (
"fmt"
"log/slog"
"os/exec"
"path"
"perfspect/internal/script"
"perfspect/internal/target"
"perfspect/internal/util"
"strings"
)

// extractPerf extracts the perf binary from the resources to the local temporary directory.
func extractPerf(myTarget target.Target, localTempDir string) (string, error) {
// get the target architecture
arch, err := myTarget.GetArchitecture()
if err != nil {
return "", fmt.Errorf("failed to get target architecture: %w", err)
// getPerfCommand is responsible for assembling the command that will be
// executed to collect event data
func getPerfCommand(eventGroups []GroupDefinition, pids []string, cids []string, cpuRange string) (string, error) {
var duration int
switch flagScope {
case scopeSystem:
duration = flagDuration
case scopeProcess:
if flagDuration > 0 {
duration = flagDuration
} else if len(flagPidList) == 0 { // don't refresh if PIDs are specified
duration = flagRefresh // refresh hot processes every flagRefresh seconds
}
case scopeCgroup:
duration = 0
}
// extract the perf binary
return util.ExtractResource(script.Resources, path.Join("resources", arch, "perf"), localTempDir)
}

// getPerfPath determines the path to the `perf` binary for the given target.
// If the target is a local target, it uses the provided localPerfPath.
// If the target is remote, it checks if `perf` version 6.1 or later is available on the target.
// If available, it uses the `perf` binary on the target.
// If not available, it pushes the local `perf` binary to the target's temporary directory and uses that.
//
// Parameters:
// - myTarget: The target system where the `perf` binary is needed.
// - localPerfPath: The local path to the `perf` binary.
//
// Returns:
// - perfPath: The path to the `perf` binary on the target.
// - err: An error if any occurred during the process.
func getPerfPath(myTarget target.Target, localPerfPath string) (string, error) {
if localPerfPath == "" {
slog.Error("local perf path is empty, cannot determine perf path")
return "", fmt.Errorf("local perf path is empty")
}
// local target
if _, ok := myTarget.(*target.LocalTarget); ok {
return localPerfPath, nil
}
// remote target
targetTempDir := myTarget.GetTempDirectory()
if targetTempDir == "" {
slog.Error("target temporary directory is empty for remote target", slog.String("target", myTarget.GetName()))
return "", fmt.Errorf("target temporary directory is empty for remote target %s", myTarget.GetName())
}
if err := myTarget.PushFile(localPerfPath, targetTempDir); err != nil {
slog.Error("failed to push perf binary to remote directory", slog.String("error", err.Error()))
return "", fmt.Errorf("failed to push perf binary to remote directory %s: %w", targetTempDir, err)
args, err := getPerfCommandArgs(pids, cids, duration, eventGroups, cpuRange)
if err != nil {
err = fmt.Errorf("failed to assemble perf args: %v", err)
return "", err
}
return path.Join(targetTempDir, "perf"), nil
return strings.Join(append([]string{"perf"}, args...), " "), nil
}

// getPerfCommandArgs returns the command arguments for the 'perf stat' command
Expand Down Expand Up @@ -120,29 +93,3 @@ func getPerfCommandArgs(pids []string, cgroups []string, timeout int, eventGroup
}
return
}

// getPerfCommand is responsible for assembling the command that will be
// executed to collect event data
func getPerfCommand(perfPath string, eventGroups []GroupDefinition, pids []string, cids []string, cpuRange string) (*exec.Cmd, error) {
var duration int
switch flagScope {
case scopeSystem:
duration = flagDuration
case scopeProcess:
if flagDuration > 0 {
duration = flagDuration
} else if len(flagPidList) == 0 { // don't refresh if PIDs are specified
duration = flagRefresh // refresh hot processes every flagRefresh seconds
}
case scopeCgroup:
duration = 0
}

args, err := getPerfCommandArgs(pids, cids, duration, eventGroups, cpuRange)
if err != nil {
err = fmt.Errorf("failed to assemble perf args: %v", err)
return nil, err
}
perfCommand := exec.Command(perfPath, args...) // #nosec G204 // nosemgrep
return perfCommand, nil
}