Skip to content

Commit

Permalink
Refactor percentile calculation function to improve performance
Browse files Browse the repository at this point in the history
  • Loading branch information
oleg-ssvlabs committed Sep 3, 2024
1 parent 4914550 commit ae63953
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 45 deletions.
2 changes: 1 addition & 1 deletion internal/benchmark/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func LoadEnabledMetrics(config configs.Config) map[metric.Group][]metricService
"Latency",
time.Second*3,
[]metric.HealthCondition[time.Duration]{
{Name: consensus.DurationMeasurement, Threshold: time.Second, Operator: metric.OperatorGreaterThanOrEqual, Severity: metric.SeverityHigh},
{Name: consensus.DurationP90Measurement, Threshold: time.Second, Operator: metric.OperatorGreaterThanOrEqual, Severity: metric.SeverityHigh},
}))
}

Expand Down
50 changes: 35 additions & 15 deletions internal/benchmark/metrics/consensus/latency.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ import (
)

const (
DurationMeasurement = "Duration"
DurationMinMeasurement = "DurationMin"
DurationP10Measurement = "DurationP10"
DurationP50Measurement = "DurationP50"
DurationP90Measurement = "DurationP90"
DurationMaxMeasurement = "DurationMax"
)

type LatencyMetric struct {
metric.Base[time.Duration]
url string
interval time.Duration
url string
interval time.Duration
durations []time.Duration
}

func NewLatencyMetric(url, name string, interval time.Duration, healthCondition []metric.HealthCondition[time.Duration]) *LatencyMetric {
Expand Down Expand Up @@ -52,11 +57,13 @@ func (l *LatencyMetric) measure(ctx context.Context) {

ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, l.url, nil)
if err != nil {
logger.WriteError(metric.ConsensusGroup, l.Name, err)
return
}

res, err := http.DefaultClient.Do(req)
if err != nil {
logger.WriteError(metric.ConsensusGroup, l.Name, err)
Expand All @@ -68,22 +75,35 @@ func (l *LatencyMetric) measure(ctx context.Context) {

latency = end.Sub(start)

l.durations = append(l.durations, latency)

percentiles := metric.CalculatePercentiles(l.durations, 0, 10, 50, 90, 100)

l.AddDataPoint(map[string]time.Duration{
DurationMeasurement: latency,
DurationMinMeasurement: percentiles[0],
DurationP10Measurement: percentiles[10],
DurationP50Measurement: percentiles[50],
DurationP90Measurement: percentiles[90],
DurationMaxMeasurement: percentiles[100],
})

logger.WriteMetric(metric.ConsensusGroup, l.Name, map[string]any{DurationMeasurement: latency})
logger.WriteMetric(metric.ConsensusGroup, l.Name, map[string]any{
DurationMinMeasurement: percentiles[0],
DurationP10Measurement: percentiles[10],
DurationP50Measurement: percentiles[50],
DurationP90Measurement: percentiles[90],
DurationMaxMeasurement: percentiles[100],
})
}

func (l *LatencyMetric) AggregateResults() string {
var values []time.Duration
for _, point := range l.DataPoints {
values = append(values, point.Values[DurationMeasurement])
}
return metric.FormatPercentiles(
metric.CalculatePercentile(values, 0),
metric.CalculatePercentile(values, 10),
metric.CalculatePercentile(values, 50),
metric.CalculatePercentile(values, 90),
metric.CalculatePercentile(values, 100))
var min, p10, p50, p90, max time.Duration

min = l.DataPoints[len(l.DataPoints)-1].Values[DurationMinMeasurement]
p10 = l.DataPoints[len(l.DataPoints)-1].Values[DurationP10Measurement]
p50 = l.DataPoints[len(l.DataPoints)-1].Values[DurationP50Measurement]
p90 = l.DataPoints[len(l.DataPoints)-1].Values[DurationP90Measurement]
max = l.DataPoints[len(l.DataPoints)-1].Values[DurationMaxMeasurement]

return metric.FormatPercentiles(min, p10, p50, p90, max)
}
13 changes: 8 additions & 5 deletions internal/benchmark/metrics/consensus/peers.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,13 @@ func (p *PeerMetric) AggregateResults() string {
for _, point := range p.DataPoints {
values = append(values, point.Values[PeerCountMeasurement])
}

percentiles := metric.CalculatePercentiles(values, 0, 10, 50, 90, 100)

return metric.FormatPercentiles(
metric.CalculatePercentile(values, 0),
metric.CalculatePercentile(values, 10),
metric.CalculatePercentile(values, 50),
metric.CalculatePercentile(values, 90),
metric.CalculatePercentile(values, 100))
percentiles[0],
percentiles[10],
percentiles[50],
percentiles[90],
percentiles[100])
}
13 changes: 8 additions & 5 deletions internal/benchmark/metrics/execution/peers.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,13 @@ func (p *PeerMetric) AggregateResults() string {
for _, point := range p.DataPoints {
values = append(values, point.Values[PeerCountMeasurement])
}

percentiles := metric.CalculatePercentiles(values, 0, 10, 50, 90, 100)

return metric.FormatPercentiles(
metric.CalculatePercentile(values, 0),
metric.CalculatePercentile(values, 10),
metric.CalculatePercentile(values, 50),
metric.CalculatePercentile(values, 90),
metric.CalculatePercentile(values, 100))
percentiles[0],
percentiles[10],
percentiles[50],
percentiles[90],
percentiles[100])
}
4 changes: 2 additions & 2 deletions internal/benchmark/metrics/infrastructure/cpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,6 @@ func (c *CPUMetric) AggregateResults() string {
}

return fmt.Sprintf("user_P50=%.2f%%, system_P50=%.2f%%, total=%v",
metric.CalculatePercentile(values[UserCPUMeasurement], 50),
metric.CalculatePercentile(values[SystemCPUMeasurement], 50), c.total)
metric.CalculatePercentiles(values[UserCPUMeasurement], 50)[50],
metric.CalculatePercentiles(values[SystemCPUMeasurement], 50)[50], c.total)
}
8 changes: 4 additions & 4 deletions internal/benchmark/metrics/infrastructure/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ func (m *MemoryMetric) AggregateResults() string {
}

return fmt.Sprintf("total_P50=%.2fMB, used_P50=%.2fMB, cached_P50=%.2fMB, free_P50=%.2fMB",
metric.CalculatePercentile(values[TotalMemoryMeasurement], 50),
metric.CalculatePercentile(values[UsedMemoryMeasurement], 50),
metric.CalculatePercentile(values[CachedMemoryMeasurement], 50),
metric.CalculatePercentile(values[FreeMemoryMeasurement], 50))
metric.CalculatePercentiles(values[TotalMemoryMeasurement], 50)[50],
metric.CalculatePercentiles(values[UsedMemoryMeasurement], 50)[50],
metric.CalculatePercentiles(values[CachedMemoryMeasurement], 50)[50],
metric.CalculatePercentiles(values[FreeMemoryMeasurement], 50)[50])
}

func toMegabytes(bytes uint64) float64 {
Expand Down
12 changes: 8 additions & 4 deletions internal/benchmark/metrics/ssv/connections.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,13 @@ func (p *ConnectionsMetric) AggregateResults() string {
measurements[OutboundConnectionsMeasurement] = append(measurements[OutboundConnectionsMeasurement], point.Values[OutboundConnectionsMeasurement])
}

inboundPercentiles := metric.CalculatePercentiles(measurements[InboundConnectionsMeasurement], 0, 50)
outboundPercentiles := metric.CalculatePercentiles(measurements[OutboundConnectionsMeasurement], 0, 50)

return fmt.Sprintf("inbound_min=%d, inbound_P50=%d, outbound_min=%d, outbound_P50=%d",
metric.CalculatePercentile(measurements[InboundConnectionsMeasurement], 0),
metric.CalculatePercentile(measurements[InboundConnectionsMeasurement], 50),
metric.CalculatePercentile(measurements[OutboundConnectionsMeasurement], 0),
metric.CalculatePercentile(measurements[OutboundConnectionsMeasurement], 50))
inboundPercentiles[0],
inboundPercentiles[50],
outboundPercentiles[0],
outboundPercentiles[50],
)
}
13 changes: 8 additions & 5 deletions internal/benchmark/metrics/ssv/peers.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,13 @@ func (p *PeerMetric) AggregateResults() string {
for _, point := range p.DataPoints {
values = append(values, point.Values[PeerCountMeasurement])
}

percentiles := metric.CalculatePercentiles(values, 0, 10, 50, 90, 100)

return metric.FormatPercentiles(
metric.CalculatePercentile(values, 0),
metric.CalculatePercentile(values, 10),
metric.CalculatePercentile(values, 50),
metric.CalculatePercentile(values, 90),
metric.CalculatePercentile(values, 100))
percentiles[0],
percentiles[10],
percentiles[50],
percentiles[90],
percentiles[100])
}
20 changes: 16 additions & 4 deletions internal/platform/metric/percentile.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,31 @@ type (
}
)

func CalculatePercentile[T Metricable](values []T, percentile float64) T {
func CalculatePercentiles[T Metricable](values []T, percentiles ...float64) map[float64]T {
result := make(map[float64]T)

if len(percentiles) == 0 {
return result
}

if len(values) == 0 {
var zero T
return zero
for _, p := range percentiles {
result[p] = zero
}
return result
}

sort.Slice(values, func(i, j int) bool {
return values[i] < values[j]
})

index := int(float64(len(values)-1) * percentile / 100.0)
for _, percentile := range percentiles {
index := int(float64(len(values)-1) * percentile / 100.0)
result[percentile] = values[index]
}

return values[index]
return result
}

func FormatPercentiles[T stringable](min, p10, p50, p90, max T) string {
Expand Down
48 changes: 48 additions & 0 deletions internal/platform/metric/percentile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package metric

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestGivenRangeOfPercentilesWhenCalculatePercentilesThenCalculatesCorrectly(t *testing.T) {
tests := []struct {
name string
values []int
percentiles []float64
expected map[float64]int
}{
{
name: "Valid percentiles",
values: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
percentiles: []float64{10, 50, 90},
expected: map[float64]int{10: 1, 50: 5, 90: 9},
},
{
name: "No percentiles provided",
values: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
percentiles: []float64{},
expected: map[float64]int{},
},
{
name: "Empty values list",
values: []int{},
percentiles: []float64{10, 50, 90},
expected: map[float64]int{10: 0, 50: 0, 90: 0},
},
{
name: "Empty values and no percentiles",
values: []int{},
percentiles: []float64{},
expected: map[float64]int{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CalculatePercentiles(tt.values, tt.percentiles...)
assert.Equal(t, tt.expected, result)
})
}
}

0 comments on commit ae63953

Please sign in to comment.