diff --git a/cmd/metrics/metadata.go b/cmd/metrics/metadata.go index 28722288..882dff59 100644 --- a/cmd/metrics/metadata.go +++ b/cmd/metrics/metadata.go @@ -19,6 +19,7 @@ import ( "time" "perfspect/internal/cpus" + "perfspect/internal/progress" "perfspect/internal/report" "perfspect/internal/script" "perfspect/internal/target" @@ -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) @@ -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) { @@ -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 @@ -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) } @@ -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() @@ -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) } @@ -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{ { @@ -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", @@ -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", diff --git a/cmd/metrics/metrics.go b/cmd/metrics/metrics.go index f17f1d63..dbfc7dee 100644 --- a/cmd/metrics/metrics.go +++ b/cmd/metrics/metrics.go @@ -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 @@ -974,14 +973,6 @@ 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 @@ -989,7 +980,7 @@ func runCmd(cmd *cobra.Command, args []string) error { 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 @@ -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") @@ -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} } @@ -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} @@ -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())) @@ -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) diff --git a/cmd/metrics/perf.go b/cmd/metrics/perf.go index b7df55f8..5d0c452d 100644 --- a/cmd/metrics/perf.go +++ b/cmd/metrics/perf.go @@ -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 @@ -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 -}