Skip to content

Commit ded2a2b

Browse files
authored
Trim metrics summary reports to user-specified time range (#573)
* add metrics trim command Signed-off-by: Harper, Jason M <[email protected]> * simplify Signed-off-by: Harper, Jason M <[email protected]> * remove commented code Signed-off-by: Harper, Jason M <[email protected]> * align README to trim functionality Signed-off-by: Harper, Jason M <[email protected]> * put new files in new output dir Signed-off-by: Harper, Jason M <[email protected]> --------- Signed-off-by: Harper, Jason M <[email protected]>
1 parent f1435e0 commit ded2a2b

File tree

7 files changed

+492
-36
lines changed

7 files changed

+492
-36
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ If neither sudo nor root access is available, an administrator must apply the fo
5252

5353
Once the configuration changes are applied, use the `--noroot` flag on the command line, for example, `perfspect metrics --noroot`.
5454

55+
##### Refining Metrics to a Specific Time Range
56+
After collecting metrics, you can generate new summary reports for a specific time interval using the `metrics trim` subcommand. This is useful when you've collected metrics for an entire workload but want to analyze only a specific portion, excluding setup, teardown, or other unwanted phases.
57+
58+
The time range can be specified using either absolute timestamps (seconds since epoch) or relative offsets from the beginning/end of the data. At least one time parameter must be specified.
59+
60+
The trimmed CSV and HTML summary files will be placed in a new output directory. The output directory can be specified using the `--output` flag.
61+
62+
**Examples:**
63+
<pre>
64+
# Skip the first 10 seconds and last 5 seconds
65+
$ ./perfspect metrics trim --input perfspect_2025-11-28_09-21-56 --start-offset 10 --end-offset 5
66+
67+
# Use absolute timestamps (seconds since epoch)
68+
$ ./perfspect metrics trim --input perfspect_2025-11-28_09-21-56 --start-time 1764174327 --end-time 1764174351
69+
</pre>
70+
5571
##### Prometheus Endpoint
5672
The `metrics` command can expose metrics via a Prometheus compatible `metrics` endpoint. This allows integration with Prometheus monitoring systems. To enable the Prometheus endpoint, use the `--prometheus-server` flag. By default, the endpoint listens on port 9090. The port can be changed using the `--prometheus-server-addr` flag. Run `perfspect metrics --prometheus-server`. See the [example daemonset](docs/perfspect-daemonset.md) for deploying in Kubernetes.
5773

@@ -149,7 +165,7 @@ $ ./perfspect metrics --syslog
149165
</pre>
150166

151167
##### Report Files
152-
By default, PerfSpect creates a unique directory in the user's current working directory to store output files. Users can specify a custom output directory, but the directory provided must exist; PerfSpect will not create it.
168+
By default, PerfSpect creates a unique directory in the user's current working directory to store output files. Users can specify a custom output directory with the --output flag.
153169
<pre>
154170
$./perfspect telemetry --output /home/elaine/perfspect/telemetry
155171
</pre>

cmd/metrics/metadata.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,12 +539,19 @@ func (md Metadata) String() string {
539539
return string(jsonData)
540540
}
541541

542+
func (md Metadata) Initialized() bool {
543+
return md.SocketCount != 0 && md.CoresPerSocket != 0
544+
}
545+
542546
// JSON converts the Metadata struct to a JSON-encoded byte slice.
543547
//
544548
// Returns:
545549
// - out: JSON-encoded byte slice representation of the Metadata.
546550
// - err: error encountered during the marshaling process, if any.
547551
func (md Metadata) JSON() (out []byte, err error) {
552+
if !md.Initialized() {
553+
return []byte("null"), nil
554+
}
548555
if out, err = json.Marshal(md); err != nil {
549556
slog.Error("failed to marshal metadata structure", slog.String("error", err.Error()))
550557
return

cmd/metrics/metrics.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ func usageFunc(cmd *cobra.Command) error {
265265
cmd.Printf(" --%-20s %s%s\n", flag.Name, flag.Help, flagDefault)
266266
}
267267
}
268+
cmd.Printf("\nSubcommands:\n")
269+
for _, subCmd := range cmd.Commands() {
270+
cmd.Printf(" %s: %s\n", subCmd.Name(), subCmd.Short)
271+
}
268272
cmd.Println("\nGlobal Flags:")
269273
cmd.Parent().PersistentFlags().VisitAll(func(pf *pflag.Flag) {
270274
flagDefault := ""

cmd/metrics/resources/base.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,7 @@
967967
</TableRow>
968968
</TableHead>
969969
<TableBody>
970-
{system_info.map(([key, value]) => (
970+
{system_info && system_info.map(([key, value]) => (
971971
<TableRow key={key}>
972972
<TableCell sx={{ fontFamily: 'Monospace' }} component="th" scope="row" >
973973
{JSON.stringify(key)}
@@ -994,7 +994,7 @@
994994
</TableRow>
995995
</TableHead>
996996
<TableBody>
997-
{Object.entries(metadata).sort(([key1], [key2]) => key1.localeCompare(key2)).map(([key, value]) => (
997+
{metadata && Object.entries(metadata).sort(([key1], [key2]) => key1.localeCompare(key2)).map(([key, value]) => (
998998
<TableRow key={key}>
999999
<TableCell sx={{ fontFamily: 'Monospace' }} component="th" scope="row" >
10001000
{JSON.stringify(key)}

cmd/metrics/summary.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,29 @@ import (
2525
"github.com/casbin/govaluate"
2626
)
2727

28+
// summarizeMetrics reads the metrics CSV from localOutputDir for targetName,
29+
// generates summary files (CSV and HTML) using the provided metadata and metric definitions,
30+
// and returns a list of created summary file paths.
2831
func summarizeMetrics(localOutputDir string, targetName string, metadata Metadata, metricDefinitions []MetricDefinition) ([]string, error) {
32+
return summarizeMetricsWithTrim(localOutputDir, localOutputDir, targetName, metadata, metricDefinitions, 0, 0)
33+
}
34+
func summarizeMetricsWithTrim(localInputDir, localOutputDir, targetName string, metadata Metadata, metricDefinitions []MetricDefinition, startTimestamp, endTimestamp int) ([]string, error) {
2935
filesCreated := []string{}
3036
// read the metrics from CSV
31-
csvMetricsFile := filepath.Join(localOutputDir, targetName+"_metrics.csv")
37+
csvMetricsFile := filepath.Join(localInputDir, targetName+"_metrics.csv")
3238
metrics, err := newMetricCollection(csvMetricsFile)
3339
if err != nil {
3440
return filesCreated, fmt.Errorf("failed to read metrics from %s: %w", csvMetricsFile, err)
3541
}
36-
// exclude the final sample if metrics were collected with a workload
37-
if metadata.WithWorkload {
38-
metrics.excludeFinalSample()
42+
if startTimestamp != 0 || endTimestamp != 0 {
43+
// trim the metrics to the specified time range
44+
metrics.filterByTimeRange(startTimestamp, endTimestamp)
45+
} else {
46+
// trim time range not specified,
47+
// exclude the final sample if metrics were collected with a workload
48+
if metadata.WithWorkload {
49+
metrics.excludeFinalSample()
50+
}
3951
}
4052
// csv summary
4153
out, err := metrics.getCSV()
@@ -74,7 +86,7 @@ type metricStats struct {
7486
}
7587

7688
type row struct {
77-
timestamp float64
89+
timestamp int
7890
socket string
7991
cpu string
8092
cgroup string
@@ -87,8 +99,8 @@ func newRow(fields []string, names []string) (r row, err error) {
8799
for fIdx, field := range fields {
88100
switch fIdx {
89101
case idxTimestamp:
90-
var ts float64
91-
if ts, err = strconv.ParseFloat(field, 64); err != nil {
102+
var ts int
103+
if ts, err = strconv.Atoi(field); err != nil {
92104
return
93105
}
94106
r.timestamp = ts
@@ -336,8 +348,8 @@ func (mc MetricCollection) aggregate() (m *MetricGroup, err error) {
336348
groupByValue: "",
337349
}
338350
// aggregate the rows by timestamp
339-
timestampMap := make(map[float64][]map[string]float64) // map of timestamp to list of metric maps
340-
var timestamps []float64 // list of timestamps in order
351+
timestampMap := make(map[int][]map[string]float64) // map of timestamp to list of metric maps
352+
var timestamps []int // list of timestamps in order
341353
for _, metrics := range mc {
342354
for _, row := range metrics.rows {
343355
if _, ok := timestampMap[row.timestamp]; !ok {
@@ -816,3 +828,21 @@ func (mc MetricCollection) getCSV() (out string, err error) {
816828
}
817829
return
818830
}
831+
832+
// filterByTimeRange filters all metric groups to only include rows within the specified time range
833+
func (mc MetricCollection) filterByTimeRange(startTime, endTime int) {
834+
for i := range mc {
835+
mc[i].filterByTimeRange(startTime, endTime)
836+
}
837+
}
838+
839+
// filterByTimeRange filters the metric group to only include rows within the specified time range
840+
func (mg *MetricGroup) filterByTimeRange(startTime, endTime int) {
841+
var filteredRows []row
842+
for _, row := range mg.rows {
843+
if row.timestamp >= startTime && row.timestamp <= endTime {
844+
filteredRows = append(filteredRows, row)
845+
}
846+
}
847+
mg.rows = filteredRows
848+
}

cmd/metrics/summary_test.go

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,46 +14,46 @@ func TestExcludeFinalSample(t *testing.T) {
1414
name string
1515
inputRows []row
1616
expectedCount int
17-
expectedMaxTS float64
17+
expectedMaxTS int
1818
}{
1919
{
2020
name: "exclude single final timestamp",
2121
inputRows: []row{
22-
{timestamp: 5.0, metrics: map[string]float64{"metric1": 100.0}},
23-
{timestamp: 10.0, metrics: map[string]float64{"metric1": 200.0}},
24-
{timestamp: 15.0, metrics: map[string]float64{"metric1": 150.0}},
25-
{timestamp: 20.0, metrics: map[string]float64{"metric1": 50.0}}, // this should be excluded
22+
{timestamp: 5, metrics: map[string]float64{"metric1": 100.0}},
23+
{timestamp: 10, metrics: map[string]float64{"metric1": 200.0}},
24+
{timestamp: 15, metrics: map[string]float64{"metric1": 150.0}},
25+
{timestamp: 20, metrics: map[string]float64{"metric1": 50.0}}, // this should be excluded
2626
},
2727
expectedCount: 3,
28-
expectedMaxTS: 15.0,
28+
expectedMaxTS: 15,
2929
},
3030
{
3131
name: "exclude multiple rows with same final timestamp",
3232
inputRows: []row{
33-
{timestamp: 5.0, socket: "0", metrics: map[string]float64{"metric1": 100.0}},
34-
{timestamp: 10.0, socket: "0", metrics: map[string]float64{"metric1": 200.0}},
35-
{timestamp: 15.0, socket: "0", metrics: map[string]float64{"metric1": 150.0}},
36-
{timestamp: 15.0, socket: "1", metrics: map[string]float64{"metric1": 160.0}}, // same timestamp, different socket
33+
{timestamp: 5, socket: "0", metrics: map[string]float64{"metric1": 100.0}},
34+
{timestamp: 10, socket: "0", metrics: map[string]float64{"metric1": 200.0}},
35+
{timestamp: 15, socket: "0", metrics: map[string]float64{"metric1": 150.0}},
36+
{timestamp: 15, socket: "1", metrics: map[string]float64{"metric1": 160.0}}, // same timestamp, different socket
3737
},
3838
expectedCount: 2,
39-
expectedMaxTS: 10.0,
39+
expectedMaxTS: 10,
4040
},
4141
{
4242
name: "single sample - should not exclude",
4343
inputRows: []row{
44-
{timestamp: 5.0, metrics: map[string]float64{"metric1": 100.0}},
44+
{timestamp: 5, metrics: map[string]float64{"metric1": 100.0}},
4545
},
4646
expectedCount: 1,
47-
expectedMaxTS: 5.0,
47+
expectedMaxTS: 5,
4848
},
4949
{
5050
name: "two samples - exclude last one",
5151
inputRows: []row{
52-
{timestamp: 5.0, metrics: map[string]float64{"metric1": 100.0}},
53-
{timestamp: 10.0, metrics: map[string]float64{"metric1": 50.0}},
52+
{timestamp: 5, metrics: map[string]float64{"metric1": 100.0}},
53+
{timestamp: 10, metrics: map[string]float64{"metric1": 50.0}},
5454
},
5555
expectedCount: 1,
56-
expectedMaxTS: 5.0,
56+
expectedMaxTS: 5,
5757
},
5858
}
5959

@@ -93,19 +93,19 @@ func TestExcludeFinalSampleMultipleGroups(t *testing.T) {
9393
groupByField: "SKT",
9494
groupByValue: "0",
9595
rows: []row{
96-
{timestamp: 5.0, socket: "0", metrics: map[string]float64{"metric1": 100.0}},
97-
{timestamp: 10.0, socket: "0", metrics: map[string]float64{"metric1": 200.0}},
98-
{timestamp: 15.0, socket: "0", metrics: map[string]float64{"metric1": 50.0}}, // should be excluded
96+
{timestamp: 5, socket: "0", metrics: map[string]float64{"metric1": 100.0}},
97+
{timestamp: 10, socket: "0", metrics: map[string]float64{"metric1": 200.0}},
98+
{timestamp: 15, socket: "0", metrics: map[string]float64{"metric1": 50.0}}, // should be excluded
9999
},
100100
},
101101
MetricGroup{
102102
names: []string{"metric1"},
103103
groupByField: "SKT",
104104
groupByValue: "1",
105105
rows: []row{
106-
{timestamp: 5.0, socket: "1", metrics: map[string]float64{"metric1": 110.0}},
107-
{timestamp: 10.0, socket: "1", metrics: map[string]float64{"metric1": 210.0}},
108-
{timestamp: 15.0, socket: "1", metrics: map[string]float64{"metric1": 60.0}}, // should be excluded
106+
{timestamp: 5, socket: "1", metrics: map[string]float64{"metric1": 110.0}},
107+
{timestamp: 10, socket: "1", metrics: map[string]float64{"metric1": 210.0}},
108+
{timestamp: 15, socket: "1", metrics: map[string]float64{"metric1": 60.0}}, // should be excluded
109109
},
110110
},
111111
}
@@ -117,8 +117,8 @@ func TestExcludeFinalSampleMultipleGroups(t *testing.T) {
117117
assert.Equal(t, 2, len(mc[1].rows), "socket 1 should have 2 rows")
118118

119119
// Verify max timestamps
120-
assert.Equal(t, 10.0, mc[0].rows[1].timestamp, "socket 0 max timestamp should be 10.0")
121-
assert.Equal(t, 10.0, mc[1].rows[1].timestamp, "socket 1 max timestamp should be 10.0")
120+
assert.Equal(t, 10, mc[0].rows[1].timestamp, "socket 0 max timestamp should be 10")
121+
assert.Equal(t, 10, mc[1].rows[1].timestamp, "socket 1 max timestamp should be 10")
122122
}
123123

124124
func TestExcludeFinalSampleEmptyCollection(t *testing.T) {

0 commit comments

Comments
 (0)