Skip to content

Commit 859fbcd

Browse files
LitemnMaXal
authored andcommitted
[detector] Add threshold-based analysis settings and detection logic
1 parent f7f6e7f commit 859fbcd

File tree

5 files changed

+224
-0
lines changed

5 files changed

+224
-0
lines changed

pkg/degradation-detector/analysisSettings.go

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

11+
// AnalysisKind selects which algorithm to use for detection.
12+
// Default (zero) keeps backward-compatible change-point detection.
13+
type AnalysisKind int
14+
15+
const (
16+
ChangePointAnalysis AnalysisKind = iota
17+
ThresholdAnalysis
18+
)
19+
20+
// ThresholdMode specifies how the latest value should be compared to the threshold.
21+
type ThresholdMode int
22+
23+
const (
24+
ThresholdGreaterThan ThresholdMode = iota
25+
ThresholdLessThan
26+
)
27+
1128
type AnalysisSettings struct {
1229
ReportType ReportType
1330
// 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
@@ -23,6 +40,10 @@ type AnalysisSettings struct {
2340
// Number of days to check for missing data.
2441
// The default value is -3 (3 days ago).
2542
DaysToCheckMissing int
43+
44+
AnalysisKind AnalysisKind
45+
ThresholdMode ThresholdMode
46+
ThresholdValue float64
2647
}
2748

2849
func (s AnalysisSettings) GetReportType() ReportType {
@@ -47,3 +68,12 @@ func (s AnalysisSettings) GetDaysToCheckMissing() int {
4768
}
4869
return s.DaysToCheckMissing
4970
}
71+
72+
// GetAnalysisKind returns the analysis kind setting.
73+
func (s AnalysisSettings) GetAnalysisKind() AnalysisKind { return s.AnalysisKind }
74+
75+
// GetThresholdMode returns the threshold comparison mode.
76+
func (s AnalysisSettings) GetThresholdMode() ThresholdMode { return s.ThresholdMode }
77+
78+
// GetThresholdValue returns the threshold value.
79+
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
@@ -26,6 +26,10 @@ type analysisSettings interface {
2626
GetMedianDifferenceThreshold() float64
2727
GetEffectSizeThreshold() float64
2828
GetDaysToCheckMissing() int
29+
30+
GetAnalysisKind() AnalysisKind
31+
GetThresholdMode() ThresholdMode
32+
GetThresholdValue() float64
2933
}
3034

3135
func (v CenterValues) PercentageChange() float64 {
@@ -35,6 +39,10 @@ func (v CenterValues) PercentageChange() float64 {
3539
func detectDegradations(values []int, builds []string, timestamps []int64, analysisSettings analysisSettings) []Degradation {
3640
degradations := make([]Degradation, 0)
3741

42+
if analysisSettings.GetAnalysisKind() == ThresholdAnalysis {
43+
return detectThresholdExceed(values, builds, timestamps, analysisSettings)
44+
}
45+
3846
minimumSegmentLength := analysisSettings.GetMinimumSegmentLength()
3947
if minimumSegmentLength == 0 {
4048
minimumSegmentLength = 5
@@ -115,6 +123,44 @@ func detectDegradations(values []int, builds []string, timestamps []int64, analy
115123
return degradations
116124
}
117125

126+
// detectThresholdExceed emits a degradation when the latest value crosses the configured threshold
127+
// according to the selected ThresholdMode. For GreaterThan mode, strictly greater (>) is used; for
128+
// LessThan mode, strictly less (<) is used.
129+
func detectThresholdExceed(values []int, builds []string, timestamps []int64, s analysisSettings) []Degradation {
130+
result := make([]Degradation, 0)
131+
if len(values) == 0 || len(builds) == 0 || len(timestamps) == 0 {
132+
return result
133+
}
134+
lastIdx := len(values) - 1
135+
last := float64(values[lastIdx])
136+
threshold := s.GetThresholdValue()
137+
138+
meets := false
139+
switch s.GetThresholdMode() {
140+
case ThresholdGreaterThan:
141+
meets = last > threshold
142+
case ThresholdLessThan:
143+
meets = last < threshold
144+
}
145+
if !meets {
146+
return result
147+
}
148+
// Treat exceeding threshold as degradation event; consumer can filter by ReportType if needed
149+
var previous float64
150+
if lastIdx > 0 {
151+
previous = float64(values[lastIdx-1])
152+
} else {
153+
previous = threshold
154+
}
155+
result = append(result, Degradation{
156+
Build: builds[lastIdx],
157+
timestamp: timestamps[lastIdx],
158+
medianValues: CenterValues{previousValue: previous, newValue: last},
159+
IsDegradation: true,
160+
})
161+
return result
162+
}
163+
118164
func GetSegmentsBetweenChangePoints(changePoints []int, values []int) [][]int {
119165
segments := make([][]int, 0, len(changePoints)+1)
120166
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.Equal(t, detector.ThresholdAnalysis, setting.AnalysisSettings.AnalysisKind)
34+
assert.Equal(t, detector.ThresholdGreaterThan, setting.AnalysisSettings.ThresholdMode)
35+
assert.InEpsilon(t, 95.0, setting.AnalysisSettings.ThresholdValue, 0.0001)
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.InEpsilon(t, 90.0, deg.medianValues.previousValue, 0.0001)
28+
assert.InEpsilon(t, 96.0, deg.medianValues.newValue, 0.0001)
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.InEpsilon(t, 100.0, deg.medianValues.previousValue, 0.0001)
66+
assert.InEpsilon(t, 80.0, deg.medianValues.newValue, 0.0001)
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)