diff --git a/cmd/metrics/resources/base.html b/cmd/metrics/resources/base.html
index 14e6b8bc..b63a9909 100644
--- a/cmd/metrics/resources/base.html
+++ b/cmd/metrics/resources/base.html
@@ -169,7 +169,31 @@
const description = <<.DESCRIPTION>>
const metadata = <<.METADATA>>
const system_summary = <<.SYSTEMSUMMARY>>
-
+ // per-group (per-socket, per-CPU, or per-cgroup) mean metric values. These are
+ // empty when metrics were collected at system scope and system granularity.
+ const group_by_field = <<.GROUPBYFIELD>>
+ const groups = <<.GROUPS>>
+ const group_means = <<.GROUPMEANS>>
+ const hasGroups = groups.length > 0 && group_means.length > 0;
+ // labels for the per-group metrics tab, by the field the metrics are grouped by
+ const groupLabels = {
+ SKT: { tab: "Per-Socket Metrics", plural: "sockets" },
+ CPU: { tab: "Per-CPU Metrics", plural: "CPUs" },
+ CID: { tab: "Per-Cgroup Metrics", plural: "cgroups" },
+ }[group_by_field] || { tab: "Per-Group Metrics", plural: "groups" };
+ // Tab indexes. The per-group metrics tab is present only when there is per-group
+ // data to present, so the tabs that follow it shift accordingly.
+ const tabIndex = {
+ tmam: 0,
+ cpu: 1,
+ memory: 2,
+ power: 3,
+ allMetrics: 4,
+ groupMetrics: hasGroups ? 5 : -1,
+ systemSummary: hasGroups ? 6 : 5,
+ metadata: hasGroups ? 7 : 6,
+ };
+
// Check for highlighted metrics whenever current_metrics changes
React.useEffect(() => {
// Look for any metrics that exceed their thresholds
@@ -177,8 +201,59 @@
setHasHighlightedMetrics(hasHighlighted);
}, [current_metrics]);
+ // the common style for informational tooltips
+ const infoTooltipProps = {
+ tooltip: {
+ sx: {
+ fontSize: '1.0rem',
+ padding: '8px 12px',
+ bgcolor: 'rgba(97, 97, 97, 0.92)',
+ color: '#ffffff',
+ fontWeight: 400,
+ boxShadow: '0px 2px 6px rgba(0, 0, 0, 0.2)'
+ }
+ }
+ };
+
+ // MetricName component - the metric's name, preceded by an info button carrying the
+ // metric's description and, for TMA metrics, indentation indicating the metric's level
+ const MetricName = ({ name, level }) => {
+ const indentationLevel = level > 1 ? level - 1 : 0;
+ return (
+
+
+
+ info
+
+
+ {indentationLevel > 0 &&
+
+
+ subdirectory_arrow_right
+
+
+ }
+ {name}
+
+ );
+ };
+
// MetricsTable component
- const MetricsTable = ({
+ const MetricsTable = ({
data,
filterPrefix = null,
showComparison = false,
@@ -251,54 +326,7 @@
}}
>
-
-
- info
-
-
- {(() => {
- const level = row[7] ? Number(row[7]) : 1;
- const indentationLevel = level > 1 ? level - 1 : 0;
-
- const indentation = indentationLevel > 0 ?
- 0 ? '1px dotted #aaa' : 'none',
- marginLeft: indentationLevel > 0 ? '4px' : '0',
- }}>
- {indentationLevel > 0 &&
- subdirectory_arrow_right
- }
- : null;
-
- return (
-
- {indentation}
- {row[0]}
-
- );
- })()}
+
);
};
+ // GroupMeansTable component - one row per metric and one column per group, e.g., per
+ // cgroup, holding the mean value of the metric for that group over the collection period
+ const GroupMeansTable = () => {
+ // group values can be long, e.g., cgroup paths, so shorten them for the column
+ // headers. The full value is available in the header's tooltip.
+ const shortGroupName = (value) => {
+ const leaf = value.split("/").filter(Boolean).pop() || value;
+ return leaf.length > 28 ? leaf.slice(0, 25) + "..." : leaf;
+ };
+ // keep the metric name visible while scrolling the group columns horizontally
+ const stickyCell = {
+ position: "sticky",
+ left: 0,
+ backgroundColor: "background.paper",
+ };
+ return (
+
+
+
+
+ Metric
+ {groups.map(([value, sampleCount]) => (
+
+
+
+ {shortGroupName(value)}
+
+
+
+ ))}
+
+
+
+ {group_means.map((row) => (
+
+
+
+
+ {groups.map(([value], idx) => (
+
+ {row[idx + 2] === "" ? "—" : Number(row[idx + 2]).toFixed(4)}
+
+ ))}
+
+ ))}
+
+
+
+ );
+ };
// Define consistent colors for TMA categories with child variations
const tmaColors = {
// Blue family for Frontend
@@ -701,6 +790,7 @@
+ {hasGroups && }
@@ -708,7 +798,7 @@
@@ -790,7 +880,7 @@
@@ -847,7 +937,7 @@
@@ -891,7 +981,7 @@
@@ -935,7 +1025,7 @@
+ {hasGroups &&
+
+
+ {`Mean value of each metric for each of the ${groups.length} ${groupLabels.plural} monitored during the collection period. Hover over a column heading for its full name and the number of samples collected for it. A dash indicates that no valid samples were collected for the metric.`}
+
+
+
+ }
@@ -983,7 +1084,7 @@
diff --git a/cmd/metrics/summary.go b/cmd/metrics/summary.go
index fafbd686..c99c51dc 100644
--- a/cmd/metrics/summary.go
+++ b/cmd/metrics/summary.go
@@ -383,18 +383,33 @@ func (mc MetricCollection) aggregate() (m *MetricGroup, err error) {
return
}
-// getHTML - generate a string containing HTML representing the metrics
-func (mg *MetricGroup) getHTML(metadata Metadata, metricDefinitions []MetricDefinition) (out string, err error) {
- var htmlTemplateBytes []byte
- if htmlTemplateBytes, err = resources.ReadFile("resources/base.html"); err != nil {
- slog.Error("failed to read base.html template", slog.String("error", err.Error()))
+// getHTML - generate a string containing HTML representing the metrics. The report's
+// charts and All Metrics tab present the aggregate of all metric groups. When the
+// collection holds more than one group, e.g., one per cgroup, the report additionally
+// presents the mean value of each metric for each group.
+func (mc MetricCollection) getHTML(metadata Metadata, metricDefinitions []MetricDefinition) (out string, err error) {
+ if len(mc) == 0 {
+ err = fmt.Errorf("no metrics to summarize")
return
}
- templateVals, err := mg.loadHTMLTemplateValues(metadata, metricDefinitions)
+ aggregated, err := mc.aggregate()
+ if err != nil {
+ return
+ }
+ templateVals, err := aggregated.loadHTMLTemplateValues(metadata, metricDefinitions)
if err != nil {
slog.Error("failed to load template values", slog.String("error", err.Error()))
return
}
+ if err = mc.addGroupTemplateValues(templateVals, metricDefinitions); err != nil {
+ slog.Error("failed to load per-group template values", slog.String("error", err.Error()))
+ return
+ }
+ var htmlTemplateBytes []byte
+ if htmlTemplateBytes, err = resources.ReadFile("resources/base.html"); err != nil {
+ slog.Error("failed to read base.html template", slog.String("error", err.Error()))
+ return
+ }
fg := texttemplate.Must(texttemplate.New("metricsSummaryTemplate").Delims("<<", ">>").Parse(string(htmlTemplateBytes)))
buf := new(bytes.Buffer)
if err = fg.Execute(buf, templateVals); err != nil {
@@ -404,20 +419,85 @@ func (mg *MetricGroup) getHTML(metadata Metadata, metricDefinitions []MetricDefi
return buf.String(), nil
}
-func (mc MetricCollection) getHTML(metadata Metadata, metricDefinitions []MetricDefinition) (out string, err error) {
- if len(mc) == 0 {
- err = fmt.Errorf("no metrics to summarize")
- return
+// addGroupTemplateValues adds the template values that describe the mean value of each
+// metric for each group in the collection, i.e., for each socket, CPU, or cgroup. These
+// drive the report's per-group metrics tab.
+//
+// When the collection holds a single group, i.e., system scope and system granularity,
+// the values are empty and the report does not present the per-group tab. The values are
+// always set, since an unset template value renders as nothing, which would produce
+// invalid JavaScript in the report.
+func (mc MetricCollection) addGroupTemplateValues(templateVals map[string]string, metricDefinitions []MetricDefinition) error {
+ // defaults, used when there is no per-group data to present
+ templateVals["GROUPBYFIELD"] = `""`
+ templateVals["GROUPS"] = "[]"
+ templateVals["GROUPMEANS"] = "[]"
+ if len(mc) < 2 {
+ return nil
+ }
+ // all groups must present the same metrics, in the same order, for the values to line
+ // up with the metric names in the report's table
+ metricNames := mc[0].names
+ for idx, mg := range mc[1:] {
+ if !slices.Equal(mg.names, metricNames) {
+ slog.Warn("metric groups have different metric names or order, omitting per-group metrics from report",
+ slog.Int("group", idx+1), slog.String("groupByValue", mg.groupByValue))
+ return nil
+ }
+ }
+ // the group values, e.g., the cgroup IDs, along with the number of samples collected
+ // for each. Groups can come and go during collection, e.g., when the list of "hot"
+ // cgroups is refreshed, so the sample counts provide context for the mean values.
+ groups := make([][]string, 0, len(mc))
+ allStats := make([]map[string]metricStats, 0, len(mc))
+ for idx, mg := range mc {
+ if mg.groupByValue == "" {
+ slog.Warn("metric group has no group value, omitting per-group metrics from report", slog.Int("group", idx))
+ return nil
+ }
+ groups = append(groups, []string{mg.groupByValue, fmt.Sprintf("%d", len(mg.rows))})
+ stats, err := mg.getStats()
+ if err != nil {
+ return fmt.Errorf("failed to get stats for metric group %d: %w", idx, err)
+ }
+ allStats = append(allStats, stats)
}
- if len(mc) == 1 {
- return mc[0].getHTML(metadata, metricDefinitions)
+ // one row per metric: the metric name, the metric level, then the mean value from
+ // each group, in the same order as the groups
+ groupMeans := make([][]string, 0, len(metricNames))
+ for _, name := range metricNames {
+ level := 1
+ if metricDef := findMetricDefinitionByName(name, metricDefinitions); metricDef != nil {
+ level = max(metricDef.Level, 1)
+ }
+ metricVals := []string{name, fmt.Sprintf("%d", level)}
+ for _, stats := range allStats {
+ mean := stats[name].mean
+ if math.IsNaN(mean) || math.IsInf(mean, 0) {
+ // no valid samples for this metric in this group
+ metricVals = append(metricVals, "")
+ } else {
+ metricVals = append(metricVals, fmt.Sprintf("%f", mean))
+ }
+ }
+ groupMeans = append(groupMeans, metricVals)
}
- metrics, err := mc.aggregate()
+ groupByFieldBytes, err := json.Marshal(mc[0].groupByField)
if err != nil {
- return
+ return err
}
- out, err = metrics.getHTML(metadata, metricDefinitions)
- return
+ groupsBytes, err := json.Marshal(groups)
+ if err != nil {
+ return err
+ }
+ groupMeansBytes, err := json.Marshal(groupMeans)
+ if err != nil {
+ return err
+ }
+ templateVals["GROUPBYFIELD"] = string(groupByFieldBytes)
+ templateVals["GROUPS"] = string(groupsBytes)
+ templateVals["GROUPMEANS"] = string(groupMeansBytes)
+ return nil
}
type tmaTip struct {