Skip to content

Commit e2764f2

Browse files
fix(studio): average p95 calculation on query performance (supabase#39725)
* fix: p95 calculation Something seemed off so had another stab at it using resp_calls * feat: simplified weighted avg p95 * feat: tweak it slightly * feat: try one with a fallback * feat: add test for percentile calculating
1 parent ab2e1e8 commit e2764f2

File tree

2 files changed

+50
-2
lines changed

2 files changed

+50
-2
lines changed

apps/studio/components/interfaces/QueryPerformance/QueryPerformance.utils.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect } from 'vitest'
22
import { formatDuration } from './QueryPerformance.utils'
3+
import { calculatePercentilesFromHistogram } from './WithMonitor/WithMonitor.utils'
34

45
describe('formatDuration', () => {
56
it('should format seconds', () => {
@@ -22,3 +23,23 @@ describe('formatDuration', () => {
2223
expect(formatDuration(90061000)).toBe('1d 1h 1m 1s')
2324
})
2425
})
26+
27+
describe('calculatePercentilesFromHistogram', () => {
28+
it('should return zero for empty histogram', () => {
29+
const result = calculatePercentilesFromHistogram([])
30+
expect(result.p95).toBe(0)
31+
})
32+
33+
it('should return valid p95 for typical distribution', () => {
34+
const result = calculatePercentilesFromHistogram([10, 20, 30, 20, 10, 10])
35+
expect(result.p95).toBeGreaterThan(0)
36+
expect(result.p95).toBeGreaterThanOrEqual(result.p50)
37+
})
38+
39+
it('should return consistent p95 for same input', () => {
40+
const histogram = [10, 20, 30, 20, 10, 10]
41+
const result1 = calculatePercentilesFromHistogram(histogram)
42+
const result2 = calculatePercentilesFromHistogram(histogram)
43+
expect(result1.p95).toBe(result2.p95)
44+
})
45+
})

apps/studio/components/interfaces/QueryPerformance/QueryPerformanceChart.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Loader2 } from 'lucide-react'
55
import { ComposedChart } from 'components/ui/Charts/ComposedChart'
66
import type { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
77
import type { ChartDataPoint } from './WithMonitor/WithMonitor.utils'
8+
import { calculatePercentilesFromHistogram } from './WithMonitor/WithMonitor.utils'
89

910
interface QueryPerformanceChartProps {
1011
dateRange?: {
@@ -61,12 +62,38 @@ export const QueryPerformanceChart = ({
6162

6263
switch (selectedMetric) {
6364
case 'query_latency': {
64-
const avgP95 = chartData.reduce((sum, d) => sum + d.p95_time, 0) / chartData.length
65+
let trueP95: number = 0
66+
67+
if (parsedLogs && parsedLogs.length > 0) {
68+
const bucketCount = parsedLogs[0]?.resp_calls?.length || 50
69+
const combinedHistogram = new Array(bucketCount).fill(0)
70+
71+
parsedLogs.forEach((log) => {
72+
if (log.resp_calls && Array.isArray(log.resp_calls)) {
73+
log.resp_calls.forEach((count: number, index: number) => {
74+
if (index < combinedHistogram.length) {
75+
combinedHistogram[index] += count
76+
}
77+
})
78+
}
79+
})
80+
81+
// [kemal]: this might need a revisit
82+
const percentiles = calculatePercentilesFromHistogram(combinedHistogram)
83+
trueP95 = percentiles.p95
84+
} else {
85+
// [kemal]: fallback to weighted average
86+
const totalCalls = chartData.reduce((sum, d) => sum + d.calls, 0)
87+
trueP95 =
88+
totalCalls > 0
89+
? chartData.reduce((sum, d) => sum + d.p95_time * d.calls, 0) / totalCalls
90+
: 0
91+
}
6592

6693
return [
6794
{
6895
label: 'Average p95',
69-
value: avgP95 >= 100 ? `${(avgP95 / 1000).toFixed(2)}s` : `${Math.round(avgP95)}ms`,
96+
value: `${Math.round(trueP95)}ms`,
7097
},
7198
]
7299
}

0 commit comments

Comments
 (0)