From c32d6b8aedd875f34a7c074dc0f65ece6ccc5765 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Tue, 3 Mar 2026 15:34:58 -0800 Subject: [PATCH 1/4] feature: all c-states telemetry Signed-off-by: Harper, Jason M --- cmd/telemetry/telemetry.go | 10 ++-- cmd/telemetry/telemetry_renderers.go | 4 +- cmd/telemetry/telemetry_tables.go | 44 ++++++++++-------- internal/extract/turbostat.go | 68 ++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 27 deletions(-) diff --git a/cmd/telemetry/telemetry.go b/cmd/telemetry/telemetry.go index dbdc3050..cdb801dc 100644 --- a/cmd/telemetry/telemetry.go +++ b/cmd/telemetry/telemetry.go @@ -57,7 +57,7 @@ var ( flagCPU bool flagFrequency bool flagIPC bool - flagC6 bool + flagCstate bool flagIRQRate bool flagMemory bool flagNetwork bool @@ -82,7 +82,7 @@ const ( flagCPUName = "cpu" flagFrequencyName = "frequency" flagIPCName = "ipc" - flagC6Name = "c6" + flagCstateName = "cstate" flagIRQRateName = "irqrate" flagMemoryName = "memory" flagNetworkName = "network" @@ -105,7 +105,7 @@ var telemetrySummaryTableName = "Telemetry Summary" var categories = []app.Category{ {FlagName: flagCPUName, FlagVar: &flagCPU, DefaultValue: false, Help: "monitor cpu utilization", Tables: []table.TableDefinition{tableDefinitions[CPUUtilizationTelemetryTableName], tableDefinitions[UtilizationCategoriesTelemetryTableName]}}, {FlagName: flagIPCName, FlagVar: &flagIPC, DefaultValue: false, Help: "monitor IPC", Tables: []table.TableDefinition{tableDefinitions[IPCTelemetryTableName]}}, - {FlagName: flagC6Name, FlagVar: &flagC6, DefaultValue: false, Help: "monitor C6 residency", Tables: []table.TableDefinition{tableDefinitions[C6TelemetryTableName]}}, + {FlagName: flagCstateName, FlagVar: &flagCstate, DefaultValue: false, Help: "monitor C-States residency", Tables: []table.TableDefinition{tableDefinitions[CstatesTelemetryTableName]}}, {FlagName: flagFrequencyName, FlagVar: &flagFrequency, DefaultValue: false, Help: "monitor cpu frequency", Tables: []table.TableDefinition{tableDefinitions[FrequencyTelemetryTableName]}}, {FlagName: flagPowerName, FlagVar: &flagPower, DefaultValue: false, Help: "monitor power", Tables: []table.TableDefinition{tableDefinitions[PowerTelemetryTableName]}}, {FlagName: flagTemperatureName, FlagVar: &flagTemperature, DefaultValue: false, Help: "monitor temperature", Tables: []table.TableDefinition{tableDefinitions[TemperatureTelemetryTableName]}}, @@ -336,7 +336,7 @@ func runCmd(cmd *cobra.Command, args []string) error { report.RegisterHTMLRenderer(CPUUtilizationTelemetryTableName, cpuUtilizationTelemetryTableHTMLRenderer) report.RegisterHTMLRenderer(UtilizationCategoriesTelemetryTableName, utilizationCategoriesTelemetryTableHTMLRenderer) report.RegisterHTMLRenderer(IPCTelemetryTableName, ipcTelemetryTableHTMLRenderer) - report.RegisterHTMLRenderer(C6TelemetryTableName, c6TelemetryTableHTMLRenderer) + report.RegisterHTMLRenderer(CstatesTelemetryTableName, cstatesTelemetryTableHTMLRenderer) report.RegisterHTMLRenderer(FrequencyTelemetryTableName, averageFrequencyTelemetryTableHTMLRenderer) report.RegisterHTMLRenderer(IRQRateTelemetryTableName, irqRateTelemetryTableHTMLRenderer) report.RegisterHTMLRenderer(DriveTelemetryTableName, driveTelemetryTableHTMLRenderer) @@ -364,7 +364,6 @@ func getTableValues(allTableValues []table.TableValues, tableName string) table. func summaryFromTableValues(allTableValues []table.TableValues, _ map[string]script.ScriptOutput) table.TableValues { cpuUtil := getCPUAveragePercentage(getTableValues(allTableValues, UtilizationCategoriesTelemetryTableName), "%idle", true) ipc := getCPUAveragePercentage(getTableValues(allTableValues, IPCTelemetryTableName), "Core (Avg.)", false) - c6 := getCPUAveragePercentage(getTableValues(allTableValues, C6TelemetryTableName), "Core (Avg.)", false) avgCoreFreq := getMetricAverage(getTableValues(allTableValues, FrequencyTelemetryTableName), []string{"Core (Avg.)"}, "Time") pkgPower := getPkgAveragePower(allTableValues) pkgTemperature := getPkgAverageTemperature(allTableValues) @@ -386,7 +385,6 @@ func summaryFromTableValues(allTableValues []table.TableValues, _ map[string]scr Fields: []table.Field{ {Name: "CPU Utilization (%)", Values: []string{cpuUtil}}, {Name: "IPC", Values: []string{ipc}}, - {Name: "C6 Core Residency (%)", Values: []string{c6}}, {Name: "Core Frequency (MHz)", Values: []string{avgCoreFreq}}, {Name: "Package Power (Watts)", Values: []string{pkgPower}}, {Name: "Package Temperature (C)", Values: []string{pkgTemperature}}, diff --git a/cmd/telemetry/telemetry_renderers.go b/cmd/telemetry/telemetry_renderers.go index c4f16032..4bd995d2 100644 --- a/cmd/telemetry/telemetry_renderers.go +++ b/cmd/telemetry/telemetry_renderers.go @@ -531,7 +531,7 @@ func ipcTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName str return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) } -func c6TelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { +func cstatesTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { if len(tableValues.Fields) < 2 { slog.Error("insufficient fields in table, expected at least 2", slog.String("table", tableValues.Name), slog.Int("fields", len(tableValues.Fields))) return "" @@ -559,7 +559,7 @@ func c6TelemetryTableHTMLRenderer(tableValues table.TableValues, targetName stri chartConfig := report.ChartTemplateStruct{ ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), XaxisText: "Time", - YaxisText: "% C6 Residency", + YaxisText: "% Residency", TitleText: "", DisplayTitle: "false", DisplayLegend: "true", diff --git a/cmd/telemetry/telemetry_tables.go b/cmd/telemetry/telemetry_tables.go index e34135b7..6e5952a7 100644 --- a/cmd/telemetry/telemetry_tables.go +++ b/cmd/telemetry/telemetry_tables.go @@ -22,7 +22,7 @@ const ( CPUUtilizationTelemetryTableName = "CPU Utilization Telemetry" UtilizationCategoriesTelemetryTableName = "Utilization Categories Telemetry" IPCTelemetryTableName = "IPC Telemetry" - C6TelemetryTableName = "C6 Telemetry" + CstatesTelemetryTableName = "C-States Telemetry" FrequencyTelemetryTableName = "Frequency Telemetry" IRQRateTelemetryTableName = "IRQ Rate Telemetry" InstructionTelemetryTableName = "Instruction Telemetry" @@ -41,7 +41,7 @@ const ( CPUUtilizationTelemetryMenuLabel = "CPU Utilization" UtilizationCategoriesTelemetryMenuLabel = "Utilization Categories" IPCTelemetryMenuLabel = "IPC" - C6TelemetryMenuLabel = "C6" + CstatesTelemetryMenuLabel = "C-States" FrequencyTelemetryMenuLabel = "Frequency" IRQRateTelemetryMenuLabel = "IRQ Rate" InstructionTelemetryMenuLabel = "Instruction" @@ -84,15 +84,15 @@ var tableDefinitions = map[string]table.TableDefinition{ script.TurbostatTelemetryScriptName, }, FieldsFunc: ipcTelemetryTableValues}, - C6TelemetryTableName: { - Name: C6TelemetryTableName, - MenuLabel: C6TelemetryMenuLabel, + CstatesTelemetryTableName: { + Name: CstatesTelemetryTableName, + MenuLabel: CstatesTelemetryMenuLabel, Architectures: []string{cpus.X86Architecture}, HasRows: true, ScriptNames: []string{ script.TurbostatTelemetryScriptName, }, - FieldsFunc: c6TelemetryTableValues}, + FieldsFunc: cstatesTelemetryTableValues}, FrequencyTelemetryTableName: { Name: FrequencyTelemetryTableName, MenuLabel: FrequencyTelemetryMenuLabel, @@ -510,13 +510,12 @@ func ipcTelemetryTableValues(outputs map[string]script.ScriptOutput) []table.Fie return fields } -func c6TelemetryTableValues(outputs map[string]script.ScriptOutput) []table.Field { - fields := []table.Field{ - {Name: "Time"}, - {Name: "Package (Avg.)"}, - {Name: "Core (Avg.)"}, - } - platformRows, err := extract.TurbostatPlatformRows(outputs[script.TurbostatTelemetryScriptName].Stdout, []string{"C6%", "CPU%c6"}) +func cstatesTelemetryTableValues(outputs map[string]script.ScriptOutput) []table.Field { + fields := []table.Field{} + // e.g., SRF - C1% C1E% C6S% C6SP% CPU%c1 CPU%c6 Mod%c6 + // e.g., GNR - POLL% C1% C1E% C6% C6P% CPU%c1 CPU%c6 + reCstate := regexp.MustCompile(`^(C\d+\w%|CPU%c\d+|POLL%|Mod%c\d+)$`) // matches C1%, C1E%, C6%, C6S%, C6P%, CPU%c1, CPU%c6, C1E%, C6P%, POLL%, Mod%c6 + platformRows, err := extract.TurbostatPlatformRowsByRegexMatch(outputs[script.TurbostatTelemetryScriptName].Stdout, []*regexp.Regexp{reCstate}) if err != nil { slog.Warn(err.Error()) return []table.Field{} @@ -525,14 +524,21 @@ func c6TelemetryTableValues(outputs map[string]script.ScriptOutput) []table.Fiel slog.Warn("no platform rows found in turbostat telemetry output") return []table.Field{} } + // dynamically build the fields based on the cstate-related fields we found in the platform rows + // e.g., if we found "C6%" and "CPU%c6", we'll create two fields, "C6%" and "CPU%c6" // for each platform row for i := range platformRows { - // append the timestamp to the fields - fields[0].Values = append(fields[0].Values, platformRows[i][0]) // Timestamp - // append the C6 residency values to the fields - fields[1].Values = append(fields[1].Values, platformRows[i][1]) // C6% - // append the CPU C6 residency values to the fields - fields[2].Values = append(fields[2].Values, platformRows[i][2]) // CPU%c6 + // the first row contains the field names, so use that to build the fields, including the timestamp field + if i == 0 { + for _, fieldName := range platformRows[i] { + fields = append(fields, table.Field{Name: fieldName}) + } + } else { + // append the values to the corresponding fields + for j := range platformRows[i] { + fields[j].Values = append(fields[j].Values, platformRows[i][j]) + } + } } return fields } diff --git a/internal/extract/turbostat.go b/internal/extract/turbostat.go index 898c2d6d..dbc828aa 100644 --- a/internal/extract/turbostat.go +++ b/internal/extract/turbostat.go @@ -6,6 +6,7 @@ package extract import ( "fmt" "log/slog" + "regexp" "slices" "strconv" "strings" @@ -77,6 +78,73 @@ func parseTurbostatOutput(output string) ([]map[string]string, error) { return rows, nil } +// TurbostatPlatformRowsByRegexMatch parses the output of the turbostat script and returns the rows +// for the platform (summary) only, matching fields by regex. +// Multiple fields may match the regex, all matching fields, and their values, will be returned in +// the order they appear in the output. +// Returns: +// - [][]string: first row is the header with "timestamp" followed by matched field names, subsequent +// rows contain the corresponding values for each platform row in the output. +func TurbostatPlatformRowsByRegexMatch(turboStatScriptOutput string, fieldRegexs []*regexp.Regexp) ([][]string, error) { + if turboStatScriptOutput == "" { + return nil, fmt.Errorf("turbostat output is empty") + } + if len(fieldRegexs) == 0 { + return nil, fmt.Errorf("no field regexes provided") + } + rows, err := parseTurbostatOutput(turboStatScriptOutput) + if err != nil { + return nil, fmt.Errorf("unable to parse turbostat output: %w", err) + } + if len(rows) == 0 { + return nil, fmt.Errorf("no platform rows found in turbostat output") + } + // Determine which fields match any of the regexes, preserving column order + // from the first platform row. + var matchedFields []string + for _, row := range rows { + if !isPlatformRow(row) { + continue + } + for field := range row { + for _, re := range fieldRegexs { + if re.MatchString(field) { + if !slices.Contains(matchedFields, field) { + matchedFields = append(matchedFields, field) + } + break + } + } + } + break // only need the first platform row to discover fields + } + if len(matchedFields) == 0 { + return nil, fmt.Errorf("no fields matched the provided regexes in turbostat output") + } + // Sort alphabetically for deterministic output since map iteration is unordered. + slices.Sort(matchedFields) + // First row is the header: timestamp followed by matched field names. + header := make([]string, len(matchedFields)+1) + header[0] = "timestamp" + copy(header[1:], matchedFields) + fieldValues := [][]string{header} + for _, row := range rows { + if !isPlatformRow(row) { + continue + } + rowValues := make([]string, len(matchedFields)+1) + rowValues[0] = row["timestamp"] + for i, field := range matchedFields { + rowValues[i+1] = row[field] + } + fieldValues = append(fieldValues, rowValues) + } + if len(fieldValues) == 1 { // only the header row, no data + return nil, fmt.Errorf("no platform data found in turbostat output for the provided regexes") + } + return fieldValues, nil +} + // TurbostatPlatformRows parses the output of the turbostat script and returns the rows // for the platform (summary) only. func TurbostatPlatformRows(turboStatScriptOutput string, fieldNames []string) ([][]string, error) { From c4baa830a88c8e08791dbff0db989c50996689b0 Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Tue, 3 Mar 2026 15:34:58 -0800 Subject: [PATCH 2/4] feature: all c-states telemetry Signed-off-by: Harper, Jason M --- cmd/telemetry/telemetry.go | 10 ++-- cmd/telemetry/telemetry_renderers.go | 4 +- cmd/telemetry/telemetry_tables.go | 44 ++++++++++-------- internal/extract/turbostat.go | 68 ++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 27 deletions(-) diff --git a/cmd/telemetry/telemetry.go b/cmd/telemetry/telemetry.go index dbdc3050..cdb801dc 100644 --- a/cmd/telemetry/telemetry.go +++ b/cmd/telemetry/telemetry.go @@ -57,7 +57,7 @@ var ( flagCPU bool flagFrequency bool flagIPC bool - flagC6 bool + flagCstate bool flagIRQRate bool flagMemory bool flagNetwork bool @@ -82,7 +82,7 @@ const ( flagCPUName = "cpu" flagFrequencyName = "frequency" flagIPCName = "ipc" - flagC6Name = "c6" + flagCstateName = "cstate" flagIRQRateName = "irqrate" flagMemoryName = "memory" flagNetworkName = "network" @@ -105,7 +105,7 @@ var telemetrySummaryTableName = "Telemetry Summary" var categories = []app.Category{ {FlagName: flagCPUName, FlagVar: &flagCPU, DefaultValue: false, Help: "monitor cpu utilization", Tables: []table.TableDefinition{tableDefinitions[CPUUtilizationTelemetryTableName], tableDefinitions[UtilizationCategoriesTelemetryTableName]}}, {FlagName: flagIPCName, FlagVar: &flagIPC, DefaultValue: false, Help: "monitor IPC", Tables: []table.TableDefinition{tableDefinitions[IPCTelemetryTableName]}}, - {FlagName: flagC6Name, FlagVar: &flagC6, DefaultValue: false, Help: "monitor C6 residency", Tables: []table.TableDefinition{tableDefinitions[C6TelemetryTableName]}}, + {FlagName: flagCstateName, FlagVar: &flagCstate, DefaultValue: false, Help: "monitor C-States residency", Tables: []table.TableDefinition{tableDefinitions[CstatesTelemetryTableName]}}, {FlagName: flagFrequencyName, FlagVar: &flagFrequency, DefaultValue: false, Help: "monitor cpu frequency", Tables: []table.TableDefinition{tableDefinitions[FrequencyTelemetryTableName]}}, {FlagName: flagPowerName, FlagVar: &flagPower, DefaultValue: false, Help: "monitor power", Tables: []table.TableDefinition{tableDefinitions[PowerTelemetryTableName]}}, {FlagName: flagTemperatureName, FlagVar: &flagTemperature, DefaultValue: false, Help: "monitor temperature", Tables: []table.TableDefinition{tableDefinitions[TemperatureTelemetryTableName]}}, @@ -336,7 +336,7 @@ func runCmd(cmd *cobra.Command, args []string) error { report.RegisterHTMLRenderer(CPUUtilizationTelemetryTableName, cpuUtilizationTelemetryTableHTMLRenderer) report.RegisterHTMLRenderer(UtilizationCategoriesTelemetryTableName, utilizationCategoriesTelemetryTableHTMLRenderer) report.RegisterHTMLRenderer(IPCTelemetryTableName, ipcTelemetryTableHTMLRenderer) - report.RegisterHTMLRenderer(C6TelemetryTableName, c6TelemetryTableHTMLRenderer) + report.RegisterHTMLRenderer(CstatesTelemetryTableName, cstatesTelemetryTableHTMLRenderer) report.RegisterHTMLRenderer(FrequencyTelemetryTableName, averageFrequencyTelemetryTableHTMLRenderer) report.RegisterHTMLRenderer(IRQRateTelemetryTableName, irqRateTelemetryTableHTMLRenderer) report.RegisterHTMLRenderer(DriveTelemetryTableName, driveTelemetryTableHTMLRenderer) @@ -364,7 +364,6 @@ func getTableValues(allTableValues []table.TableValues, tableName string) table. func summaryFromTableValues(allTableValues []table.TableValues, _ map[string]script.ScriptOutput) table.TableValues { cpuUtil := getCPUAveragePercentage(getTableValues(allTableValues, UtilizationCategoriesTelemetryTableName), "%idle", true) ipc := getCPUAveragePercentage(getTableValues(allTableValues, IPCTelemetryTableName), "Core (Avg.)", false) - c6 := getCPUAveragePercentage(getTableValues(allTableValues, C6TelemetryTableName), "Core (Avg.)", false) avgCoreFreq := getMetricAverage(getTableValues(allTableValues, FrequencyTelemetryTableName), []string{"Core (Avg.)"}, "Time") pkgPower := getPkgAveragePower(allTableValues) pkgTemperature := getPkgAverageTemperature(allTableValues) @@ -386,7 +385,6 @@ func summaryFromTableValues(allTableValues []table.TableValues, _ map[string]scr Fields: []table.Field{ {Name: "CPU Utilization (%)", Values: []string{cpuUtil}}, {Name: "IPC", Values: []string{ipc}}, - {Name: "C6 Core Residency (%)", Values: []string{c6}}, {Name: "Core Frequency (MHz)", Values: []string{avgCoreFreq}}, {Name: "Package Power (Watts)", Values: []string{pkgPower}}, {Name: "Package Temperature (C)", Values: []string{pkgTemperature}}, diff --git a/cmd/telemetry/telemetry_renderers.go b/cmd/telemetry/telemetry_renderers.go index c4f16032..4bd995d2 100644 --- a/cmd/telemetry/telemetry_renderers.go +++ b/cmd/telemetry/telemetry_renderers.go @@ -531,7 +531,7 @@ func ipcTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName str return telemetryTableHTMLRenderer(tableValues, data, datasetNames, chartConfig, nil) } -func c6TelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { +func cstatesTelemetryTableHTMLRenderer(tableValues table.TableValues, targetName string) string { if len(tableValues.Fields) < 2 { slog.Error("insufficient fields in table, expected at least 2", slog.String("table", tableValues.Name), slog.Int("fields", len(tableValues.Fields))) return "" @@ -559,7 +559,7 @@ func c6TelemetryTableHTMLRenderer(tableValues table.TableValues, targetName stri chartConfig := report.ChartTemplateStruct{ ID: fmt.Sprintf("%s%d", tableValues.Name, util.RandUint(10000)), XaxisText: "Time", - YaxisText: "% C6 Residency", + YaxisText: "% Residency", TitleText: "", DisplayTitle: "false", DisplayLegend: "true", diff --git a/cmd/telemetry/telemetry_tables.go b/cmd/telemetry/telemetry_tables.go index e34135b7..6e5952a7 100644 --- a/cmd/telemetry/telemetry_tables.go +++ b/cmd/telemetry/telemetry_tables.go @@ -22,7 +22,7 @@ const ( CPUUtilizationTelemetryTableName = "CPU Utilization Telemetry" UtilizationCategoriesTelemetryTableName = "Utilization Categories Telemetry" IPCTelemetryTableName = "IPC Telemetry" - C6TelemetryTableName = "C6 Telemetry" + CstatesTelemetryTableName = "C-States Telemetry" FrequencyTelemetryTableName = "Frequency Telemetry" IRQRateTelemetryTableName = "IRQ Rate Telemetry" InstructionTelemetryTableName = "Instruction Telemetry" @@ -41,7 +41,7 @@ const ( CPUUtilizationTelemetryMenuLabel = "CPU Utilization" UtilizationCategoriesTelemetryMenuLabel = "Utilization Categories" IPCTelemetryMenuLabel = "IPC" - C6TelemetryMenuLabel = "C6" + CstatesTelemetryMenuLabel = "C-States" FrequencyTelemetryMenuLabel = "Frequency" IRQRateTelemetryMenuLabel = "IRQ Rate" InstructionTelemetryMenuLabel = "Instruction" @@ -84,15 +84,15 @@ var tableDefinitions = map[string]table.TableDefinition{ script.TurbostatTelemetryScriptName, }, FieldsFunc: ipcTelemetryTableValues}, - C6TelemetryTableName: { - Name: C6TelemetryTableName, - MenuLabel: C6TelemetryMenuLabel, + CstatesTelemetryTableName: { + Name: CstatesTelemetryTableName, + MenuLabel: CstatesTelemetryMenuLabel, Architectures: []string{cpus.X86Architecture}, HasRows: true, ScriptNames: []string{ script.TurbostatTelemetryScriptName, }, - FieldsFunc: c6TelemetryTableValues}, + FieldsFunc: cstatesTelemetryTableValues}, FrequencyTelemetryTableName: { Name: FrequencyTelemetryTableName, MenuLabel: FrequencyTelemetryMenuLabel, @@ -510,13 +510,12 @@ func ipcTelemetryTableValues(outputs map[string]script.ScriptOutput) []table.Fie return fields } -func c6TelemetryTableValues(outputs map[string]script.ScriptOutput) []table.Field { - fields := []table.Field{ - {Name: "Time"}, - {Name: "Package (Avg.)"}, - {Name: "Core (Avg.)"}, - } - platformRows, err := extract.TurbostatPlatformRows(outputs[script.TurbostatTelemetryScriptName].Stdout, []string{"C6%", "CPU%c6"}) +func cstatesTelemetryTableValues(outputs map[string]script.ScriptOutput) []table.Field { + fields := []table.Field{} + // e.g., SRF - C1% C1E% C6S% C6SP% CPU%c1 CPU%c6 Mod%c6 + // e.g., GNR - POLL% C1% C1E% C6% C6P% CPU%c1 CPU%c6 + reCstate := regexp.MustCompile(`^(C\d+\w%|CPU%c\d+|POLL%|Mod%c\d+)$`) // matches C1%, C1E%, C6%, C6S%, C6P%, CPU%c1, CPU%c6, C1E%, C6P%, POLL%, Mod%c6 + platformRows, err := extract.TurbostatPlatformRowsByRegexMatch(outputs[script.TurbostatTelemetryScriptName].Stdout, []*regexp.Regexp{reCstate}) if err != nil { slog.Warn(err.Error()) return []table.Field{} @@ -525,14 +524,21 @@ func c6TelemetryTableValues(outputs map[string]script.ScriptOutput) []table.Fiel slog.Warn("no platform rows found in turbostat telemetry output") return []table.Field{} } + // dynamically build the fields based on the cstate-related fields we found in the platform rows + // e.g., if we found "C6%" and "CPU%c6", we'll create two fields, "C6%" and "CPU%c6" // for each platform row for i := range platformRows { - // append the timestamp to the fields - fields[0].Values = append(fields[0].Values, platformRows[i][0]) // Timestamp - // append the C6 residency values to the fields - fields[1].Values = append(fields[1].Values, platformRows[i][1]) // C6% - // append the CPU C6 residency values to the fields - fields[2].Values = append(fields[2].Values, platformRows[i][2]) // CPU%c6 + // the first row contains the field names, so use that to build the fields, including the timestamp field + if i == 0 { + for _, fieldName := range platformRows[i] { + fields = append(fields, table.Field{Name: fieldName}) + } + } else { + // append the values to the corresponding fields + for j := range platformRows[i] { + fields[j].Values = append(fields[j].Values, platformRows[i][j]) + } + } } return fields } diff --git a/internal/extract/turbostat.go b/internal/extract/turbostat.go index 898c2d6d..dbc828aa 100644 --- a/internal/extract/turbostat.go +++ b/internal/extract/turbostat.go @@ -6,6 +6,7 @@ package extract import ( "fmt" "log/slog" + "regexp" "slices" "strconv" "strings" @@ -77,6 +78,73 @@ func parseTurbostatOutput(output string) ([]map[string]string, error) { return rows, nil } +// TurbostatPlatformRowsByRegexMatch parses the output of the turbostat script and returns the rows +// for the platform (summary) only, matching fields by regex. +// Multiple fields may match the regex, all matching fields, and their values, will be returned in +// the order they appear in the output. +// Returns: +// - [][]string: first row is the header with "timestamp" followed by matched field names, subsequent +// rows contain the corresponding values for each platform row in the output. +func TurbostatPlatformRowsByRegexMatch(turboStatScriptOutput string, fieldRegexs []*regexp.Regexp) ([][]string, error) { + if turboStatScriptOutput == "" { + return nil, fmt.Errorf("turbostat output is empty") + } + if len(fieldRegexs) == 0 { + return nil, fmt.Errorf("no field regexes provided") + } + rows, err := parseTurbostatOutput(turboStatScriptOutput) + if err != nil { + return nil, fmt.Errorf("unable to parse turbostat output: %w", err) + } + if len(rows) == 0 { + return nil, fmt.Errorf("no platform rows found in turbostat output") + } + // Determine which fields match any of the regexes, preserving column order + // from the first platform row. + var matchedFields []string + for _, row := range rows { + if !isPlatformRow(row) { + continue + } + for field := range row { + for _, re := range fieldRegexs { + if re.MatchString(field) { + if !slices.Contains(matchedFields, field) { + matchedFields = append(matchedFields, field) + } + break + } + } + } + break // only need the first platform row to discover fields + } + if len(matchedFields) == 0 { + return nil, fmt.Errorf("no fields matched the provided regexes in turbostat output") + } + // Sort alphabetically for deterministic output since map iteration is unordered. + slices.Sort(matchedFields) + // First row is the header: timestamp followed by matched field names. + header := make([]string, len(matchedFields)+1) + header[0] = "timestamp" + copy(header[1:], matchedFields) + fieldValues := [][]string{header} + for _, row := range rows { + if !isPlatformRow(row) { + continue + } + rowValues := make([]string, len(matchedFields)+1) + rowValues[0] = row["timestamp"] + for i, field := range matchedFields { + rowValues[i+1] = row[field] + } + fieldValues = append(fieldValues, rowValues) + } + if len(fieldValues) == 1 { // only the header row, no data + return nil, fmt.Errorf("no platform data found in turbostat output for the provided regexes") + } + return fieldValues, nil +} + // TurbostatPlatformRows parses the output of the turbostat script and returns the rows // for the platform (summary) only. func TurbostatPlatformRows(turboStatScriptOutput string, fieldNames []string) ([][]string, error) { From 8f580c1d920a7e39029aafb1268cb35d2e146def Mon Sep 17 00:00:00 2001 From: Jason Harper <78619061+harp-intel@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:43:41 -0800 Subject: [PATCH 3/4] improve cstate field name regex Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/telemetry/telemetry_tables.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/telemetry/telemetry_tables.go b/cmd/telemetry/telemetry_tables.go index 6e5952a7..5a68093a 100644 --- a/cmd/telemetry/telemetry_tables.go +++ b/cmd/telemetry/telemetry_tables.go @@ -514,7 +514,7 @@ func cstatesTelemetryTableValues(outputs map[string]script.ScriptOutput) []table fields := []table.Field{} // e.g., SRF - C1% C1E% C6S% C6SP% CPU%c1 CPU%c6 Mod%c6 // e.g., GNR - POLL% C1% C1E% C6% C6P% CPU%c1 CPU%c6 - reCstate := regexp.MustCompile(`^(C\d+\w%|CPU%c\d+|POLL%|Mod%c\d+)$`) // matches C1%, C1E%, C6%, C6S%, C6P%, CPU%c1, CPU%c6, C1E%, C6P%, POLL%, Mod%c6 + reCstate := regexp.MustCompile(`^(C\d+\w*%|CPU%c\d+|POLL%|Mod%c\d+)$`) // matches C1%, C1E%, C6%, C6S%, C6P%, CPU%c1, CPU%c6, C1E%, C6P%, POLL%, Mod%c6 platformRows, err := extract.TurbostatPlatformRowsByRegexMatch(outputs[script.TurbostatTelemetryScriptName].Stdout, []*regexp.Regexp{reCstate}) if err != nil { slog.Warn(err.Error()) From 5e9ab7e99caecffd1371e322c9e9e5fd1eb2b65f Mon Sep 17 00:00:00 2001 From: "Harper, Jason M" Date: Tue, 3 Mar 2026 20:00:18 -0800 Subject: [PATCH 4/4] fix comment and error Signed-off-by: Harper, Jason M --- internal/extract/turbostat.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/extract/turbostat.go b/internal/extract/turbostat.go index dbc828aa..da14d137 100644 --- a/internal/extract/turbostat.go +++ b/internal/extract/turbostat.go @@ -81,7 +81,7 @@ func parseTurbostatOutput(output string) ([]map[string]string, error) { // TurbostatPlatformRowsByRegexMatch parses the output of the turbostat script and returns the rows // for the platform (summary) only, matching fields by regex. // Multiple fields may match the regex, all matching fields, and their values, will be returned in -// the order they appear in the output. +// alphabetical order. // Returns: // - [][]string: first row is the header with "timestamp" followed by matched field names, subsequent // rows contain the corresponding values for each platform row in the output. @@ -97,15 +97,16 @@ func TurbostatPlatformRowsByRegexMatch(turboStatScriptOutput string, fieldRegexs return nil, fmt.Errorf("unable to parse turbostat output: %w", err) } if len(rows) == 0 { - return nil, fmt.Errorf("no platform rows found in turbostat output") + return nil, fmt.Errorf("no rows found in turbostat output") } - // Determine which fields match any of the regexes, preserving column order - // from the first platform row. + // Build our list of field names var matchedFields []string + foundPlatformRow := false for _, row := range rows { if !isPlatformRow(row) { continue } + foundPlatformRow = true for field := range row { for _, re := range fieldRegexs { if re.MatchString(field) { @@ -118,6 +119,9 @@ func TurbostatPlatformRowsByRegexMatch(turboStatScriptOutput string, fieldRegexs } break // only need the first platform row to discover fields } + if !foundPlatformRow { + return nil, fmt.Errorf("no platform rows found in turbostat output") + } if len(matchedFields) == 0 { return nil, fmt.Errorf("no fields matched the provided regexes in turbostat output") }