diff --git a/test/metricsviz/BUILD b/test/metricsviz/BUILD index abf7b628d..8ab4b7849 100644 --- a/test/metricsviz/BUILD +++ b/test/metricsviz/BUILD @@ -10,6 +10,7 @@ go_library( testonly = 1, srcs = [ "metricsviz.go", + "metricsviz_groups.go", "metricsviz_publish.go", ], visibility = ["//:sandbox"], diff --git a/test/metricsviz/metricsviz.go b/test/metricsviz/metricsviz.go index bc6035065..0844b0b04 100644 --- a/test/metricsviz/metricsviz.go +++ b/test/metricsviz/metricsviz.go @@ -22,6 +22,7 @@ import ( "fmt" "hash/adler32" "regexp" + "slices" "sort" "strconv" "strings" @@ -122,8 +123,8 @@ type HTMLOptions struct { } type chart struct { - PageTitle string - Series []*TimeSeries + Title string + Series []*TimeSeries } // isCumulative returns whether the given series are all cumulative or all @@ -290,16 +291,7 @@ func (c *chart) Charter() (components.Charter, error) { charts.WithLabelOpts(opts.Label{Show: false}), ) } - metricNames := map[MetricName]struct{}{} - for _, ts := range c.Series { - metricNames[ts.Metric.Name] = struct{}{} - } - metricNameList := make([]string, 0, len(metricNames)) - for m := range metricNames { - metricNameList = append(metricNameList, string(m)) - } - sort.Strings(metricNameList) - chartTitle := fmt.Sprintf("%s: %s", c.PageTitle, strings.Join(metricNameList, ", ")) + chartTitle := c.Title if isCumulative { chartTitle += " per second" yAxis.Name = "per second" @@ -343,24 +335,64 @@ func (d *Data) ToHTML(opts HTMLOptions) (string, error) { page.PageTitle = fmt.Sprintf("Metrics for %s at %v", opts.Title, opts.When.Format(time.DateTime)) page.Theme = echartstypes.ThemeVintage page.SetLayout(components.PageFlexLayout) - titleToChart := make(map[string]*chart) - var titles []string - for maf, ts := range d.data { - title := string(maf.MetricName) - c, ok := titleToChart[title] - if !ok { - c = &chart{PageTitle: opts.Title} - titleToChart[title] = c - titles = append(titles, title) + + // Find which groups contain which metrics that we're seeing in the data. + groupsToMetricNames := make(map[GroupName][]MetricName, len(d.data)) + for maf := range d.data { + for groupName, metricsInGroup := range Groups { + if slices.Contains(metricsInGroup, maf.MetricName) && !slices.Contains(groupsToMetricNames[groupName], maf.MetricName) { + groupsToMetricNames[groupName] = append(groupsToMetricNames[groupName], maf.MetricName) + } } - c.Series = append(c.Series, ts) } - sort.Strings(titles) - for _, title := range titles { - c := titleToChart[title] + + // Find which groups for which we have data for at least 2 metrics. + // These metrics will be displayed in the group charts only. + // The rest of the metrics will be displayed in their own chart. + metricToGroups := make(map[MetricName][]GroupName, len(d.data)) + for groupName, activeGroupMetrics := range groupsToMetricNames { + if len(activeGroupMetrics) >= 2 { + for _, m := range activeGroupMetrics { + metricToGroups[m] = append(metricToGroups[m], groupName) + } + } + } + + // Now go through the data and group it by the chart it'll end up in. + chartNameToChart := make(map[string]*chart, len(d.data)) + var chartNames []string + for maf, ts := range d.data { + groupsForMetric := metricToGroups[maf.MetricName] + if len(groupsForMetric) > 0 { + // Group metric chart. + for _, groupName := range groupsForMetric { + chartName := string(groupName) + c, ok := chartNameToChart[chartName] + if !ok { + c = &chart{Title: fmt.Sprintf("%s: %s", opts.Title, groupName)} + chartNameToChart[chartName] = c + chartNames = append(chartNames, chartName) + } + c.Series = append(c.Series, ts) + } + } else { + // Individual metric chart. + chartName := string(maf.MetricName) + c, ok := chartNameToChart[chartName] + if !ok { + c = &chart{Title: fmt.Sprintf("%s: %s", opts.Title, ts.String())} + chartNameToChart[chartName] = c + chartNames = append(chartNames, chartName) + } + c.Series = append(c.Series, ts) + } + } + sort.Strings(chartNames) + for _, chartName := range chartNames { + c := chartNameToChart[chartName] charter, err := c.Charter() if err != nil { - return "", fmt.Errorf("failed to create charter for %q: %w", title, err) + return "", fmt.Errorf("failed to create charter for %q: %w", chartName, err) } page.AddCharts(charter) } diff --git a/test/metricsviz/metricsviz_groups.go b/test/metricsviz/metricsviz_groups.go new file mode 100644 index 000000000..bd9e501f3 --- /dev/null +++ b/test/metricsviz/metricsviz_groups.go @@ -0,0 +1,56 @@ +// Copyright 2024 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metricsviz + +// GroupName is the name of a group of metrics. +type GroupName string + +// Groups maps metrics which are in the same named group. +// If more than one metric in a group is in the profiled data, it will +// be shown on the same graph. +// A metric may be in multiple groups. +var Groups = map[GroupName][]MetricName{ + "Network packets": { + "/netstack/dropped_packets", + "/netstack/nic/malformed_l4_received_packets", + "/netstack/nic/tx/packets", + "/netstack/nic/tx_packets_dropped_no_buffer_space", + "/netstack/nic/rx/packets", + "/netstack/nic/disabled_rx/packets", + }, + "Network throughput": { + "/netstack/nic/tx/bytes", + "/netstack/nic/rx/bytes", + "/netstack/nic/disabled_rx/bytes", + }, + "IP packets": { + "/netstack/ip/packets_received", + "/netstack/ip/disabled_packets_received", + "/netstack/ip/invalid_addresses_received", + "/netstack/ip/invalid_source_addresses_received", + "/netstack/ip/packets_delivered", + "/netstack/ip/packets_sent", + "/netstack/ip/outgoing_packet_errors", + "/netstack/ip/malformed_packets_received", + "/netstack/ip/malformed_fragments_received", + "/netstack/ip/iptables/prerouting_dropped", + "/netstack/ip/iptables/input_dropped", + "/netstack/ip/iptables/output_dropped", + "/netstack/ip/options/timestamp_received", + "/netstack/ip/options/record_route_received", + "/netstack/ip/options/router_alert_received", + "/netstack/ip/options/unknown_received", + }, +}