Skip to content

Commit a97f187

Browse files
committed
[detector] Add threshold-based analysis settings and detection logic
1 parent f928415 commit a97f187

File tree

5 files changed

+220
-0
lines changed

5 files changed

+220
-0
lines changed

pkg/degradation-detector/analysisSettings.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,23 @@ const (
99
ImprovementEvent
1010
)
1111

12+
// AnalysisKind selects which algorithm to use for detection.
13+
// Default (zero) keeps backward-compatible change-point detection.
14+
type AnalysisKind int
15+
16+
const (
17+
ChangePointAnalysis AnalysisKind = iota
18+
ThresholdAnalysis
19+
)
20+
21+
// ThresholdMode specifies how the latest value should be compared to the threshold.
22+
type ThresholdMode int
23+
24+
const (
25+
ThresholdGreaterThan ThresholdMode = iota
26+
ThresholdLessThan
27+
)
28+
1229
type AnalysisSettings struct {
1330
ReportType ReportType
1431
// Determines the minimum length of a segment the larger the segment the more accurate the analysis but it will take more time to detect degradation
@@ -24,6 +41,10 @@ type AnalysisSettings struct {
2441
// Number of days to check for missing data.
2542
// The default value is -3 (3 days ago).
2643
DaysToCheckMissing int
44+
45+
AnalysisKind AnalysisKind
46+
ThresholdMode ThresholdMode
47+
ThresholdValue float64
2748
}
2849

2950
func (s AnalysisSettings) GetReportType() ReportType {
@@ -48,3 +69,8 @@ func (s AnalysisSettings) GetDaysToCheckMissing() int {
4869
}
4970
return s.DaysToCheckMissing
5071
}
72+
73+
// New getters to satisfy the interface and allow branching
74+
func (s AnalysisSettings) GetAnalysisKind() AnalysisKind { return s.AnalysisKind }
75+
func (s AnalysisSettings) GetThresholdMode() ThresholdMode { return s.ThresholdMode }
76+
func (s AnalysisSettings) GetThresholdValue() float64 { return s.ThresholdValue }

pkg/degradation-detector/degradationDetector.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ type analysisSettings interface {
2525
GetMedianDifferenceThreshold() float64
2626
GetEffectSizeThreshold() float64
2727
GetDaysToCheckMissing() int
28+
29+
GetAnalysisKind() AnalysisKind
30+
GetThresholdMode() ThresholdMode
31+
GetThresholdValue() float64
2832
}
2933

3034
func (v MedianValues) PercentageChange() float64 {
@@ -34,6 +38,10 @@ func (v MedianValues) PercentageChange() float64 {
3438
func detectDegradations(values []int, builds []string, timestamps []int64, analysisSettings analysisSettings) []Degradation {
3539
degradations := make([]Degradation, 0)
3640

41+
if analysisSettings.GetAnalysisKind() == ThresholdAnalysis {
42+
return detectThresholdExceed(values, builds, timestamps, analysisSettings)
43+
}
44+
3745
minimumSegmentLength := analysisSettings.GetMinimumSegmentLength()
3846
if minimumSegmentLength == 0 {
3947
minimumSegmentLength = 5
@@ -105,6 +113,44 @@ func detectDegradations(values []int, builds []string, timestamps []int64, analy
105113
return degradations
106114
}
107115

116+
// detectThresholdExceed emits a degradation when the latest value crosses the configured threshold
117+
// according to the selected ThresholdMode. For GreaterThan mode, strictly greater (>) is used; for
118+
// LessThan mode, strictly less (<) is used.
119+
func detectThresholdExceed(values []int, builds []string, timestamps []int64, s analysisSettings) []Degradation {
120+
result := make([]Degradation, 0)
121+
if len(values) == 0 || len(builds) == 0 || len(timestamps) == 0 {
122+
return result
123+
}
124+
lastIdx := len(values) - 1
125+
last := float64(values[lastIdx])
126+
threshold := s.GetThresholdValue()
127+
128+
meets := false
129+
switch s.GetThresholdMode() {
130+
case ThresholdGreaterThan:
131+
meets = last > threshold
132+
case ThresholdLessThan:
133+
meets = last < threshold
134+
}
135+
if !meets {
136+
return result
137+
}
138+
// Treat exceeding threshold as degradation event; consumer can filter by ReportType if needed
139+
var previous float64
140+
if lastIdx > 0 {
141+
previous = float64(values[lastIdx-1])
142+
} else {
143+
previous = threshold
144+
}
145+
result = append(result, Degradation{
146+
Build: builds[lastIdx],
147+
timestamp: timestamps[lastIdx],
148+
medianValues: MedianValues{previousValue: previous, newValue: last},
149+
IsDegradation: true,
150+
})
151+
return result
152+
}
153+
108154
func getSegmentsBetweenChangePoints(changePoints []int, values []int) [][]int {
109155
segments := make([][]int, 0, len(changePoints)+1)
110156
prevChangePoint := 0
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package setting
2+
3+
import detector "github.com/JetBrains/ij-perf-report-aggregator/pkg/degradation-detector"
4+
5+
func GenerateAIATestTokenSettings() []detector.PerformanceSettings {
6+
metrics := map[string][]string{
7+
"stage: tokenQuota": {"currentPercents"},
8+
"prod: tokenQuota": {"currentPercents"},
9+
}
10+
11+
settings := make([]detector.PerformanceSettings, 0, 10)
12+
13+
for test, metrics := range metrics {
14+
for _, metric := range metrics {
15+
settings = append(settings, detector.PerformanceSettings{
16+
Db: "perfintDev",
17+
Table: "ml",
18+
Project: test,
19+
BaseSettings: detector.BaseSettings{
20+
Machine: "intellij-linux-%-aws-%",
21+
Metric: metric,
22+
Branch: "master",
23+
SlackSettings: detector.SlackSettings{
24+
Channel: "ai-assistant-autotest-notifications",
25+
ProductLink: "ml/dev",
26+
},
27+
AnalysisSettings: detector.AnalysisSettings{
28+
ReportType: detector.AllEvent,
29+
AnalysisKind: detector.ThresholdAnalysis,
30+
ThresholdMode: detector.ThresholdGreaterThan,
31+
ThresholdValue: 95,
32+
},
33+
},
34+
})
35+
}
36+
}
37+
return settings
38+
}

pkg/degradation-detector/setting/setting_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,14 @@ func TestMavenSetting(t *testing.T) {
2424
assert.Equal(t, detector.DegradationEvent, setting.AnalysisSettings.ReportType)
2525
}
2626
}
27+
28+
func TestGenerateAIATestTokenSettings(t *testing.T) {
29+
t.Parallel()
30+
settings := make([]detector.PerformanceSettings, 0, 1000)
31+
settings = append(settings, GenerateAIATestTokenSettings()...)
32+
for _, setting := range settings {
33+
assert.True(t, setting.AnalysisSettings.AnalysisKind == detector.ThresholdAnalysis)
34+
assert.True(t, setting.AnalysisSettings.ThresholdMode == detector.ThresholdGreaterThan)
35+
assert.True(t, setting.AnalysisSettings.ThresholdValue == 95)
36+
}
37+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package degradation_detector
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestThresholdGreaterThan_TriggersWhenStrictlyGreater(t *testing.T) {
10+
t.Parallel()
11+
values := []int{90, 96}
12+
builds := []string{"b0", "b1"}
13+
times := []int64{0, 123}
14+
15+
settings := AnalysisSettings{
16+
AnalysisKind: ThresholdAnalysis,
17+
ThresholdMode: ThresholdGreaterThan,
18+
ThresholdValue: 95,
19+
}
20+
21+
degradations := detectDegradations(values, builds, times, settings)
22+
assert.Len(t, degradations, 1)
23+
deg := degradations[0]
24+
assert.Equal(t, "b1", deg.Build)
25+
assert.Equal(t, int64(123), deg.timestamp)
26+
assert.True(t, deg.IsDegradation)
27+
assert.Equal(t, 90.0, deg.medianValues.previousValue)
28+
assert.Equal(t, 96.0, deg.medianValues.newValue)
29+
}
30+
31+
func TestThresholdGreaterThan_DoesNotTriggerOnEqual(t *testing.T) {
32+
t.Parallel()
33+
values := []int{95}
34+
builds := []string{"b0"}
35+
times := []int64{0}
36+
37+
settings := AnalysisSettings{
38+
AnalysisKind: ThresholdAnalysis,
39+
ThresholdMode: ThresholdGreaterThan,
40+
ThresholdValue: 95,
41+
}
42+
43+
degradations := detectDegradations(values, builds, times, settings)
44+
assert.Empty(t, degradations)
45+
}
46+
47+
func TestThresholdLessThan_TriggersWhenStrictlyLess(t *testing.T) {
48+
t.Parallel()
49+
values := []int{100, 80}
50+
builds := []string{"b0", "b1"}
51+
times := []int64{0, 77}
52+
53+
settings := AnalysisSettings{
54+
AnalysisKind: ThresholdAnalysis,
55+
ThresholdMode: ThresholdLessThan,
56+
ThresholdValue: 85,
57+
}
58+
59+
degradations := detectDegradations(values, builds, times, settings)
60+
assert.Len(t, degradations, 1)
61+
deg := degradations[0]
62+
assert.Equal(t, "b1", deg.Build)
63+
assert.Equal(t, int64(77), deg.timestamp)
64+
assert.True(t, deg.IsDegradation)
65+
assert.Equal(t, 100.0, deg.medianValues.previousValue)
66+
assert.Equal(t, 80.0, deg.medianValues.newValue)
67+
}
68+
69+
func TestThresholdLessThan_DoesNotTriggerOnEqual(t *testing.T) {
70+
t.Parallel()
71+
values := []int{85}
72+
builds := []string{"b0"}
73+
times := []int64{0}
74+
75+
settings := AnalysisSettings{
76+
AnalysisKind: ThresholdAnalysis,
77+
ThresholdMode: ThresholdLessThan,
78+
ThresholdValue: 85,
79+
}
80+
81+
degradations := detectDegradations(values, builds, times, settings)
82+
assert.Empty(t, degradations)
83+
}
84+
85+
func TestThreshold_NoDataReturnsEmpty(t *testing.T) {
86+
t.Parallel()
87+
var values []int
88+
var builds []string
89+
var times []int64
90+
91+
settings := AnalysisSettings{
92+
AnalysisKind: ThresholdAnalysis,
93+
ThresholdMode: ThresholdGreaterThan,
94+
ThresholdValue: 95,
95+
}
96+
97+
degradations := detectDegradations(values, builds, times, settings)
98+
assert.Empty(t, degradations)
99+
}

0 commit comments

Comments
 (0)