From 3711af3761b6aae006e894ba194d1b0b6718973d Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Mon, 27 Jan 2025 17:48:49 -0800 Subject: [PATCH 1/3] add instruction mix reporting to telemetry command --- cmd/flame/flame.go | 4 +- cmd/lock/lock.go | 4 +- cmd/report/report.go | 2 +- cmd/telemetry/telemetry.go | 50 ++++++++++++------- internal/common/common.go | 9 ++-- internal/report/html.go | 49 ++++++++++++++++--- internal/report/table_defs.go | 56 ++++++++++++++++++++++ internal/script/script_defs.go | 88 ++++++++++++++++++++++++---------- tools/Makefile | 13 ++++- tools/build.Dockerfile | 1 + 10 files changed, 216 insertions(+), 60 deletions(-) diff --git a/cmd/flame/flame.go b/cmd/flame/flame.go index 04b441aa..426fae9e 100644 --- a/cmd/flame/flame.go +++ b/cmd/flame/flame.go @@ -9,6 +9,7 @@ import ( "os" "perfspect/internal/common" "perfspect/internal/report" + "perfspect/internal/script" "perfspect/internal/util" "strings" @@ -138,8 +139,7 @@ func runCmd(cmd *cobra.Command, args []string) error { reportingCommand := common.ReportingCommand{ Cmd: cmd, ReportNamePost: "flame", - Frequency: flagFrequency, - Duration: flagDuration, + ScriptParams: script.ScriptParams{Frequency: flagFrequency, Duration: flagDuration}, TableNames: []string{report.CodePathFrequencyTableName}, } return reportingCommand.Run() diff --git a/cmd/lock/lock.go b/cmd/lock/lock.go index 26fc648f..5328d157 100755 --- a/cmd/lock/lock.go +++ b/cmd/lock/lock.go @@ -9,6 +9,7 @@ import ( "os" "perfspect/internal/common" "perfspect/internal/report" + "perfspect/internal/script" "strings" "github.com/spf13/cobra" @@ -115,8 +116,7 @@ func runCmd(cmd *cobra.Command, args []string) error { reportingCommand := common.ReportingCommand{ Cmd: cmd, ReportNamePost: "lock", - Frequency: flagFrequency, - Duration: flagDuration, + ScriptParams: script.ScriptParams{Frequency: flagFrequency, Duration: flagDuration}, TableNames: []string{report.KernelLockAnalysisTableName}, } return reportingCommand.Run() diff --git a/cmd/report/report.go b/cmd/report/report.go index e6c009cd..c6224d75 100644 --- a/cmd/report/report.go +++ b/cmd/report/report.go @@ -333,7 +333,7 @@ func runCmd(cmd *cobra.Command, args []string) error { } reportingCommand := common.ReportingCommand{ Cmd: cmd, - StorageDir: flagStorageDir, + ScriptParams: script.ScriptParams{StorageDir: flagStorageDir}, TableNames: tableNames, SummaryFunc: summaryFunc, SummaryTableName: benchmarkSummaryTableName, diff --git a/cmd/telemetry/telemetry.go b/cmd/telemetry/telemetry.go index 2f00ace6..6fff757a 100644 --- a/cmd/telemetry/telemetry.go +++ b/cmd/telemetry/telemetry.go @@ -50,13 +50,17 @@ var ( flagAll bool - flagCpu bool - flagCpuAvg bool - flagIrq bool - flagNetwork bool - flagStorage bool - flagMemory bool - flagPower bool + flagCpu bool + flagCpuAvg bool + flagIrq bool + flagNetwork bool + flagStorage bool + flagMemory bool + flagPower bool + flagInstrMix bool + + flagInstrMixPid int + flagInstrMixFilter []string ) const ( @@ -65,13 +69,17 @@ const ( flagAllName = "all" - flagCpuName = "cpu" - flagCpuAvgName = "cpuavg" - flagIrqName = "irq" - flagNetworkName = "network" - flagStorageName = "storage" - flagMemoryName = "memory" - flagPowerName = "power" + flagCpuName = "cpu" + flagCpuAvgName = "cpuavg" + flagIrqName = "irq" + flagNetworkName = "network" + flagStorageName = "storage" + flagMemoryName = "memory" + flagPowerName = "power" + flagInstrMixName = "instrmix" + + flagInstrMixPidName = "instrmix-pid" + flagInstrMixFilterName = "instrmix-filter" ) var telemetrySummaryTableName = "Telemetry Summary" @@ -79,6 +87,7 @@ var telemetrySummaryTableName = "Telemetry Summary" var categories = []common.Category{ {FlagName: flagCpuName, FlagVar: &flagCpu, DefaultValue: false, Help: "monitor cpu", TableNames: []string{report.CPUUtilizationTableName}}, {FlagName: flagCpuAvgName, FlagVar: &flagCpuAvg, DefaultValue: false, Help: "monitor cpu average", TableNames: []string{report.AverageCPUUtilizationTableName}}, + {FlagName: flagInstrMixName, FlagVar: &flagInstrMix, DefaultValue: false, Help: "monitor instruction mix", TableNames: []string{report.InstructionMixTableName}}, {FlagName: flagIrqName, FlagVar: &flagIrq, DefaultValue: false, Help: "monitor irq", TableNames: []string{report.IRQRateTableName}}, {FlagName: flagStorageName, FlagVar: &flagStorage, DefaultValue: false, Help: "monitor storage", TableNames: []string{report.DriveStatsTableName}}, {FlagName: flagNetworkName, FlagVar: &flagNetwork, DefaultValue: false, Help: "monitor network", TableNames: []string{report.NetworkStatsTableName}}, @@ -96,6 +105,8 @@ func init() { Cmd.Flags().StringSliceVar(&common.FlagFormat, common.FlagFormatName, []string{report.FormatAll}, "") Cmd.Flags().IntVar(&flagDuration, flagDurationName, 30, "") Cmd.Flags().IntVar(&flagInterval, flagIntervalName, 2, "") + Cmd.Flags().IntVar(&flagInstrMixPid, flagInstrMixPidName, 0, "") + Cmd.Flags().StringSliceVar(&flagInstrMixFilter, flagInstrMixFilterName, []string{"SSE", "AVX", "AVX2", "AVX512", "AMX_TILE"}, "") common.AddTargetFlags(Cmd) @@ -158,6 +169,14 @@ func getFlagGroups() []common.FlagGroup { Name: flagIntervalName, Help: "number of seconds between each sample", }, + { + Name: flagInstrMixPidName, + Help: "pid to monitor for instruction mix, no pid means all processes", + }, + { + Name: flagInstrMixFilterName, + Help: "filter to apply to instruction mix", + }, } groups = append(groups, common.FlagGroup{ GroupName: "Others Options", @@ -229,8 +248,7 @@ func runCmd(cmd *cobra.Command, args []string) error { reportingCommand := common.ReportingCommand{ Cmd: cmd, ReportNamePost: "telem", - Interval: flagInterval, - Duration: flagDuration, + ScriptParams: script.ScriptParams{Interval: flagInterval, Duration: flagDuration, PID: flagInstrMixPid, Filter: flagInstrMixFilter}, TableNames: tableNames, SummaryFunc: summaryFunc, SummaryTableName: telemetrySummaryTableName, diff --git a/internal/common/common.go b/internal/common/common.go index f74b6a44..aa02632c 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -80,10 +80,7 @@ type ReportingCommand struct { Cmd *cobra.Command ReportNamePost string TableNames []string - Duration int - Interval int - Frequency int - StorageDir string + ScriptParams script.ScriptParams SummaryFunc SummaryFunc SummaryTableName string InsightsFunc InsightsFunc @@ -138,7 +135,7 @@ func (rc *ReportingCommand) Run() error { // make a list of unique script definitions var scriptsToRun []script.ScriptDefinition for _, scriptName := range scriptNames { - scriptsToRun = append(scriptsToRun, script.GetParameterizedScriptByName(scriptName, rc.Duration, rc.Interval, rc.Frequency, rc.StorageDir)) + scriptsToRun = append(scriptsToRun, script.GetParameterizedScriptByName(scriptName, rc.ScriptParams)) } // do any of the scripts require elevated privileges? elevated := false @@ -188,7 +185,7 @@ func (rc *ReportingCommand) Run() error { channelTargetScriptOutputs := make(chan TargetScriptOutputs) channelError := make(chan error) for _, target := range myTargets { - go collectOnTarget(rc.Cmd, rc.Duration, target, scriptsToRun, localTempDir, channelTargetScriptOutputs, channelError, multiSpinner.Status) + go collectOnTarget(rc.Cmd, rc.ScriptParams.Duration, target, scriptsToRun, localTempDir, channelTargetScriptOutputs, channelError, multiSpinner.Status) } // wait for scripts to run on all targets var allTargetScriptOutputs []TargetScriptOutputs diff --git a/internal/report/html.go b/internal/report/html.go index 3290b705..e7a675c1 100644 --- a/internal/report/html.go +++ b/internal/report/html.go @@ -743,7 +743,7 @@ func cpuUtilizationTableHTMLRenderer(tableValues TableValues, targetName string) } chartConfig := scatterChartTemplateStruct{ ID: fmt.Sprintf("cpuUtilization%d", rand.Intn(10000)), - XaxisText: "Time/Samples", + XaxisText: "Time", YaxisText: "% Utilization", TitleText: "", DisplayTitle: "false", @@ -778,7 +778,7 @@ func averageCPUUtilizationTableHTMLRenderer(tableValues TableValues, targetName } chartConfig := scatterChartTemplateStruct{ ID: fmt.Sprintf("avgCPUUtil%d", rand.Intn(10000)), - XaxisText: "Time/Samples", + XaxisText: "Time", YaxisText: "% Utilization", TitleText: "", DisplayTitle: "false", @@ -819,7 +819,7 @@ func irqRateTableHTMLRenderer(tableValues TableValues, targetName string) string } chartConfig := scatterChartTemplateStruct{ ID: fmt.Sprintf("irqRate%d", rand.Intn(10000)), - XaxisText: "Time/Samples", + XaxisText: "Time", YaxisText: "IRQ/s", TitleText: "", DisplayTitle: "false", @@ -875,7 +875,7 @@ func driveStatsTableHTMLRenderer(tableValues TableValues, targetName string) str } chartConfig := scatterChartTemplateStruct{ ID: fmt.Sprintf("driveStats%d", rand.Intn(10000)), - XaxisText: "Time/Samples", + XaxisText: "Time", YaxisText: "", TitleText: drive, DisplayTitle: "true", @@ -933,7 +933,7 @@ func networkStatsTableHTMLRenderer(tableValues TableValues, targetName string) s } chartConfig := scatterChartTemplateStruct{ ID: fmt.Sprintf("nicStats%d", rand.Intn(10000)), - XaxisText: "Time/Samples", + XaxisText: "Time", YaxisText: "", TitleText: nic, DisplayTitle: "true", @@ -970,7 +970,7 @@ func memoryStatsTableHTMLRenderer(tableValues TableValues, targetName string) st } chartConfig := scatterChartTemplateStruct{ ID: fmt.Sprintf("memoryStats%d", rand.Intn(10000)), - XaxisText: "Time/Samples", + XaxisText: "Time", YaxisText: "kilobytes", TitleText: "", DisplayTitle: "false", @@ -1005,7 +1005,7 @@ func powerStatsTableHTMLRenderer(tableValues TableValues, targetName string) str } chartConfig := scatterChartTemplateStruct{ ID: fmt.Sprintf("powerStats%d", rand.Intn(10000)), - XaxisText: "Time/Samples", + XaxisText: "Time", YaxisText: "Watts", TitleText: "", DisplayTitle: "false", @@ -1061,3 +1061,38 @@ func kernelLockAnalysisHTMLRenderer(tableValues TableValues, targetName string) } return renderHTMLTable([]string{}, values, "pure-table pure-table-striped", tableValueStyles) } + +func instructionMixTableHTMLRenderer(tableValues TableValues, targetname string) string { + data := [][]scatterPoint{} + datasetNames := []string{} + for _, field := range tableValues.Fields { + points := []scatterPoint{} + for i, val := range field.Values { + if val == "" { + break + } + stat, err := strconv.ParseFloat(val, 64) + if err != nil { + slog.Error("error parsing stat", slog.String("error", err.Error())) + return "" + } + points = append(points, scatterPoint{float64(i), stat}) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, field.Name) + } + } + chartConfig := scatterChartTemplateStruct{ + ID: fmt.Sprintf("instrMix%d", rand.Intn(10000)), + XaxisText: "Time", + YaxisText: "% Samples", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + return renderScatterChart(data, datasetNames, chartConfig) +} diff --git a/internal/report/table_defs.go b/internal/report/table_defs.go index 52a06372..1be52a96 100644 --- a/internal/report/table_defs.go +++ b/internal/report/table_defs.go @@ -6,6 +6,7 @@ package report // table_defs.go defines the tables used for generating reports import ( + "encoding/csv" "fmt" "log/slog" "math" @@ -118,6 +119,8 @@ const ( CodePathFrequencyTableName = "Code Path Frequency" // lock table names KernelLockAnalysisTableName = "Kernel Lock Analysis" + // process watch instruction mix table names + InstructionMixTableName = "Instruction Mix" ) const ( @@ -612,6 +615,16 @@ var tableDefinitions = map[string]TableDefinition{ }, FieldsFunc: powerStatsTableValues, HTMLTableRendererFunc: powerStatsTableHTMLRenderer}, + InstructionMixTableName: { + Name: InstructionMixTableName, + MenuLabel: InstructionMixTableName, + HasRows: true, + ScriptNames: []string{ + script.InstructionMixScriptName, + }, + FieldsFunc: instructionMixTableValues, + HTMLTableRendererFunc: instructionMixTableHTMLRenderer, + }, // // flamegraph tables // @@ -1937,3 +1950,46 @@ func kernelLockAnalysisTableValues(outputs map[string]script.ScriptOutput) []Fie } return fields } + +func instructionMixTableValues(outputs map[string]script.ScriptOutput) []Field { + // parse the CSV output + csvOutput := outputs[script.InstructionMixScriptName].Stdout + r := csv.NewReader(strings.NewReader(csvOutput)) + rows, err := r.ReadAll() + if err != nil { + slog.Error(err.Error()) + return []Field{} + } + if len(rows) < 2 { + slog.Error("instruction mix output is not in expected format") + return []Field{} + } + fields := []Field{} + // first row is the header, extract field names, skip the first three fields (interval, pid, name) + if len(rows[0]) < 3 { + slog.Error("instruction mix output is not in expected format") + return []Field{} + } + for _, field := range rows[0][3:] { + fields = append(fields, Field{Name: field}) + } + sample := -1 + // values start in 2nd row, we're only interested in the first row of the sample + for _, row := range rows[1:] { + if len(row) < 3+len(fields) { + continue + } + rowSample, err := strconv.Atoi(row[0]) + if err != nil { + slog.Error(fmt.Sprintf("unable to convert instruction mix sample to int: %s", row[0])) + continue + } + if rowSample != sample { // new sample + sample = rowSample + for i := range fields { + fields[i].Values = append(fields[i].Values, row[i+3]) + } + } + } + return fields +} diff --git a/internal/script/script_defs.go b/internal/script/script_defs.go index fb5ff21b..474882d7 100644 --- a/internal/script/script_defs.go +++ b/internal/script/script_defs.go @@ -105,6 +105,7 @@ const ( GaudiInfoScriptName = "gaudi info" GaudiFirmwareScriptName = "gaudi firmware" GaudiNumaScriptName = "gaudi numa" + InstructionMixScriptName = "instruction mix" ) const ( @@ -113,12 +114,21 @@ const ( // GetScriptByName returns the script definition with the given name. It will panic if the script is not found. func GetScriptByName(name string) ScriptDefinition { - return GetParameterizedScriptByName(name, 0, 0, 0, "") + return GetParameterizedScriptByName(name, ScriptParams{}) +} + +type ScriptParams struct { + Duration int + Interval int + Frequency int + StorageDir string + PID int + Filter []string } // GetParameterizedScriptByName returns the script definition with the given name. It will panic if the script is not found. -func GetParameterizedScriptByName(name string, duration int, interval int, frequency int, fioDir string) ScriptDefinition { - for _, script := range getCollectionScripts(duration, interval, frequency, fioDir) { +func GetParameterizedScriptByName(name string, params ScriptParams) ScriptDefinition { + for _, script := range getCollectionScripts(params) { if script.Name == name { return script } @@ -127,7 +137,7 @@ func GetParameterizedScriptByName(name string, duration int, interval int, frequ } // getCollectionScripts returns the script definitions that are used to collect information from the target system. -func getCollectionScripts(duration, interval int, frequency int, fioDir string) (scripts []ScriptDefinition) { +func getCollectionScripts(params ScriptParams) (scripts []ScriptDefinition) { // script definitions scripts = []ScriptDefinition{ @@ -877,7 +887,7 @@ fio --name=bandwidth --directory=$test_dir --numjobs=$numjobs \ --direct=1 --verify=0 --bs=1M --iodepth=64 --rw=rw \ --group_reporting=1 --iodepth_batch_submit=64 \ --iodepth_batch_complete_max=64 -rm -rf $test_dir`, fioDir) +rm -rf $test_dir`, params.StorageDir) }(), Superuser: true, Sequential: true, @@ -888,11 +898,11 @@ rm -rf $test_dir`, fioDir) Name: MpstatScriptName, Script: func() string { var count string - if duration != 0 && interval != 0 { - countInt := duration / interval + if params.Duration != 0 && params.Interval != 0 { + countInt := params.Duration / params.Interval count = strconv.Itoa(countInt) } - return fmt.Sprintf(`mpstat -u -T -I SCPU -P ALL %d %s`, interval, count) + return fmt.Sprintf(`mpstat -u -T -I SCPU -P ALL %d %s`, params.Interval, count) }(), Superuser: true, Lkms: []string{}, @@ -902,11 +912,11 @@ rm -rf $test_dir`, fioDir) Name: IostatScriptName, Script: func() string { var count string - if duration != 0 && interval != 0 { - countInt := duration / interval + if params.Duration != 0 && params.Interval != 0 { + countInt := params.Duration / params.Interval count = strconv.Itoa(countInt) } - return fmt.Sprintf(`S_TIME_FORMAT=ISO iostat -d -t %d %s | sed '/^loop/d'`, interval, count) + return fmt.Sprintf(`S_TIME_FORMAT=ISO iostat -d -t %d %s | sed '/^loop/d'`, params.Interval, count) }(), Superuser: true, Lkms: []string{}, @@ -916,11 +926,11 @@ rm -rf $test_dir`, fioDir) Name: SarMemoryScriptName, Script: func() string { var count string - if duration != 0 && interval != 0 { - countInt := duration / interval + if params.Duration != 0 && params.Interval != 0 { + countInt := params.Duration / params.Interval count = strconv.Itoa(countInt) } - return fmt.Sprintf(`sar -r %d %s`, interval, count) + return fmt.Sprintf(`sar -r %d %s`, params.Interval, count) }(), Superuser: true, Lkms: []string{}, @@ -930,11 +940,11 @@ rm -rf $test_dir`, fioDir) Name: SarNetworkScriptName, Script: func() string { var count string - if duration != 0 && interval != 0 { - countInt := duration / interval + if params.Duration != 0 && params.Interval != 0 { + countInt := params.Duration / params.Interval count = strconv.Itoa(countInt) } - return fmt.Sprintf(`sar -n DEV %d %s`, interval, count) + return fmt.Sprintf(`sar -n DEV %d %s`, params.Interval, count) }(), Superuser: true, Lkms: []string{}, @@ -944,11 +954,11 @@ rm -rf $test_dir`, fioDir) Name: TurbostatScriptName, Script: func() string { var count string - if duration != 0 && interval != 0 { - countInt := duration / interval + if params.Duration != 0 && params.Interval != 0 { + countInt := params.Duration / params.Interval count = "-n " + strconv.Itoa(countInt) } - return fmt.Sprintf(`turbostat -S -s PkgWatt,RAMWatt -q -i %d %s`, interval, count) + ` | awk '{ print strftime("%H:%M:%S"), $0 }'` + return fmt.Sprintf(`turbostat -S -s PkgWatt,RAMWatt -q -i %d %s`, params.Interval, count) + ` | awk '{ print strftime("%H:%M:%S"), $0 }'` }(), Superuser: true, Lkms: []string{"msr"}, @@ -960,8 +970,8 @@ rm -rf $test_dir`, fioDir) Name: ProfileJavaScriptName, Script: func() string { apInterval := 0 - if frequency > 0 { - apInterval = int(1 / float64(frequency) * 1000000000) + if params.Frequency > 0 { + apInterval = int(1 / float64(params.Frequency) * 1000000000) } return fmt.Sprintf(`# JAVA app call stack collection (run in background) ap_interval=%d @@ -985,7 +995,7 @@ for idx in "${!java_pids[@]}"; do echo "########## async-profiler $pid $cmd ##########" async-profiler/profiler.sh stop -o collapsed "$pid" done -`, apInterval, duration) +`, apInterval, params.Duration) }(), Superuser: true, Depends: []string{"async-profiler"}, @@ -1024,7 +1034,7 @@ if [ -f "perf_fp.folded" ]; then echo "########## perf_fp ##########" cat perf_fp.folded fi -`, frequency, duration) +`, params.Frequency, params.Duration) }(), Superuser: true, Depends: []string{"perf", "stackcollapse-perf.pl"}, @@ -1083,11 +1093,39 @@ if [ -f "perf_lock_contention.txt" ]; then echo "########## perf_lock_contention ##########" cat perf_lock_contention.txt fi -`, frequency, duration) +`, params.Frequency, params.Duration) }(), Superuser: true, Depends: []string{"perf"}, }, + { + Name: InstructionMixScriptName, + Script: func() string { + scriptParts := []string{ + "processwatch -c", + } + // if no PID specified, increase the sampling interval (defaults to 10,000) to reduce overhead + if params.PID == 0 { + scriptParts = append(scriptParts, fmt.Sprintf("-s %d", 1000000)) + } else { + scriptParts = append(scriptParts, fmt.Sprintf("-p %d", params.PID)) + } + for _, cat := range params.Filter { + scriptParts = append(scriptParts, fmt.Sprintf("-f %s", cat)) + } + if params.Duration != 0 && params.Interval != 0 { + count := params.Duration / params.Interval + scriptParts = append(scriptParts, fmt.Sprintf("-n %d", count)) + } + if params.Interval != 0 { + scriptParts = append(scriptParts, fmt.Sprintf("-i %d", params.Interval)) + } + return strings.Join(scriptParts, " ") + }(), + Superuser: true, + Lkms: []string{"msr"}, + Depends: []string{"processwatch"}, + }, } // validate script definitions diff --git a/tools/Makefile b/tools/Makefile index b373ad98..407e19e7 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -5,7 +5,7 @@ # default: tools -.PHONY: default tools async-profiler avx-turbo cpuid dmidecode ethtool fio flamegraph ipmitool lshw lspci msr-tools pcm perf spectre-meltdown-checker sshpass stress-ng sysstat tsc turbostat +.PHONY: default tools async-profiler avx-turbo cpuid dmidecode ethtool fio flamegraph ipmitool lshw lspci msr-tools pcm perf processwatch spectre-meltdown-checker sshpass stress-ng sysstat tsc turbostat tools: async-profiler avx-turbo cpuid dmidecode ethtool fio flamegraph ipmitool lshw lspci msr-tools pcm spectre-meltdown-checker sshpass stress-ng sysstat tsc turbostat mkdir -p bin @@ -163,6 +163,17 @@ perf: cp linux_perf/tools/perf/perf bin/ strip --strip-unneeded bin/perf +processwatch: +ifeq ("$(wildcard processwatch)","") + git clone --recursive https://github.com/intel/processwatch.git +else + cd processwatch && git checkout master && git pull +endif + cd processwatch && ./build.sh + mkdir -p bin + cp processwatch/processwatch bin/ + strip --strip-unneeded bin/processwatch + spectre-meltdown-checker: ifeq ("$(wildcard spectre-meltdown-checker)","") git clone https://github.com/speed47/spectre-meltdown-checker.git diff --git a/tools/build.Dockerfile b/tools/build.Dockerfile index dd1e975b..8be4ce6e 100644 --- a/tools/build.Dockerfile +++ b/tools/build.Dockerfile @@ -52,6 +52,7 @@ RUN mkdir workdir ADD . /workdir WORKDIR /workdir RUN make perf +RUN make processwatch FROM scratch AS output COPY --from=builder workdir/bin /bin From a9aede380d6aa16c897a019e501568e45e5f8a82 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Tue, 28 Jan 2025 17:38:10 -0800 Subject: [PATCH 2/3] lock to processwatch commit --- internal/script/script_defs.go | 2 +- tools/Makefile | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/script/script_defs.go b/internal/script/script_defs.go index 474882d7..2fcb894b 100644 --- a/internal/script/script_defs.go +++ b/internal/script/script_defs.go @@ -1104,7 +1104,7 @@ fi scriptParts := []string{ "processwatch -c", } - // if no PID specified, increase the sampling interval (defaults to 10,000) to reduce overhead + // if no PID specified, increase the sampling interval (defaults to 100,000) to reduce overhead if params.PID == 0 { scriptParts = append(scriptParts, fmt.Sprintf("-s %d", 1000000)) } else { diff --git a/tools/Makefile b/tools/Makefile index 407e19e7..9db0b4fb 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -167,8 +167,9 @@ processwatch: ifeq ("$(wildcard processwatch)","") git clone --recursive https://github.com/intel/processwatch.git else - cd processwatch && git checkout master && git pull + cd processwatch && git checkout main && git pull endif + cd processwatch && git checkout c394065 # this commit id has been tested cd processwatch && ./build.sh mkdir -p bin cp processwatch/processwatch bin/ From 6b94eee81034e242587b3e2bd5e18ff1e15fa4cf Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Wed, 29 Jan 2025 12:41:11 -0800 Subject: [PATCH 3/3] add timestamp --- internal/report/html.go | 2 +- internal/report/table_defs.go | 47 +++++++++++++++++++++++++++++++--- internal/script/script_defs.go | 3 ++- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/internal/report/html.go b/internal/report/html.go index e7a675c1..3a67172f 100644 --- a/internal/report/html.go +++ b/internal/report/html.go @@ -1065,7 +1065,7 @@ func kernelLockAnalysisHTMLRenderer(tableValues TableValues, targetName string) func instructionMixTableHTMLRenderer(tableValues TableValues, targetname string) string { data := [][]scatterPoint{} datasetNames := []string{} - for _, field := range tableValues.Fields { + for _, field := range tableValues.Fields[1:] { points := []scatterPoint{} for i, val := range field.Values { if val == "" { diff --git a/internal/report/table_defs.go b/internal/report/table_defs.go index 1be52a96..e243e25f 100644 --- a/internal/report/table_defs.go +++ b/internal/report/table_defs.go @@ -13,6 +13,7 @@ import ( "regexp" "strconv" "strings" + "time" "perfspect/internal/cpudb" "perfspect/internal/script" @@ -1952,8 +1953,42 @@ func kernelLockAnalysisTableValues(outputs map[string]script.ScriptOutput) []Fie } func instructionMixTableValues(outputs map[string]script.ScriptOutput) []Field { + // first two lines are not part of the CSV output, they are the start time and interval + var startTime time.Time + var interval int + for i, line := range strings.Split(outputs[script.InstructionMixScriptName].Stdout, "\n") { + if i == 0 { + if !strings.HasPrefix(line, "TIME") { + slog.Error("instruction mix output is not in expected format, missing TIME") + return []Field{} + } else { + val := strings.Split(line, " ")[1] + var err error + startTime, err = time.Parse("15:04:05", val) + if err != nil { + slog.Error(fmt.Sprintf("unable to parse instruction mix start time: %s", val)) + return []Field{} + } + } + } else if i == 1 { + if !strings.HasPrefix(line, "INTERVAL") { + slog.Error("instruction mix output is not in expected format, missing INTERVAL") + return []Field{} + } else { + val := strings.Split(line, " ")[1] + var err error + interval, err = strconv.Atoi(val) + if err != nil { + slog.Error(fmt.Sprintf("unable to convert instruction mix interval to int: %s", val)) + return []Field{} + } + } + } else { + break + } + } // parse the CSV output - csvOutput := outputs[script.InstructionMixScriptName].Stdout + csvOutput := strings.Join(strings.Split(outputs[script.InstructionMixScriptName].Stdout, "\n")[2:], "\n") r := csv.NewReader(strings.NewReader(csvOutput)) rows, err := r.ReadAll() if err != nil { @@ -1964,7 +1999,7 @@ func instructionMixTableValues(outputs map[string]script.ScriptOutput) []Field { slog.Error("instruction mix output is not in expected format") return []Field{} } - fields := []Field{} + fields := []Field{{Name: "Time"}} // first row is the header, extract field names, skip the first three fields (interval, pid, name) if len(rows[0]) < 3 { slog.Error("instruction mix output is not in expected format") @@ -1976,7 +2011,7 @@ func instructionMixTableValues(outputs map[string]script.ScriptOutput) []Field { sample := -1 // values start in 2nd row, we're only interested in the first row of the sample for _, row := range rows[1:] { - if len(row) < 3+len(fields) { + if len(row) < 2+len(fields) { continue } rowSample, err := strconv.Atoi(row[0]) @@ -1987,7 +2022,11 @@ func instructionMixTableValues(outputs map[string]script.ScriptOutput) []Field { if rowSample != sample { // new sample sample = rowSample for i := range fields { - fields[i].Values = append(fields[i].Values, row[i+3]) + if i == 0 { + fields[i].Values = append(fields[i].Values, startTime.Add(time.Duration(sample*interval)*time.Second).Format("15:04:05")) + } else { + fields[i].Values = append(fields[i].Values, row[i+2]) + } } } } diff --git a/internal/script/script_defs.go b/internal/script/script_defs.go index 2fcb894b..617301d8 100644 --- a/internal/script/script_defs.go +++ b/internal/script/script_defs.go @@ -1101,6 +1101,7 @@ fi { Name: InstructionMixScriptName, Script: func() string { + script := fmt.Sprintf("echo TIME: $(date +\"%%H:%%M:%%S\")\necho INTERVAL: %d\n", params.Interval) scriptParts := []string{ "processwatch -c", } @@ -1120,7 +1121,7 @@ fi if params.Interval != 0 { scriptParts = append(scriptParts, fmt.Sprintf("-i %d", params.Interval)) } - return strings.Join(scriptParts, " ") + return script + strings.Join(scriptParts, " ") }(), Superuser: true, Lkms: []string{"msr"},