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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@ If neither sudo nor root access is available, an administrator must apply the fo

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

##### Refining Metrics to a Specific Time Range
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.

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.

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.

**Examples:**
<pre>
# Skip the first 10 seconds and last 5 seconds
$ ./perfspect metrics trim --input perfspect_2025-11-28_09-21-56 --start-offset 10 --end-offset 5

# Use absolute timestamps (seconds since epoch)
$ ./perfspect metrics trim --input perfspect_2025-11-28_09-21-56 --start-time 1764174327 --end-time 1764174351
</pre>

##### Prometheus Endpoint
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.

Expand Down Expand Up @@ -149,7 +165,7 @@ $ ./perfspect metrics --syslog
</pre>

##### Report Files
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.
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.
<pre>
$./perfspect telemetry --output /home/elaine/perfspect/telemetry
</pre>
Expand Down
7 changes: 7 additions & 0 deletions cmd/metrics/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,12 +539,19 @@ func (md Metadata) String() string {
return string(jsonData)
}

func (md Metadata) Initialized() bool {
return md.SocketCount != 0 && md.CoresPerSocket != 0
}

// JSON converts the Metadata struct to a JSON-encoded byte slice.
//
// Returns:
// - out: JSON-encoded byte slice representation of the Metadata.
// - err: error encountered during the marshaling process, if any.
func (md Metadata) JSON() (out []byte, err error) {
if !md.Initialized() {
return []byte("null"), nil
}
if out, err = json.Marshal(md); err != nil {
slog.Error("failed to marshal metadata structure", slog.String("error", err.Error()))
return
Expand Down
4 changes: 4 additions & 0 deletions cmd/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ func usageFunc(cmd *cobra.Command) error {
cmd.Printf(" --%-20s %s%s\n", flag.Name, flag.Help, flagDefault)
}
}
cmd.Printf("\nSubcommands:\n")
for _, subCmd := range cmd.Commands() {
cmd.Printf(" %s: %s\n", subCmd.Name(), subCmd.Short)
}
cmd.Println("\nGlobal Flags:")
cmd.Parent().PersistentFlags().VisitAll(func(pf *pflag.Flag) {
flagDefault := ""
Expand Down
4 changes: 2 additions & 2 deletions cmd/metrics/resources/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,7 @@
</TableRow>
</TableHead>
<TableBody>
{system_info.map(([key, value]) => (
{system_info && system_info.map(([key, value]) => (
<TableRow key={key}>
<TableCell sx={{ fontFamily: 'Monospace' }} component="th" scope="row" >
{JSON.stringify(key)}
Expand All @@ -994,7 +994,7 @@
</TableRow>
</TableHead>
<TableBody>
{Object.entries(metadata).sort(([key1], [key2]) => key1.localeCompare(key2)).map(([key, value]) => (
{metadata && Object.entries(metadata).sort(([key1], [key2]) => key1.localeCompare(key2)).map(([key, value]) => (
<TableRow key={key}>
<TableCell sx={{ fontFamily: 'Monospace' }} component="th" scope="row" >
{JSON.stringify(key)}
Expand Down
48 changes: 39 additions & 9 deletions cmd/metrics/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,29 @@ import (
"github.com/casbin/govaluate"
)

// summarizeMetrics reads the metrics CSV from localOutputDir for targetName,
// generates summary files (CSV and HTML) using the provided metadata and metric definitions,
// and returns a list of created summary file paths.
func summarizeMetrics(localOutputDir string, targetName string, metadata Metadata, metricDefinitions []MetricDefinition) ([]string, error) {
return summarizeMetricsWithTrim(localOutputDir, localOutputDir, targetName, metadata, metricDefinitions, 0, 0)
}
func summarizeMetricsWithTrim(localInputDir, localOutputDir, targetName string, metadata Metadata, metricDefinitions []MetricDefinition, startTimestamp, endTimestamp int) ([]string, error) {
filesCreated := []string{}
// read the metrics from CSV
csvMetricsFile := filepath.Join(localOutputDir, targetName+"_metrics.csv")
csvMetricsFile := filepath.Join(localInputDir, targetName+"_metrics.csv")
metrics, err := newMetricCollection(csvMetricsFile)
if err != nil {
return filesCreated, fmt.Errorf("failed to read metrics from %s: %w", csvMetricsFile, err)
}
// exclude the final sample if metrics were collected with a workload
if metadata.WithWorkload {
metrics.excludeFinalSample()
if startTimestamp != 0 || endTimestamp != 0 {
// trim the metrics to the specified time range
metrics.filterByTimeRange(startTimestamp, endTimestamp)
} else {
// trim time range not specified,
// exclude the final sample if metrics were collected with a workload
if metadata.WithWorkload {
metrics.excludeFinalSample()
}
}
// csv summary
out, err := metrics.getCSV()
Expand Down Expand Up @@ -74,7 +86,7 @@ type metricStats struct {
}

type row struct {
timestamp float64
timestamp int
socket string
cpu string
cgroup string
Expand All @@ -87,8 +99,8 @@ func newRow(fields []string, names []string) (r row, err error) {
for fIdx, field := range fields {
switch fIdx {
case idxTimestamp:
var ts float64
if ts, err = strconv.ParseFloat(field, 64); err != nil {
var ts int
if ts, err = strconv.Atoi(field); err != nil {
return
}
r.timestamp = ts
Expand Down Expand Up @@ -336,8 +348,8 @@ func (mc MetricCollection) aggregate() (m *MetricGroup, err error) {
groupByValue: "",
}
// aggregate the rows by timestamp
timestampMap := make(map[float64][]map[string]float64) // map of timestamp to list of metric maps
var timestamps []float64 // list of timestamps in order
timestampMap := make(map[int][]map[string]float64) // map of timestamp to list of metric maps
var timestamps []int // list of timestamps in order
for _, metrics := range mc {
for _, row := range metrics.rows {
if _, ok := timestampMap[row.timestamp]; !ok {
Expand Down Expand Up @@ -816,3 +828,21 @@ func (mc MetricCollection) getCSV() (out string, err error) {
}
return
}

// filterByTimeRange filters all metric groups to only include rows within the specified time range
func (mc MetricCollection) filterByTimeRange(startTime, endTime int) {
for i := range mc {
mc[i].filterByTimeRange(startTime, endTime)
}
}

// filterByTimeRange filters the metric group to only include rows within the specified time range
func (mg *MetricGroup) filterByTimeRange(startTime, endTime int) {
var filteredRows []row
for _, row := range mg.rows {
if row.timestamp >= startTime && row.timestamp <= endTime {
filteredRows = append(filteredRows, row)
}
}
mg.rows = filteredRows
}
48 changes: 24 additions & 24 deletions cmd/metrics/summary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,46 @@ func TestExcludeFinalSample(t *testing.T) {
name string
inputRows []row
expectedCount int
expectedMaxTS float64
expectedMaxTS int
}{
{
name: "exclude single final timestamp",
inputRows: []row{
{timestamp: 5.0, metrics: map[string]float64{"metric1": 100.0}},
{timestamp: 10.0, metrics: map[string]float64{"metric1": 200.0}},
{timestamp: 15.0, metrics: map[string]float64{"metric1": 150.0}},
{timestamp: 20.0, metrics: map[string]float64{"metric1": 50.0}}, // this should be excluded
{timestamp: 5, metrics: map[string]float64{"metric1": 100.0}},
{timestamp: 10, metrics: map[string]float64{"metric1": 200.0}},
{timestamp: 15, metrics: map[string]float64{"metric1": 150.0}},
{timestamp: 20, metrics: map[string]float64{"metric1": 50.0}}, // this should be excluded
},
expectedCount: 3,
expectedMaxTS: 15.0,
expectedMaxTS: 15,
},
{
name: "exclude multiple rows with same final timestamp",
inputRows: []row{
{timestamp: 5.0, socket: "0", metrics: map[string]float64{"metric1": 100.0}},
{timestamp: 10.0, socket: "0", metrics: map[string]float64{"metric1": 200.0}},
{timestamp: 15.0, socket: "0", metrics: map[string]float64{"metric1": 150.0}},
{timestamp: 15.0, socket: "1", metrics: map[string]float64{"metric1": 160.0}}, // same timestamp, different socket
{timestamp: 5, socket: "0", metrics: map[string]float64{"metric1": 100.0}},
{timestamp: 10, socket: "0", metrics: map[string]float64{"metric1": 200.0}},
{timestamp: 15, socket: "0", metrics: map[string]float64{"metric1": 150.0}},
{timestamp: 15, socket: "1", metrics: map[string]float64{"metric1": 160.0}}, // same timestamp, different socket
},
expectedCount: 2,
expectedMaxTS: 10.0,
expectedMaxTS: 10,
},
{
name: "single sample - should not exclude",
inputRows: []row{
{timestamp: 5.0, metrics: map[string]float64{"metric1": 100.0}},
{timestamp: 5, metrics: map[string]float64{"metric1": 100.0}},
},
expectedCount: 1,
expectedMaxTS: 5.0,
expectedMaxTS: 5,
},
{
name: "two samples - exclude last one",
inputRows: []row{
{timestamp: 5.0, metrics: map[string]float64{"metric1": 100.0}},
{timestamp: 10.0, metrics: map[string]float64{"metric1": 50.0}},
{timestamp: 5, metrics: map[string]float64{"metric1": 100.0}},
{timestamp: 10, metrics: map[string]float64{"metric1": 50.0}},
},
expectedCount: 1,
expectedMaxTS: 5.0,
expectedMaxTS: 5,
},
}

Expand Down Expand Up @@ -93,19 +93,19 @@ func TestExcludeFinalSampleMultipleGroups(t *testing.T) {
groupByField: "SKT",
groupByValue: "0",
rows: []row{
{timestamp: 5.0, socket: "0", metrics: map[string]float64{"metric1": 100.0}},
{timestamp: 10.0, socket: "0", metrics: map[string]float64{"metric1": 200.0}},
{timestamp: 15.0, socket: "0", metrics: map[string]float64{"metric1": 50.0}}, // should be excluded
{timestamp: 5, socket: "0", metrics: map[string]float64{"metric1": 100.0}},
{timestamp: 10, socket: "0", metrics: map[string]float64{"metric1": 200.0}},
{timestamp: 15, socket: "0", metrics: map[string]float64{"metric1": 50.0}}, // should be excluded
},
},
MetricGroup{
names: []string{"metric1"},
groupByField: "SKT",
groupByValue: "1",
rows: []row{
{timestamp: 5.0, socket: "1", metrics: map[string]float64{"metric1": 110.0}},
{timestamp: 10.0, socket: "1", metrics: map[string]float64{"metric1": 210.0}},
{timestamp: 15.0, socket: "1", metrics: map[string]float64{"metric1": 60.0}}, // should be excluded
{timestamp: 5, socket: "1", metrics: map[string]float64{"metric1": 110.0}},
{timestamp: 10, socket: "1", metrics: map[string]float64{"metric1": 210.0}},
{timestamp: 15, socket: "1", metrics: map[string]float64{"metric1": 60.0}}, // should be excluded
},
},
}
Expand All @@ -117,8 +117,8 @@ func TestExcludeFinalSampleMultipleGroups(t *testing.T) {
assert.Equal(t, 2, len(mc[1].rows), "socket 1 should have 2 rows")

// Verify max timestamps
assert.Equal(t, 10.0, mc[0].rows[1].timestamp, "socket 0 max timestamp should be 10.0")
assert.Equal(t, 10.0, mc[1].rows[1].timestamp, "socket 1 max timestamp should be 10.0")
assert.Equal(t, 10, mc[0].rows[1].timestamp, "socket 0 max timestamp should be 10")
assert.Equal(t, 10, mc[1].rows[1].timestamp, "socket 1 max timestamp should be 10")
}

func TestExcludeFinalSampleEmptyCollection(t *testing.T) {
Expand Down
Loading