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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions cmd/metrics/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ import (

// MetricDefinition is the common (across loader implementations) representation of a single metric
type MetricDefinition struct {
Name string `json:"name"`
Expression string `json:"expression"`
Description string `json:"description"`
Name string
LegacyName string
Expression string
Description string
Category string
Level int
ThresholdExpression string
// Evaluation fields - used during metric expression evaluation
//
// Variables - map of variable names found in Expression to the indices of the event
Expand All @@ -26,14 +30,19 @@ type MetricDefinition struct {
// definitions are loaded and parsed, so that the expression does not need to be
// parsed each time the metric is evaluated.
Evaluable *govaluate.EvaluableExpression
// ThresholdVariables - list of variable names found in ThresholdExpression.
ThresholdVariables []string
// ThresholdEvaluable - parsed threshold expression from govaluate. These are set once when the metric
// definitions are loaded and parsed, so that the expression does not need to be
// parsed each time the metric is evaluated.
ThresholdEvaluable *govaluate.EvaluableExpression
}

// EventDefinition is the common (across loader implementations) representation of a single perf event
type EventDefinition struct {
Raw string // the event string in perf format
Name string // the event name
Device string // the event device (e.g., "cpu" from cpu/event=0x3c/,umask=...)
Description string // the event description
Raw string // the event string in perf format
Name string // the event name
Device string // the event device (e.g., "cpu" from cpu/event=0x3c/,umask=...), currently used by legacy loader only
}

// GroupDefinition represents a group of perf events
Expand Down
4 changes: 4 additions & 0 deletions cmd/metrics/loader_legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func loadMetricDefinitions(metricDefinitionOverridePath string, selectedMetrics
if err = json.Unmarshal(bytes, &metricsInFile); err != nil {
return
}
// set LegacyName to Name for all metrics
for i := range metricsInFile {
metricsInFile[i].LegacyName = metricsInFile[i].Name
}
// if a list of metric names provided, reduce list to match
if len(selectedMetrics) > 0 {
// confirm provided metric names are valid (included in metrics defined in file)
Expand Down
102 changes: 75 additions & 27 deletions cmd/metrics/loader_perfmon.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ type MetricsConfigHeader struct {
}
type PerfspectMetric struct {
MetricName string `json:"MetricName"`
LegacyName string `json:"LegacyName"`
Origin string `json:"Origin"`
}
type MetricsConfig struct {
Expand All @@ -74,10 +73,6 @@ func (l *PerfmonLoader) Load(loaderConfig LoaderConfig) ([]MetricDefinition, []G
if err != nil {
return nil, nil, fmt.Errorf("failed to load metrics config: %w", err)
}
reportMetrics, err := filterReportMetrics(config.ReportMetrics, loaderConfig.SelectedMetrics)
if err != nil {
return nil, nil, fmt.Errorf("error filtering report metrics: %w", err)
}
// Load the perfmon metric definitions from the JSON file
perfmonMetricDefinitions, err := loadPerfmonMetricsFromFile(config.PerfmonMetricsFile)
if err != nil {
Expand Down Expand Up @@ -111,6 +106,11 @@ func (l *PerfmonLoader) Load(loaderConfig LoaderConfig) ([]MetricDefinition, []G
if err != nil {
return nil, nil, fmt.Errorf("error loading Perfspect metrics from file: %w", err)
}
// Filter the report metrics based on the selected metrics
reportMetrics, err := filterReportMetrics(config.ReportMetrics, loaderConfig.SelectedMetrics, append(perfmonMetricDefinitions.Metrics, perfspectMetrics.Metrics...))
if err != nil {
return nil, nil, fmt.Errorf("error filtering report metrics: %w", err)
}
// Combine the PerfSpect-defined metrics with the metric definitions from perfmon and filter based on report metrics
// Creates one list of all metrics to be used in the loader
perfmonMetrics, err := loadPerfmonMetrics(reportMetrics, perfmonMetricDefinitions.Metrics, perfspectMetrics.Metrics, alternateTMAMetrics.Metrics, loaderConfig.Metadata)
Expand Down Expand Up @@ -226,24 +226,29 @@ func loadPerfmonMetricsFromFile(pathWithSource string) (PerfmonMetrics, error) {
return metrics, nil
}

func filterReportMetrics(reportMetrics []PerfspectMetric, selectedMetricNames []string) ([]PerfspectMetric, error) {
func filterReportMetrics(reportMetrics []PerfspectMetric, selectedMetricNames []string, perfmonMetrics []PerfmonMetric) ([]PerfspectMetric, error) {
if len(selectedMetricNames) == 0 {
slog.Debug("No selected metrics provided, using all report metrics")
return reportMetrics, nil
}
slog.Debug("Filtering report metrics based on selected metrics", slog.Any("selectedMetrics", selectedMetricNames))
var filteredMetrics []PerfspectMetric
for _, metricName := range selectedMetricNames {
found := false
for _, metric := range reportMetrics {
if metric.LegacyName == "metric_"+metricName {
filteredMetrics = append(filteredMetrics, metric)
found = true
break
pm, pmFound := findPerfmonMetricByLegacyName(perfmonMetrics, "metric_"+metricName)
if pmFound {
found := false
for _, metric := range reportMetrics {
if pm.MetricName == metric.MetricName {
filteredMetrics = append(filteredMetrics, metric)
found = true
break
}
}
}
if !found {
return nil, fmt.Errorf("unknown metric: %s", metricName)
if !found {
return nil, fmt.Errorf("metric not found in perfspect metrics: %s", metricName)
}
} else {
return nil, fmt.Errorf("metric not found in perfmon metrics: %s", metricName)
}
}
return filteredMetrics, nil
Expand All @@ -256,13 +261,13 @@ func loadPerfmonMetrics(reportMetrics []PerfspectMetric, perfmonMetrics []Perfmo
var perfmonMetric *PerfmonMetric
var found bool
if !metadata.SupportsFixedTMA {
perfmonMetric, found = findPerfmonMetric(alternateTMAMetrics, metric.LegacyName)
perfmonMetric, found = findPerfmonMetric(alternateTMAMetrics, metric.MetricName)
}
if !found {
perfmonMetric, found = findPerfmonMetric(allPerfmonMetrics, metric.LegacyName)
perfmonMetric, found = findPerfmonMetric(allPerfmonMetrics, metric.MetricName)
}
if !found {
slog.Warn("Metric not found in metric definitions", "metric", metric.LegacyName, "origin", metric.Origin)
slog.Warn("Metric not found in metric definitions", "metric", metric.MetricName, "origin", metric.Origin)
continue
}
// Add the metric to the list of metrics to return
Expand Down Expand Up @@ -332,20 +337,53 @@ func getExpression(perfmonMetric PerfmonMetric) (string, error) {
return expression, nil
}

func getThresholdExpression(perfmonMetric PerfmonMetric) (string, error) {
if perfmonMetric.Threshold == nil {
return "", nil // no threshold defined
}
// SRF metrics are currently defined using an older perfmon format that doesn't
// define threshold metrics. We could parse them out manually, but for now we'll just skip them.
if perfmonMetric.Threshold.ThresholdMetrics == nil {
return "", nil // no threshold defined
}
expression := perfmonMetric.Threshold.Formula
if expression == "" {
return "", nil // no threshold defined
}
replacers := make(map[string]string)
for _, thresholdMetric := range perfmonMetric.Threshold.ThresholdMetrics {
replacers[thresholdMetric["Alias"]] = fmt.Sprintf("[%s]", thresholdMetric["Value"])
}
for alias, replacement := range replacers {
// Replace alias as whole words only (not substrings)
expression = util.ReplaceWholeWord(expression, alias, replacement)
}
return expression, nil
}

func perfmonToMetricDefs(perfmonMetrics []PerfmonMetric) ([]MetricDefinition, error) {
var metrics []MetricDefinition
for _, perfmonMetric := range perfmonMetrics {
// get the expression for the metric
expression, err := getExpression(perfmonMetric)
if err != nil {
slog.Warn("Failed getting expression for metric", "metric", perfmonMetric.LegacyName, "error", err)
slog.Warn("Failed getting expression for metric", "metric", perfmonMetric.MetricName, "error", err)
continue
}
thresholdExpression, err := getThresholdExpression(perfmonMetric)
if err != nil {
slog.Warn("Failed getting threshold expression for metric", "metric", perfmonMetric.MetricName, "error", err)
continue
}
// create a MetricDefinition from the perfmon metric
metric := MetricDefinition{
Name: perfmonMetric.LegacyName,
Description: perfmonMetric.BriefDescription,
Expression: expression,
Name: perfmonMetric.MetricName,
LegacyName: perfmonMetric.LegacyName,
Description: perfmonMetric.BriefDescription,
Category: perfmonMetric.Category,
Level: perfmonMetric.Level,
Expression: expression,
ThresholdExpression: thresholdExpression,
}
// add the metric to the list of metrics
metrics = append(metrics, metric)
Expand All @@ -363,7 +401,7 @@ func removeUncollectableMetrics(perfmonMetrics []PerfmonMetric, coreEvents CoreE
}
uncollectableEvents := getUncollectableEvents(eventNames, coreEvents, uncoreEvents, otherEvents, metadata)
if len(uncollectableEvents) > 0 {
slog.Warn("Metric contains uncollectable events", "metric", perfmonMetric.LegacyName, "uncollectableEvents", uncollectableEvents)
slog.Warn("Metric contains uncollectable events", "metric", perfmonMetric.MetricName, "uncollectableEvents", uncollectableEvents)
continue
}
// if the metric is collectable, add it to the list of collectable metrics
Expand All @@ -389,19 +427,19 @@ func loadEventGroupsFromMetrics(perfmonMetrics []PerfmonMetric, coreEvents CoreE
uncollectableEvents = util.UniqueAppend(uncollectableEvents, uncollectableMetricEvents...)
// skip metrics that have uncollectable events
if len(uncollectableMetricEvents) > 0 {
slog.Warn("Metric contains uncollectable events", "metric", perfmonMetric.LegacyName, "uncollectableEvents", uncollectableMetricEvents)
slog.Warn("Metric contains uncollectable events", "metric", perfmonMetric.MetricName, "uncollectableEvents", uncollectableMetricEvents)
continue
}
metricCoreGroups, metricUncoreGroups, metricOtherGroups, err := groupsFromEventNames(
perfmonMetric.LegacyName,
perfmonMetric.MetricName,
metricEventNames,
coreEvents,
uncoreEvents,
otherEvents,
metadata,
)
if err != nil {
slog.Error("Error creating groups from event names", "metric", perfmonMetric.LegacyName, "error", err)
slog.Error("Error creating groups from event names", "metric", perfmonMetric.MetricName, "error", err)
continue
}
// Add the groups to the main lists
Expand Down Expand Up @@ -565,7 +603,17 @@ func groupsFromEventNames(metricName string, eventNames []string, coreEvents Cor
// findPerfmonMetric -- Helper function to find a metric by name
func findPerfmonMetric(metricsList []PerfmonMetric, metricName string) (*PerfmonMetric, bool) {
for _, metric := range metricsList {
if metric.LegacyName == metricName {
if metric.MetricName == metricName {
return &metric, true
}
}
return nil, false
}

// findPerfmonMetricByLegacyName -- Helper function to find a metric by legacy name
func findPerfmonMetricByLegacyName(metricsList []PerfmonMetric, legacyName string) (*PerfmonMetric, bool) {
for _, metric := range metricsList {
if metric.LegacyName == legacyName {
return &metric, true
}
}
Expand Down
67 changes: 59 additions & 8 deletions cmd/metrics/loader_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package metrics
import (
"fmt"
"log/slog"
"perfspect/internal/util"
"regexp"
"strings"

Expand Down Expand Up @@ -54,6 +55,12 @@ func configureMetrics(metrics []MetricDefinition, uncollectableEvents []string,
return nil, fmt.Errorf("failed to initialize metric variables: %w", err)
}

// initialize threshold variables
metrics, err = initializeThresholdVariables(metrics)
if err != nil {
return nil, fmt.Errorf("failed to initialize threshold variables: %w", err)
}

// remove "metric_" prefix from metric names
metrics, err = removeMetricsPrefix(metrics)
if err != nil {
Expand Down Expand Up @@ -200,23 +207,37 @@ func removeIfUncollectableEvents(metrics []MetricDefinition, uncollectableEvents
return filteredMetrics, nil
}

func transformExpression(expression string) (string, error) {
// transform if/else to ?/:
transformed, err := perfmonToPerfspectConditional(expression)
if err != nil {
return "", fmt.Errorf("failed to transform metric expression: %w", err)
}
// replace "> =" with ">=" and "< =" with "<="
transformed = strings.ReplaceAll(transformed, "> =", ">=")
transformed = strings.ReplaceAll(transformed, "< =", "<=")
// replace "&" with "&&" and "|" with "||"
transformed = strings.ReplaceAll(transformed, " & ", " && ")
transformed = strings.ReplaceAll(transformed, " | ", " || ")
return transformed, nil
}

// transformMetricExpressions transforms metric expressions from perfmon format to perfspect format
// it replaces if/else with ternary conditional, replaces "> =" with ">=", and "< =" with "<="
func transformMetricExpressions(metrics []MetricDefinition) ([]MetricDefinition, error) {
// transform if/else to ?/:
var transformedMetrics []MetricDefinition
for i := range metrics {
metric := &metrics[i]
transformed, err := perfmonToPerfspectConditional(metric.Expression)
var err error
metric.Expression, err = transformExpression(metric.Expression)
if err != nil {
return nil, fmt.Errorf("failed to transform metric expression: %w", err)
}
// replace "> =" with ">=" and "< =" with "<="
transformed = strings.ReplaceAll(transformed, "> =", ">=")
transformed = strings.ReplaceAll(transformed, "< =", "<=")
if transformed != metric.Expression {
slog.Debug("transformed metric", slog.String("name", metric.Name), slog.String("transformed", transformed))
metric.Expression = transformed
if metric.ThresholdExpression != "" {
metric.ThresholdExpression, err = transformExpression(metric.ThresholdExpression)
if err != nil {
return nil, fmt.Errorf("failed to transform threshold expression: %w", err)
}
}
// add the transformed metric to the list
transformedMetrics = append(transformedMetrics, *metric)
Expand All @@ -235,6 +256,12 @@ func setEvaluableExpressions(metrics []MetricDefinition) ([]MetricDefinition, er
slog.Error("failed to create evaluable expression for metric", slog.String("error", err.Error()), slog.String("name", metric.Name), slog.String("expression", metric.Expression))
return nil, err
}
if metric.ThresholdExpression != "" {
if metric.ThresholdEvaluable, err = govaluate.NewEvaluableExpressionWithFunctions(metric.ThresholdExpression, evaluatorFunctions); err != nil {
slog.Error("failed to create threshold evaluable expression for metric", slog.String("error", err.Error()), slog.String("name", metric.Name), slog.String("threshold_expression", metric.ThresholdExpression))
return nil, err
}
}
}
return metrics, nil
}
Expand Down Expand Up @@ -287,6 +314,30 @@ func initializeMetricVariables(metrics []MetricDefinition) ([]MetricDefinition,
return metrics, nil
}

func initializeThresholdVariables(metrics []MetricDefinition) ([]MetricDefinition, error) {
// get a list of the variables in the threshold expression
for i := range metrics {
metric := &metrics[i]
metric.ThresholdVariables = []string{}
expressionIdx := 0
for {
startVar := strings.IndexRune(metric.ThresholdExpression[expressionIdx:], '[')
if startVar == -1 { // no more vars in this expression
break
}
endVar := strings.IndexRune(metric.ThresholdExpression[expressionIdx:], ']')
if endVar == -1 {
return nil, fmt.Errorf("didn't find end of variable indicator (]) in threshold expression: %s", metric.ThresholdExpression[expressionIdx:])
}
// add the variable name to the list if it is not already present
varName := metric.ThresholdExpression[expressionIdx:][startVar+1 : endVar]
metric.ThresholdVariables = util.UniqueAppend(metric.ThresholdVariables, varName)
expressionIdx += endVar + 1
}
}
return metrics, nil
}

// removeMetricsPrefix removes the "metric_" prefix from metric names
// this is done to make the names more readable and consistent with other metrics
// it is assumed that all metric names start with "metric_"
Expand Down
9 changes: 5 additions & 4 deletions cmd/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,10 +819,10 @@ func processRawData(localOutputDir string) error {
if err != nil {
return err
}
filesWritten = printMetrics(metricFrames, frameCount, metadata.Hostname, metadata.CollectionStartTime, localOutputDir)
filesWritten = printMetrics(metricFrames, frameCount, metricDefinitions, metadata.Hostname, metadata.CollectionStartTime, localOutputDir)
frameCount += len(metricFrames)
}
summaryFiles, err := summarizeMetrics(localOutputDir, metadata.Hostname, metadata)
summaryFiles, err := summarizeMetrics(localOutputDir, metadata.Hostname, metadata, metricDefinitions)
if err != nil {
return err
}
Expand Down Expand Up @@ -1043,7 +1043,8 @@ func runCmd(cmd *cobra.Command, args []string) error {
for _, targetContext := range targetContexts {
fmt.Printf("\nMetrics available on %s:\n", targetContext.target.GetName())
for _, metric := range targetContext.metricDefinitions {
fmt.Printf("\"%s\"\n", metric.Name)
name, _ := strings.CutPrefix(metric.LegacyName, "metric_")
fmt.Printf("\"%s\"\n", name)
}
}
return nil
Expand Down Expand Up @@ -1103,7 +1104,7 @@ func runCmd(cmd *cobra.Command, args []string) error {
_ = multiSpinner.Status(targetContext.target.GetName(), "no metrics collected")
} else {
targetContext.metadata.PerfSpectVersion = appContext.Version
summaryFiles, err := summarizeMetrics(localOutputDir, targetContext.target.GetName(), targetContext.metadata)
summaryFiles, err := summarizeMetrics(localOutputDir, targetContext.target.GetName(), targetContext.metadata, targetContext.metricDefinitions)
if err != nil {
err = fmt.Errorf("failed to summarize metrics: %w", err)
exitErrs = append(exitErrs, err)
Expand Down
Loading