diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 96fc9e8b..050676d5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,7 +6,7 @@ PerfSpect is a performance analysis tool for Linux systems written in Go. It pro - `metrics`: Collects CPU performance metrics using hardware performance counters - `report`: Generates system configuration and health (performance) from collected data - `telemetry`: Gathers system telemetry data -- `flame`: Creates CPU flamegraphs +- `flamegraph`: Creates CPU flamegraphs - `lock`: Analyzes lock contention - `config`: Modifies system configuration for performance tuning @@ -15,7 +15,7 @@ The tool can target both local and remote systems via SSH. ## Project Structure - `main.go` - Application entry point -- `cmd/` - Command implementations (metrics, report, telemetry, flame, lock, config) +- `cmd/` - Command implementations (metrics, report, telemetry, flamegraph, lock, config) - `internal/` - Internal packages (common, cpus, progress, report, script, table, target, util) - `internal/common/` - Shared types, functions, and workflows for commands - `internal/target/` - Abstraction for local and remote target systems diff --git a/README.md b/README.md index 7e312dfd..365fb781 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Usage: | [`metrics`](#metrics-command) | CPU core and uncore metrics | | [`report`](#report-command) | System configuration and health | | [`telemetry`](#telemetry-command) | System telemetry | -| [`flame`](#flame-command) | Software call-stacks as flamegraphs | +| [`flamegraph`](#flamegraph-command) | Software call-stacks as flamegraphs | | [`lock`](#lock-command) | Software hot spot, cache-to-cache and lock contention | | [`config`](#config-command) | Modify system configuration | @@ -109,10 +109,10 @@ The `telemetry` command reports CPU utilization, instruction mix, disk stats, ne ![screenshot of the CPU utilization chart from the HTML output of the telemetry command](docs/telemetry_html.png) -#### Flame Command -Software flamegraphs are useful in diagnosing software performance bottlenecks. Run `perfspect flame` to capture a system-wide software flamegraph. +#### Flamegraph Command +Software flamegraphs are useful in diagnosing software performance bottlenecks. Run `perfspect flamegraph` to capture a system-wide software flamegraph. -![screenshot of a flame graph from the HTML output of the flame command](docs/flamegraph.png) +![screenshot of a flamegraph from the HTML output of the flamegraph command](docs/flamegraph.png) #### Lock Command As systems contain more and more cores, it can be useful to analyze the Linux kernel lock overhead and potential false-sharing that impacts system scalability. Run `perfspect lock` to collect system-wide hot spot, cache-to-cache and lock contention information. Experienced performance engineers can analyze the collected information to identify bottlenecks. diff --git a/cmd/config/config.go b/cmd/config/config.go index 6981b73a..aafefffd 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -36,7 +36,7 @@ var examples = []string{ var Cmd = &cobra.Command{ Use: cmdName, - Short: "Modify target(s) system configuration", + Short: "Modify system configuration on target(s)", Long: `Sets system configuration items on target platform(s). USE CAUTION! Target may become unstable. It is up to the user to ensure that the requested configuration is valid for the target. There is not an automated way to revert the configuration changes. If all else fails, reboot the target.`, diff --git a/cmd/config/restore.go b/cmd/config/restore.go index 1a7942ff..b6b41035 100644 --- a/cmd/config/restore.go +++ b/cmd/config/restore.go @@ -39,7 +39,7 @@ var restoreExamples = []string{ var RestoreCmd = &cobra.Command{ Use: restoreCmdName + " ", - Short: "Restore system configuration from a previously recorded file", + Short: "Restore system configuration from file", Long: `Restores system configuration from a file that was previously recorded using the --record flag. The restore command will parse the configuration file, validate the settings against the target system, diff --git a/cmd/flame/flame.go b/cmd/flamegraph/flamegraph.go similarity index 95% rename from cmd/flame/flame.go rename to cmd/flamegraph/flamegraph.go index 2637888e..f42c8de7 100644 --- a/cmd/flame/flame.go +++ b/cmd/flamegraph/flamegraph.go @@ -1,5 +1,5 @@ -// Package flame is a subcommand of the root command. It is used to generate flamegraphs from target(s). -package flame +// Package flamegraph is a subcommand of the root command. It is used to generate flamegraphs from target(s). +package flamegraph // Copyright (C) 2021-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause @@ -19,7 +19,7 @@ import ( "github.com/spf13/pflag" ) -const cmdName = "flame" +const cmdName = "flamegraph" var examples = []string{ fmt.Sprintf(" Flamegraph from local host: $ %s %s", common.AppName, cmdName), @@ -29,7 +29,8 @@ var examples = []string{ var Cmd = &cobra.Command{ Use: cmdName, - Short: "Generate flamegraphs from target(s)", + Aliases: []string{"flame"}, + Short: "Collect flamegraph data from target(s)", Long: "", Example: strings.Join(examples, "\n"), RunE: runCmd, diff --git a/cmd/flame/render_html_flamegraph.go b/cmd/flamegraph/flamegraph_renderers.go similarity index 99% rename from cmd/flame/render_html_flamegraph.go rename to cmd/flamegraph/flamegraph_renderers.go index 190fb8b9..8838dd88 100644 --- a/cmd/flame/render_html_flamegraph.go +++ b/cmd/flamegraph/flamegraph_renderers.go @@ -1,4 +1,4 @@ -package flame +package flamegraph // Copyright (C) 2021-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause diff --git a/cmd/flame/flame_tables.go b/cmd/flamegraph/flamegraph_tables.go similarity index 99% rename from cmd/flame/flame_tables.go rename to cmd/flamegraph/flamegraph_tables.go index 4b58a23e..e600bc40 100644 --- a/cmd/flame/flame_tables.go +++ b/cmd/flamegraph/flamegraph_tables.go @@ -1,4 +1,4 @@ -package flame +package flamegraph // Copyright (C) 2021-2025 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause diff --git a/cmd/lock/lock.go b/cmd/lock/lock.go index 25fc8813..ba2ad9e4 100755 --- a/cmd/lock/lock.go +++ b/cmd/lock/lock.go @@ -31,7 +31,7 @@ var examples = []string{ var Cmd = &cobra.Command{ Use: cmdName, - Short: "Collect system information for kernel lock analysis from target(s)", + Short: "Collect kernel lock data from target(s)", Long: "", Example: strings.Join(examples, "\n"), RunE: runCmd, diff --git a/cmd/lock/lock_renderers.go b/cmd/lock/lock_renderers.go new file mode 100644 index 00000000..4df29ca2 --- /dev/null +++ b/cmd/lock/lock_renderers.go @@ -0,0 +1,26 @@ +package lock + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + htmltemplate "html/template" + "perfspect/internal/report" + "perfspect/internal/table" +) + +func kernelLockAnalysisHTMLRenderer(tableValues table.TableValues, targetName string) string { + values := [][]string{} + var tableValueStyles [][]string + for _, field := range tableValues.Fields { + rowValues := []string{} + rowValues = append(rowValues, field.Name) + rowValues = append(rowValues, htmltemplate.HTMLEscapeString(field.Values[0])) + values = append(values, rowValues) + rowStyles := []string{} + rowStyles = append(rowStyles, "font-weight:bold") + rowStyles = append(rowStyles, "white-space: pre-wrap") + tableValueStyles = append(tableValueStyles, rowStyles) + } + return report.RenderHTMLTable([]string{}, values, "pure-table pure-table-striped", tableValueStyles) +} diff --git a/cmd/lock/lock_tables.go b/cmd/lock/lock_tables.go index 72ad4860..11823456 100644 --- a/cmd/lock/lock_tables.go +++ b/cmd/lock/lock_tables.go @@ -4,9 +4,7 @@ package lock // SPDX-License-Identifier: BSD-3-Clause import ( - htmltemplate "html/template" "perfspect/internal/common" - "perfspect/internal/report" "perfspect/internal/script" "perfspect/internal/table" "strings" @@ -39,19 +37,3 @@ func kernelLockAnalysisTableValues(outputs map[string]script.ScriptOutput) []tab } return fields } - -func kernelLockAnalysisHTMLRenderer(tableValues table.TableValues, targetName string) string { - values := [][]string{} - var tableValueStyles [][]string - for _, field := range tableValues.Fields { - rowValues := []string{} - rowValues = append(rowValues, field.Name) - rowValues = append(rowValues, htmltemplate.HTMLEscapeString(field.Values[0])) - values = append(values, rowValues) - rowStyles := []string{} - rowStyles = append(rowStyles, "font-weight:bold") - rowStyles = append(rowStyles, "white-space: pre-wrap") - tableValueStyles = append(tableValueStyles, rowStyles) - } - return report.RenderHTMLTable([]string{}, values, "pure-table pure-table-striped", tableValueStyles) -} diff --git a/cmd/metrics/metrics.go b/cmd/metrics/metrics.go index 2d9b7533..80b2f487 100644 --- a/cmd/metrics/metrics.go +++ b/cmd/metrics/metrics.go @@ -1,4 +1,4 @@ -// Package metrics is a subcommand of the root command. It provides functionality to monitor core and uncore metrics on one target. +// Package metrics is a subcommand of the root command. It provides functionality to collect performance metrics from target(s). package metrics // Copyright (C) 2021-2025 Intel Corporation @@ -51,7 +51,7 @@ var examples = []string{ var Cmd = &cobra.Command{ Use: cmdName, - Short: "Monitor core and uncore metrics from one target", + Short: "Collect performance metrics from target(s)", Long: "", Example: strings.Join(examples, "\n"), RunE: runCmd, diff --git a/cmd/metrics/trim.go b/cmd/metrics/trim.go index df3251fa..168a2ab8 100644 --- a/cmd/metrics/trim.go +++ b/cmd/metrics/trim.go @@ -44,7 +44,7 @@ var trimExamples = []string{ var trimCmd = &cobra.Command{ Use: trimCmdName, - Short: "Generate new summary reports from existing metrics collection by filtering to a specific time range", + Short: "Filter existing metrics to a time range", Long: "", Example: strings.Join(trimExamples, "\n"), RunE: runTrimCmd, diff --git a/cmd/report/report.go b/cmd/report/report.go index 27a26b3e..b240750a 100644 --- a/cmd/report/report.go +++ b/cmd/report/report.go @@ -37,7 +37,7 @@ var examples = []string{ var Cmd = &cobra.Command{ Use: cmdName, - Short: "Generate configuration report for target(s)", + Short: "Collect configuration data from target(s)", Example: strings.Join(examples, "\n"), RunE: runCmd, PreRunE: validateFlags, diff --git a/cmd/root.go b/cmd/root.go index 584ed825..17b0854e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,7 +23,7 @@ import ( "time" "perfspect/cmd/config" - "perfspect/cmd/flame" + "perfspect/cmd/flamegraph" "perfspect/cmd/lock" "perfspect/cmd/metrics" "perfspect/cmd/report" @@ -46,7 +46,7 @@ const ( var examples = []string{ fmt.Sprintf(" Generate a configuration report: $ %s report", common.AppName), - fmt.Sprintf(" Monitor micro-architectural metrics: $ %s metrics", common.AppName), + fmt.Sprintf(" Collect micro-architectural metrics: $ %s metrics", common.AppName), fmt.Sprintf(" Generate a configuration report on a remote target: $ %s report --target 192.168.1.2 --user elaine --key ~/.ssh/id_rsa", common.AppName), fmt.Sprintf(" Generate configuration reports for multiple remote targets: $ %s report --targets ./targets.yaml", common.AppName), } @@ -116,7 +116,7 @@ Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} rootCmd.AddCommand(report.Cmd) rootCmd.AddCommand(metrics.Cmd) rootCmd.AddCommand(telemetry.Cmd) - rootCmd.AddCommand(flame.Cmd) + rootCmd.AddCommand(flamegraph.Cmd) rootCmd.AddCommand(lock.Cmd) rootCmd.AddCommand(config.Cmd) rootCmd.AddGroup([]*cobra.Group{{ID: "other", Title: "Other Commands:"}}...) diff --git a/cmd/telemetry/telemetry_renderers.go b/cmd/telemetry/telemetry_renderers.go new file mode 100644 index 00000000..e1e87e72 --- /dev/null +++ b/cmd/telemetry/telemetry_renderers.go @@ -0,0 +1,675 @@ +package telemetry + +// Copyright (C) 2021-2025 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause + +import ( + "fmt" + "log/slog" + "perfspect/internal/report" + "perfspect/internal/table" + "perfspect/internal/util" + "slices" + "sort" + "strconv" + "strings" +) + +func telemetryTableHTMLRenderer(tableValues table.TableValues, data [][]float64, datasetNames []string, chartConfig report.ChartTemplateStruct, datasetHiddenFlags []bool) string { + tsFieldIdx := 0 + var timestamps []string + for i := range tableValues.Fields[0].Values { + timestamp := tableValues.Fields[tsFieldIdx].Values[i] + if !slices.Contains(timestamps, timestamp) { // could be slow if list is long + timestamps = append(timestamps, timestamp) + } + } + return renderLineChart(timestamps, data, datasetNames, chartConfig, datasetHiddenFlags) +} + +// renderLineChart generates an HTML string for a line chart using the provided data and configuration. +// +// Parameters: +// +// xAxisLabels - Slice of strings representing the labels for the X axis. +// data - 2D slice of float64 values, where each inner slice represents a dataset's data points. +// datasetNames - Slice of strings representing the names of each dataset. +// config - chartTemplateStruct containing chart configuration options. +// datasetHiddenFlags - Slice of booleans indicating whether each dataset should be hidden initially. +// +// Returns: +// +// A string containing the rendered HTML for the line chart. +func renderLineChart(xAxisLabels []string, data [][]float64, datasetNames []string, config report.ChartTemplateStruct, datasetHiddenFlags []bool) string { + allFormattedPoints := []string{} + for dataIdx := range data { + formattedPoints := []string{} + for _, point := range data[dataIdx] { + formattedPoints = append(formattedPoints, fmt.Sprintf("%f", point)) + } + allFormattedPoints = append(allFormattedPoints, strings.Join(formattedPoints, ",")) + } + return report.RenderChart("line", allFormattedPoints, datasetNames, xAxisLabels, config, datasetHiddenFlags) +} + +func cpuUtilizationTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + data := [][]float64{} + datasetNames := []string{} + // collect the busy (100 - idle) values for each CPU + cpuBusyStats := make(map[int][]float64) + idleFieldIdx := len(tableValues.Fields) - 1 + cpuFieldIdx := 1 + for i := range tableValues.Fields[0].Values { + idle, err := strconv.ParseFloat(tableValues.Fields[idleFieldIdx].Values[i], 64) + if err != nil { + continue + } + busy := 100 - idle + cpu, err := strconv.Atoi(tableValues.Fields[cpuFieldIdx].Values[i]) + if err != nil { + continue + } + if _, ok := cpuBusyStats[cpu]; !ok { + cpuBusyStats[cpu] = []float64{} + } + cpuBusyStats[cpu] = append(cpuBusyStats[cpu], busy) + } + // sort map keys by cpu number + var keys []int + for cpu := range cpuBusyStats { + keys = append(keys, cpu) + } + sort.Ints(keys) + // build the data + for _, cpu := range keys { + if len(cpuBusyStats[cpu]) > 0 { + data = append(data, cpuBusyStats[cpu]) + datasetNames = append(datasetNames, fmt.Sprintf("CPU %d", cpu)) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "% Utilization", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "false", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "100", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} + +func utilizationCategoriesTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + data := [][]float64{} + datasetNames := []string{} + for _, field := range tableValues.Fields[1:] { + points := []float64{} + for _, val := range field.Values { + if val == "" { + break + } + util, err := strconv.ParseFloat(val, 64) + if err != nil { + slog.Error("error parsing percentage", slog.String("error", err.Error())) + return "" + } + points = append(points, util) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, field.Name) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "% Utilization", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "100", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} + +func irqRateTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + data := [][]float64{} + datasetNames := []string{} + for _, field := range tableValues.Fields[2:] { // 1 data set per field, e.g., %usr, %nice, etc., skip Time and CPU fields + datasetNames = append(datasetNames, field.Name) + // sum the values in the field per timestamp, store the sum as a point + timeStamp := tableValues.Fields[0].Values[0] + points := []float64{} + total := 0.0 + for i := range field.Values { + if tableValues.Fields[0].Values[i] != timeStamp { // new timestamp? + points = append(points, total) + total = 0.0 + timeStamp = tableValues.Fields[0].Values[i] + } + val, err := strconv.ParseFloat(field.Values[i], 64) + if err != nil { + slog.Error("error parsing value", slog.String("error", err.Error())) + return "" + } + total += val + } + points = append(points, total) // add the point for the last timestamp + // save the points in the data slice + data = append(data, points) + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "IRQ/s", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} + +// driveTelemetryTableHTMLRenderer renders charts of drive statistics +// - one scatter chart per drive, showing the drive's utilization over time +// - each drive stat is a separate dataset within the chart +func driveTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + var out strings.Builder + driveStats := make(map[string][][]string) + for i := range tableValues.Fields[0].Values { + drive := tableValues.Fields[1].Values[i] + if _, ok := driveStats[drive]; !ok { + driveStats[drive] = make([][]string, len(tableValues.Fields)-2) + } + for j := range len(tableValues.Fields) - 2 { + driveStats[drive][j] = append(driveStats[drive][j], tableValues.Fields[j+2].Values[i]) + } + } + var keys []string + for drive := range driveStats { + keys = append(keys, drive) + } + sort.Strings(keys) + for _, drive := range keys { + data := [][]float64{} + datasetNames := []string{} + for i, statVals := range driveStats[drive] { + points := []float64{} + for i, val := range statVals { + if val == "" { + slog.Error("empty stat value", slog.String("drive", drive), slog.Int("index", i)) + return "" + } + util, err := strconv.ParseFloat(val, 64) + if err != nil { + slog.Error("error parsing stat", slog.String("error", err.Error())) + return "" + } + points = append(points, util) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, tableValues.Fields[i+2].Name) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "", + TitleText: drive, + DisplayTitle: "true", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + out.WriteString(telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil)) + } + return out.String() +} + +// networkTelemetryTableHTMLRenderer renders charts of network device statistics +// - one scatter chart per network device, showing the device's utilization over time +// - each network stat is a separate dataset within the chart +func networkTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + var out strings.Builder + nicStats := make(map[string][][]string) + for i := range tableValues.Fields[0].Values { + drive := tableValues.Fields[1].Values[i] + if _, ok := nicStats[drive]; !ok { + nicStats[drive] = make([][]string, len(tableValues.Fields)-2) + } + for j := range len(tableValues.Fields) - 2 { + nicStats[drive][j] = append(nicStats[drive][j], tableValues.Fields[j+2].Values[i]) + } + } + var keys []string + for drive := range nicStats { + keys = append(keys, drive) + } + sort.Strings(keys) + for _, nic := range keys { + data := [][]float64{} + datasetNames := []string{} + for i, statVals := range nicStats[nic] { + points := []float64{} + for i, val := range statVals { + if val == "" { + slog.Error("empty stat value", slog.String("nic", nic), slog.Int("index", i)) + return "" + } + util, err := strconv.ParseFloat(val, 64) + if err != nil { + slog.Error("error parsing stat", slog.String("error", err.Error())) + return "" + } + points = append(points, util) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, tableValues.Fields[i+2].Name) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "", + TitleText: nic, + DisplayTitle: "true", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + out.WriteString(telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil)) + } + return out.String() +} + +func memoryTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + data := [][]float64{} + datasetNames := []string{} + for _, field := range tableValues.Fields[1:] { + points := []float64{} + for _, 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, stat) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, field.Name) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "kilobytes", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} + +func averageFrequencyTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + data := [][]float64{} + datasetNames := []string{} + for _, field := range tableValues.Fields[1:] { + points := []float64{} + for _, 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, stat) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, field.Name) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "MHz", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} + +func powerTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + data := [][]float64{} + datasetNames := []string{} + for _, field := range tableValues.Fields[1:] { + points := []float64{} + for _, 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, stat) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, field.Name) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "Watts", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} + +func temperatureTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + data := [][]float64{} + datasetNames := []string{} + for _, field := range tableValues.Fields[1:] { + points := []float64{} + for _, 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, stat) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, field.Name) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "Celsius", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} + +func ipcTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + data := [][]float64{} + datasetNames := []string{} + for _, field := range tableValues.Fields[1:] { + points := []float64{} + for _, 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, stat) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, field.Name) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "IPC", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} + +func c6TelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + data := [][]float64{} + datasetNames := []string{} + for _, field := range tableValues.Fields[1:] { + points := []float64{} + for _, 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, stat) + } + if len(points) > 0 { + data = append(data, points) + datasetNames = append(datasetNames, field.Name) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "% C6 Residency", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} + +// instructionTelemetryTableHTMLRenderer renders instruction set usage statistics. +// Each category is a separate dataset within the chart. +// Categories with zero total usage are hidden by default. +// Categories are sorted in two tiers: first, all non-zero categories are sorted alphabetically; +// then, all zero-sum categories are sorted alphabetically and placed after the non-zero categories. +func instructionTelemetryTableHTMLRenderer(tableValues table.TableValues, targetname string) string { + // Collect entries with their sums so we can sort per requirements + type instrEntry struct { + name string + points []float64 + sum float64 + } + entries := []instrEntry{} + for _, field := range tableValues.Fields[1:] { // skip timestamp field + points := []float64{} + sum := 0.0 + for _, val := range field.Values { + if val == "" { // end of data for this category + break + } + stat, err := strconv.ParseFloat(val, 64) + if err != nil { + slog.Error("error parsing stat", slog.String("error", err.Error())) + return "" + } + points = append(points, stat) + sum += stat + } + if len(points) > 0 { // only include categories with at least one point + entries = append(entries, instrEntry{name: field.Name, points: points, sum: sum}) + } + } + // Partition into non-zero and zero-sum groups + nonZero := []instrEntry{} + zero := []instrEntry{} + for _, e := range entries { + if e.sum > 0 { + nonZero = append(nonZero, e) + } else { + zero = append(zero, e) + } + } + sort.Slice(nonZero, func(i, j int) bool { return nonZero[i].name < nonZero[j].name }) + sort.Slice(zero, func(i, j int) bool { return zero[i].name < zero[j].name }) + ordered := append(nonZero, zero...) + data := make([][]float64, 0, len(ordered)) + datasetNames := make([]string, 0, len(ordered)) + hiddenFlags := make([]bool, 0, len(ordered)) + for _, e := range ordered { + data = append(data, e.points) + datasetNames = append(datasetNames, e.name) + // hide zero-sum categories by default + hiddenFlags = append(hiddenFlags, e.sum == 0) + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "% Samples", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "1", // extra tall due to large number of data sets + SuggestedMin: "0", + SuggestedMax: "0", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, hiddenFlags) +} + +func renderGaudiStatsChart(tableValues table.TableValues, chartStatFieldName string, titleText string, yAxisText string, suggestedMax string) string { + data := [][]float64{} + datasetNames := []string{} + // timestamp is in the first field + // find the module_id field index + moduleIdFieldIdx, err := table.GetFieldIndex("module_id", tableValues) + if err != nil { + slog.Error("no gaudi module_id field found") + return "" + } + // find the chartStatFieldName field index + chartStatFieldIndex, err := table.GetFieldIndex(chartStatFieldName, tableValues) + if err != nil { + slog.Error("no gaudi chartStatFieldName field found") + return "" + } + // group the data points by module_id + moduleStat := make(map[string][]float64) + for i := range tableValues.Fields[0].Values { + moduleId := tableValues.Fields[moduleIdFieldIdx].Values[i] + val, err := strconv.ParseFloat(tableValues.Fields[chartStatFieldIndex].Values[i], 64) + if err != nil { + slog.Error("error parsing utilization", slog.String("error", err.Error())) + return "" + } + if _, ok := moduleStat[moduleId]; !ok { + moduleStat[moduleId] = []float64{} + } + moduleStat[moduleId] = append(moduleStat[moduleId], val) + } + // sort the module ids + var moduleIds []string + for moduleId := range moduleStat { + moduleIds = append(moduleIds, moduleId) + } + sort.Strings(moduleIds) + // build the data + for _, moduleId := range moduleIds { + if len(moduleStat[moduleId]) > 0 { + data = append(data, moduleStat[moduleId]) + datasetNames = append(datasetNames, "module "+moduleId) + } + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: yAxisText, + TitleText: titleText, + DisplayTitle: "true", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: suggestedMax, + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} + +func gaudiTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + out := "" + out += renderGaudiStatsChart(tableValues, "utilization.aip [%]", "Utilization", "% Utilization", "100") + out += renderGaudiStatsChart(tableValues, "memory.free [MiB]", "Memory Free", "Memory (MiB)", "0") + out += renderGaudiStatsChart(tableValues, "memory.used [MiB]", "Memory Used", "Memory (MiB)", "0") + out += renderGaudiStatsChart(tableValues, "power.draw [W]", "Power", "Watts", "0") + out += renderGaudiStatsChart(tableValues, "temperature.aip [C]", "Temperature", "Temperature (C)", "0") + return out +} + +func pduTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { + data := [][]float64{} + for _, field := range tableValues.Fields[1:] { + points := []float64{} + for _, 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, stat) + } + if len(points) > 0 { + data = append(data, points) + } + } + datasetNames := []string{} + for _, field := range tableValues.Fields[1:] { + datasetNames = append(datasetNames, field.Name) + } + chartConfig := report.ChartTemplateStruct{ + ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), + XaxisText: "Time", + YaxisText: "Watts", + TitleText: "", + DisplayTitle: "false", + DisplayLegend: "true", + AspectRatio: "2", + SuggestedMin: "0", + SuggestedMax: "0", + } + return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) +} diff --git a/cmd/telemetry/telemetry_tables.go b/cmd/telemetry/telemetry_tables.go index 03f6ce0e..3656a36c 100644 --- a/cmd/telemetry/telemetry_tables.go +++ b/cmd/telemetry/telemetry_tables.go @@ -9,13 +9,9 @@ import ( "log/slog" "perfspect/internal/common" "perfspect/internal/cpus" - "perfspect/internal/report" "perfspect/internal/script" "perfspect/internal/table" - "perfspect/internal/util" "regexp" - "slices" - "sort" "strconv" "strings" "time" @@ -707,662 +703,3 @@ func instructionTelemetryTableValues(outputs map[string]script.ScriptOutput) []t } return fields } - -func telemetryTableHTMLRenderer(tableValues table.TableValues, data [][]float64, datasetNames []string, chartConfig report.ChartTemplateStruct, datasetHiddenFlags []bool) string { - tsFieldIdx := 0 - var timestamps []string - for i := range tableValues.Fields[0].Values { - timestamp := tableValues.Fields[tsFieldIdx].Values[i] - if !slices.Contains(timestamps, timestamp) { // could be slow if list is long - timestamps = append(timestamps, timestamp) - } - } - return renderLineChart(timestamps, data, datasetNames, chartConfig, datasetHiddenFlags) -} - -// renderLineChart generates an HTML string for a line chart using the provided data and configuration. -// -// Parameters: -// -// xAxisLabels - Slice of strings representing the labels for the X axis. -// data - 2D slice of float64 values, where each inner slice represents a dataset's data points. -// datasetNames - Slice of strings representing the names of each dataset. -// config - chartTemplateStruct containing chart configuration options. -// datasetHiddenFlags - Slice of booleans indicating whether each dataset should be hidden initially. -// -// Returns: -// -// A string containing the rendered HTML for the line chart. -func renderLineChart(xAxisLabels []string, data [][]float64, datasetNames []string, config report.ChartTemplateStruct, datasetHiddenFlags []bool) string { - allFormattedPoints := []string{} - for dataIdx := range data { - formattedPoints := []string{} - for _, point := range data[dataIdx] { - formattedPoints = append(formattedPoints, fmt.Sprintf("%f", point)) - } - allFormattedPoints = append(allFormattedPoints, strings.Join(formattedPoints, ",")) - } - return report.RenderChart("line", allFormattedPoints, datasetNames, xAxisLabels, config, datasetHiddenFlags) -} - -func cpuUtilizationTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - data := [][]float64{} - datasetNames := []string{} - // collect the busy (100 - idle) values for each CPU - cpuBusyStats := make(map[int][]float64) - idleFieldIdx := len(tableValues.Fields) - 1 - cpuFieldIdx := 1 - for i := range tableValues.Fields[0].Values { - idle, err := strconv.ParseFloat(tableValues.Fields[idleFieldIdx].Values[i], 64) - if err != nil { - continue - } - busy := 100 - idle - cpu, err := strconv.Atoi(tableValues.Fields[cpuFieldIdx].Values[i]) - if err != nil { - continue - } - if _, ok := cpuBusyStats[cpu]; !ok { - cpuBusyStats[cpu] = []float64{} - } - cpuBusyStats[cpu] = append(cpuBusyStats[cpu], busy) - } - // sort map keys by cpu number - var keys []int - for cpu := range cpuBusyStats { - keys = append(keys, cpu) - } - sort.Ints(keys) - // build the data - for _, cpu := range keys { - if len(cpuBusyStats[cpu]) > 0 { - data = append(data, cpuBusyStats[cpu]) - datasetNames = append(datasetNames, fmt.Sprintf("CPU %d", cpu)) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "% Utilization", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "false", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "100", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -} - -func utilizationCategoriesTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - data := [][]float64{} - datasetNames := []string{} - for _, field := range tableValues.Fields[1:] { - points := []float64{} - for _, val := range field.Values { - if val == "" { - break - } - util, err := strconv.ParseFloat(val, 64) - if err != nil { - slog.Error("error parsing percentage", slog.String("error", err.Error())) - return "" - } - points = append(points, util) - } - if len(points) > 0 { - data = append(data, points) - datasetNames = append(datasetNames, field.Name) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "% Utilization", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "100", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -} - -func irqRateTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - data := [][]float64{} - datasetNames := []string{} - for _, field := range tableValues.Fields[2:] { // 1 data set per field, e.g., %usr, %nice, etc., skip Time and CPU fields - datasetNames = append(datasetNames, field.Name) - // sum the values in the field per timestamp, store the sum as a point - timeStamp := tableValues.Fields[0].Values[0] - points := []float64{} - total := 0.0 - for i := range field.Values { - if tableValues.Fields[0].Values[i] != timeStamp { // new timestamp? - points = append(points, total) - total = 0.0 - timeStamp = tableValues.Fields[0].Values[i] - } - val, err := strconv.ParseFloat(field.Values[i], 64) - if err != nil { - slog.Error("error parsing value", slog.String("error", err.Error())) - return "" - } - total += val - } - points = append(points, total) // add the point for the last timestamp - // save the points in the data slice - data = append(data, points) - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "IRQ/s", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "0", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -} - -// driveTelemetryTableHTMLRenderer renders charts of drive statistics -// - one scatter chart per drive, showing the drive's utilization over time -// - each drive stat is a separate dataset within the chart -func driveTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - var out strings.Builder - driveStats := make(map[string][][]string) - for i := range tableValues.Fields[0].Values { - drive := tableValues.Fields[1].Values[i] - if _, ok := driveStats[drive]; !ok { - driveStats[drive] = make([][]string, len(tableValues.Fields)-2) - } - for j := range len(tableValues.Fields) - 2 { - driveStats[drive][j] = append(driveStats[drive][j], tableValues.Fields[j+2].Values[i]) - } - } - var keys []string - for drive := range driveStats { - keys = append(keys, drive) - } - sort.Strings(keys) - for _, drive := range keys { - data := [][]float64{} - datasetNames := []string{} - for i, statVals := range driveStats[drive] { - points := []float64{} - for i, val := range statVals { - if val == "" { - slog.Error("empty stat value", slog.String("drive", drive), slog.Int("index", i)) - return "" - } - util, err := strconv.ParseFloat(val, 64) - if err != nil { - slog.Error("error parsing stat", slog.String("error", err.Error())) - return "" - } - points = append(points, util) - } - if len(points) > 0 { - data = append(data, points) - datasetNames = append(datasetNames, tableValues.Fields[i+2].Name) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "", - TitleText: drive, - DisplayTitle: "true", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "0", - } - out.WriteString(telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil)) - } - return out.String() -} - -// networkTelemetryTableHTMLRenderer renders charts of network device statistics -// - one scatter chart per network device, showing the device's utilization over time -// - each network stat is a separate dataset within the chart -func networkTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - var out strings.Builder - nicStats := make(map[string][][]string) - for i := range tableValues.Fields[0].Values { - drive := tableValues.Fields[1].Values[i] - if _, ok := nicStats[drive]; !ok { - nicStats[drive] = make([][]string, len(tableValues.Fields)-2) - } - for j := range len(tableValues.Fields) - 2 { - nicStats[drive][j] = append(nicStats[drive][j], tableValues.Fields[j+2].Values[i]) - } - } - var keys []string - for drive := range nicStats { - keys = append(keys, drive) - } - sort.Strings(keys) - for _, nic := range keys { - data := [][]float64{} - datasetNames := []string{} - for i, statVals := range nicStats[nic] { - points := []float64{} - for i, val := range statVals { - if val == "" { - slog.Error("empty stat value", slog.String("nic", nic), slog.Int("index", i)) - return "" - } - util, err := strconv.ParseFloat(val, 64) - if err != nil { - slog.Error("error parsing stat", slog.String("error", err.Error())) - return "" - } - points = append(points, util) - } - if len(points) > 0 { - data = append(data, points) - datasetNames = append(datasetNames, tableValues.Fields[i+2].Name) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "", - TitleText: nic, - DisplayTitle: "true", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "0", - } - out.WriteString(telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil)) - } - return out.String() -} - -func memoryTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - data := [][]float64{} - datasetNames := []string{} - for _, field := range tableValues.Fields[1:] { - points := []float64{} - for _, 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, stat) - } - if len(points) > 0 { - data = append(data, points) - datasetNames = append(datasetNames, field.Name) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "kilobytes", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "0", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -} - -func averageFrequencyTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - data := [][]float64{} - datasetNames := []string{} - for _, field := range tableValues.Fields[1:] { - points := []float64{} - for _, 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, stat) - } - if len(points) > 0 { - data = append(data, points) - datasetNames = append(datasetNames, field.Name) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "MHz", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "0", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -} - -func powerTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - data := [][]float64{} - datasetNames := []string{} - for _, field := range tableValues.Fields[1:] { - points := []float64{} - for _, 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, stat) - } - if len(points) > 0 { - data = append(data, points) - datasetNames = append(datasetNames, field.Name) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "Watts", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "0", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -} - -func temperatureTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - data := [][]float64{} - datasetNames := []string{} - for _, field := range tableValues.Fields[1:] { - points := []float64{} - for _, 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, stat) - } - if len(points) > 0 { - data = append(data, points) - datasetNames = append(datasetNames, field.Name) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "Celsius", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "0", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -} - -func ipcTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - data := [][]float64{} - datasetNames := []string{} - for _, field := range tableValues.Fields[1:] { - points := []float64{} - for _, 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, stat) - } - if len(points) > 0 { - data = append(data, points) - datasetNames = append(datasetNames, field.Name) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "IPC", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "0", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -} - -func c6TelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - data := [][]float64{} - datasetNames := []string{} - for _, field := range tableValues.Fields[1:] { - points := []float64{} - for _, 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, stat) - } - if len(points) > 0 { - data = append(data, points) - datasetNames = append(datasetNames, field.Name) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "% C6 Residency", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "0", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -} - -// instructionTelemetryTableHTMLRenderer renders instruction set usage statistics. -// Each category is a separate dataset within the chart. -// Categories with zero total usage are hidden by default. -// Categories are sorted in two tiers: first, all non-zero categories are sorted alphabetically; -// then, all zero-sum categories are sorted alphabetically and placed after the non-zero categories. -func instructionTelemetryTableHTMLRenderer(tableValues table.TableValues, targetname string) string { - // Collect entries with their sums so we can sort per requirements - type instrEntry struct { - name string - points []float64 - sum float64 - } - entries := []instrEntry{} - for _, field := range tableValues.Fields[1:] { // skip timestamp field - points := []float64{} - sum := 0.0 - for _, val := range field.Values { - if val == "" { // end of data for this category - break - } - stat, err := strconv.ParseFloat(val, 64) - if err != nil { - slog.Error("error parsing stat", slog.String("error", err.Error())) - return "" - } - points = append(points, stat) - sum += stat - } - if len(points) > 0 { // only include categories with at least one point - entries = append(entries, instrEntry{name: field.Name, points: points, sum: sum}) - } - } - // Partition into non-zero and zero-sum groups - nonZero := []instrEntry{} - zero := []instrEntry{} - for _, e := range entries { - if e.sum > 0 { - nonZero = append(nonZero, e) - } else { - zero = append(zero, e) - } - } - sort.Slice(nonZero, func(i, j int) bool { return nonZero[i].name < nonZero[j].name }) - sort.Slice(zero, func(i, j int) bool { return zero[i].name < zero[j].name }) - ordered := append(nonZero, zero...) - data := make([][]float64, 0, len(ordered)) - datasetNames := make([]string, 0, len(ordered)) - hiddenFlags := make([]bool, 0, len(ordered)) - for _, e := range ordered { - data = append(data, e.points) - datasetNames = append(datasetNames, e.name) - // hide zero-sum categories by default - hiddenFlags = append(hiddenFlags, e.sum == 0) - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "% Samples", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "1", // extra tall due to large number of data sets - SuggestedMin: "0", - SuggestedMax: "0", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, hiddenFlags) -} - -func renderGaudiStatsChart(tableValues table.TableValues, chartStatFieldName string, titleText string, yAxisText string, suggestedMax string) string { - data := [][]float64{} - datasetNames := []string{} - // timestamp is in the first field - // find the module_id field index - moduleIdFieldIdx, err := table.GetFieldIndex("module_id", tableValues) - if err != nil { - slog.Error("no gaudi module_id field found") - return "" - } - // find the chartStatFieldName field index - chartStatFieldIndex, err := table.GetFieldIndex(chartStatFieldName, tableValues) - if err != nil { - slog.Error("no gaudi chartStatFieldName field found") - return "" - } - // group the data points by module_id - moduleStat := make(map[string][]float64) - for i := range tableValues.Fields[0].Values { - moduleId := tableValues.Fields[moduleIdFieldIdx].Values[i] - val, err := strconv.ParseFloat(tableValues.Fields[chartStatFieldIndex].Values[i], 64) - if err != nil { - slog.Error("error parsing utilization", slog.String("error", err.Error())) - return "" - } - if _, ok := moduleStat[moduleId]; !ok { - moduleStat[moduleId] = []float64{} - } - moduleStat[moduleId] = append(moduleStat[moduleId], val) - } - // sort the module ids - var moduleIds []string - for moduleId := range moduleStat { - moduleIds = append(moduleIds, moduleId) - } - sort.Strings(moduleIds) - // build the data - for _, moduleId := range moduleIds { - if len(moduleStat[moduleId]) > 0 { - data = append(data, moduleStat[moduleId]) - datasetNames = append(datasetNames, "module "+moduleId) - } - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: yAxisText, - TitleText: titleText, - DisplayTitle: "true", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: suggestedMax, - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -} - -func gaudiTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - out := "" - out += renderGaudiStatsChart(tableValues, "utilization.aip [%]", "Utilization", "% Utilization", "100") - out += renderGaudiStatsChart(tableValues, "memory.free [MiB]", "Memory Free", "Memory (MiB)", "0") - out += renderGaudiStatsChart(tableValues, "memory.used [MiB]", "Memory Used", "Memory (MiB)", "0") - out += renderGaudiStatsChart(tableValues, "power.draw [W]", "Power", "Watts", "0") - out += renderGaudiStatsChart(tableValues, "temperature.aip [C]", "Temperature", "Temperature (C)", "0") - return out -} - -func pduTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { - data := [][]float64{} - for _, field := range tableValues.Fields[1:] { - points := []float64{} - for _, 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, stat) - } - if len(points) > 0 { - data = append(data, points) - } - } - datasetNames := []string{} - for _, field := range tableValues.Fields[1:] { - datasetNames = append(datasetNames, field.Name) - } - chartConfig := report.ChartTemplateStruct{ - ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), - XaxisText: "Time", - YaxisText: "Watts", - TitleText: "", - DisplayTitle: "false", - DisplayLegend: "true", - AspectRatio: "2", - SuggestedMin: "0", - SuggestedMax: "0", - } - return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) -}