diff --git a/console/frontend/src/views/VisualizePage/DataTable.vue b/console/frontend/src/views/VisualizePage/DataTable.vue index d3d7efb5..63bc8991 100644 --- a/console/frontend/src/views/VisualizePage/DataTable.vue +++ b/console/frontend/src/views/VisualizePage/DataTable.vue @@ -86,6 +86,7 @@ const table = computed(() => { { name: "Min", classNames: "text-right" }, { name: "Max", classNames: "text-right" }, { name: "Average", classNames: "text-right" }, + { name: "~95th", classNames: "text-right" }, ], rows: data.rows?.map((rows, idx) => { @@ -97,7 +98,12 @@ const table = computed(() => { // Dimensions ...rows.map((r) => ({ value: r })), // Stats - ...[data.min[idx], data.max[idx], data.average[idx]].map((d) => ({ + ...[ + data.min[idx], + data.max[idx], + data.average[idx], + data["95th"][idx], + ].map((d) => ({ value: formatBps(d) + "bps", classNames: "text-right tabular-nums", })), diff --git a/console/graph.go b/console/graph.go index f627ad0a..02529629 100644 --- a/console/graph.go +++ b/console/graph.go @@ -81,12 +81,13 @@ ORDER BY time`, strings.Join(with, ",\n "), strings.Join(fields, ",\n "), where) } type graphHandlerOutput struct { - Rows [][]string `json:"rows"` - Time []time.Time `json:"t"` - Points [][]int `json:"points"` // t → row → bps - Average []int `json:"average"` // row → bps - Min []int `json:"min"` - Max []int `json:"max"` + Rows [][]string `json:"rows"` + Time []time.Time `json:"t"` + Points [][]int `json:"points"` // t → row → bps + Average []int `json:"average"` // row → bps + Min []int `json:"min"` + Max []int `json:"max"` + NinetyFivePercentile []int `json:"95th"` } func (c *Component) graphHandlerFunc(gc *gin.Context) { @@ -174,18 +175,38 @@ func (c *Component) graphHandlerFunc(gc *gin.Context) { output.Average = make([]int, len(rows)) output.Min = make([]int, len(rows)) output.Max = make([]int, len(rows)) + output.NinetyFivePercentile = make([]int, len(rows)) for idx, r := range rows { output.Rows[idx] = rowKeys[r] output.Points[idx] = rowValues[r] output.Average[idx] = int(rowSums[r] / uint64(len(output.Time))) - for j, v := range rowValues[r] { - if j == 0 || output.Min[idx] > v { - output.Min[idx] = v - } - if j == 0 || output.Max[idx] < v { - output.Max[idx] = v - } + // For 95th percentile, we need to sort the values. + // Use that for min/max too. + if len(rowValues[r]) == 0 { + continue + } + if len(rowValues[r]) == 1 { + v := rowValues[r][0] + output.Min[idx] = v + output.Max[idx] = v + output.NinetyFivePercentile[idx] = v + continue + } + + s := make([]int, len(rowValues[r])) + copy(s, rowValues[r]) + sort.Ints(s) + output.Min[idx] = s[0] + output.Max[idx] = s[len(s)-1] + index := 0.95 * float64(len(s)) + j := int(index) + if index == float64(j) { + output.NinetyFivePercentile[idx] = s[j-1] + } else if index > 1 { + // We use the average of the two values. This + // is good enough for bps + output.NinetyFivePercentile[idx] = (s[j-1] + s[j]) / 2 } } diff --git a/console/graph_test.go b/console/graph_test.go index 2a471ac6..416afe46 100644 --- a/console/graph_test.go +++ b/console/graph_test.go @@ -183,6 +183,14 @@ func TestGraphHandler(t *testing.T) { 333, 700, }, + "95th": []int{ + 4000, + 750, + 600, + 550, + 500, + 1000, + }, } mockConn.EXPECT(). Select(gomock.Any(), gomock.Any(), gomock.Any()).