itsi 0.2.16 → 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.zed/settings.json +32 -0
- data/CHANGELOG.md +21 -0
- data/Cargo.lock +4 -2
- data/crates/itsi_acme/Cargo.toml +1 -1
- data/crates/itsi_scheduler/Cargo.toml +1 -1
- data/crates/itsi_server/Cargo.toml +3 -1
- data/crates/itsi_server/src/lib.rs +6 -1
- data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +2 -0
- data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +4 -4
- data/crates/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +14 -13
- data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +64 -33
- data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +151 -152
- data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +422 -110
- data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +62 -15
- data/crates/itsi_server/src/ruby_types/itsi_server.rs +1 -1
- data/crates/itsi_server/src/server/binds/listener.rs +45 -7
- data/crates/itsi_server/src/server/frame_stream.rs +142 -0
- data/crates/itsi_server/src/server/http_message_types.rs +142 -9
- data/crates/itsi_server/src/server/io_stream.rs +28 -5
- data/crates/itsi_server/src/server/lifecycle_event.rs +1 -1
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +2 -3
- data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +8 -10
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +2 -3
- data/crates/itsi_server/src/server/middleware_stack/middlewares/csp.rs +3 -3
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +54 -56
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +5 -7
- data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +5 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +7 -10
- data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +2 -3
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +1 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +4 -6
- data/crates/itsi_server/src/server/mod.rs +1 -0
- data/crates/itsi_server/src/server/process_worker.rs +3 -4
- data/crates/itsi_server/src/server/serve_strategy/acceptor.rs +16 -12
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +83 -31
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +166 -142
- data/crates/itsi_server/src/server/signal.rs +37 -9
- data/crates/itsi_server/src/server/thread_worker.rs +84 -69
- data/crates/itsi_server/src/services/itsi_http_service.rs +43 -43
- data/crates/itsi_server/src/services/static_file_server.rs +28 -47
- data/docs/benchmark-dashboard/.gitignore +27 -0
- data/docs/benchmark-dashboard/app/api/benchmarks/route.ts +22 -0
- data/docs/benchmark-dashboard/app/globals.css +94 -0
- data/docs/benchmark-dashboard/app/layout.tsx +20 -0
- data/docs/benchmark-dashboard/app/page.tsx +252 -0
- data/docs/benchmark-dashboard/components/benchmark-dashboard.tsx +1663 -0
- data/docs/benchmark-dashboard/components/theme-provider.tsx +11 -0
- data/docs/benchmark-dashboard/components/ui/accordion.tsx +58 -0
- data/docs/benchmark-dashboard/components/ui/alert-dialog.tsx +141 -0
- data/docs/benchmark-dashboard/components/ui/alert.tsx +59 -0
- data/docs/benchmark-dashboard/components/ui/aspect-ratio.tsx +7 -0
- data/docs/benchmark-dashboard/components/ui/avatar.tsx +50 -0
- data/docs/benchmark-dashboard/components/ui/badge.tsx +36 -0
- data/docs/benchmark-dashboard/components/ui/breadcrumb.tsx +115 -0
- data/docs/benchmark-dashboard/components/ui/button.tsx +56 -0
- data/docs/benchmark-dashboard/components/ui/calendar.tsx +66 -0
- data/docs/benchmark-dashboard/components/ui/card.tsx +79 -0
- data/docs/benchmark-dashboard/components/ui/carousel.tsx +262 -0
- data/docs/benchmark-dashboard/components/ui/chart.tsx +365 -0
- data/docs/benchmark-dashboard/components/ui/checkbox.tsx +30 -0
- data/docs/benchmark-dashboard/components/ui/collapsible.tsx +11 -0
- data/docs/benchmark-dashboard/components/ui/command.tsx +153 -0
- data/docs/benchmark-dashboard/components/ui/context-menu.tsx +200 -0
- data/docs/benchmark-dashboard/components/ui/dialog.tsx +122 -0
- data/docs/benchmark-dashboard/components/ui/drawer.tsx +118 -0
- data/docs/benchmark-dashboard/components/ui/dropdown-menu.tsx +200 -0
- data/docs/benchmark-dashboard/components/ui/form.tsx +178 -0
- data/docs/benchmark-dashboard/components/ui/hover-card.tsx +29 -0
- data/docs/benchmark-dashboard/components/ui/input-otp.tsx +71 -0
- data/docs/benchmark-dashboard/components/ui/input.tsx +22 -0
- data/docs/benchmark-dashboard/components/ui/label.tsx +26 -0
- data/docs/benchmark-dashboard/components/ui/loading-spinner.tsx +12 -0
- data/docs/benchmark-dashboard/components/ui/menubar.tsx +236 -0
- data/docs/benchmark-dashboard/components/ui/navigation-menu.tsx +128 -0
- data/docs/benchmark-dashboard/components/ui/pagination.tsx +117 -0
- data/docs/benchmark-dashboard/components/ui/popover.tsx +31 -0
- data/docs/benchmark-dashboard/components/ui/progress.tsx +28 -0
- data/docs/benchmark-dashboard/components/ui/radio-group.tsx +44 -0
- data/docs/benchmark-dashboard/components/ui/resizable.tsx +45 -0
- data/docs/benchmark-dashboard/components/ui/scroll-area.tsx +48 -0
- data/docs/benchmark-dashboard/components/ui/select.tsx +160 -0
- data/docs/benchmark-dashboard/components/ui/separator.tsx +31 -0
- data/docs/benchmark-dashboard/components/ui/sheet.tsx +140 -0
- data/docs/benchmark-dashboard/components/ui/sidebar.tsx +763 -0
- data/docs/benchmark-dashboard/components/ui/skeleton.tsx +15 -0
- data/docs/benchmark-dashboard/components/ui/slider.tsx +28 -0
- data/docs/benchmark-dashboard/components/ui/sonner.tsx +31 -0
- data/docs/benchmark-dashboard/components/ui/switch.tsx +29 -0
- data/docs/benchmark-dashboard/components/ui/table.tsx +117 -0
- data/docs/benchmark-dashboard/components/ui/tabs.tsx +55 -0
- data/docs/benchmark-dashboard/components/ui/textarea.tsx +22 -0
- data/docs/benchmark-dashboard/components/ui/toast.tsx +129 -0
- data/docs/benchmark-dashboard/components/ui/toaster.tsx +35 -0
- data/docs/benchmark-dashboard/components/ui/toggle-group.tsx +61 -0
- data/docs/benchmark-dashboard/components/ui/toggle.tsx +45 -0
- data/docs/benchmark-dashboard/components/ui/tooltip.tsx +30 -0
- data/docs/benchmark-dashboard/components/ui/use-mobile.tsx +19 -0
- data/docs/benchmark-dashboard/components/ui/use-toast.ts +194 -0
- data/docs/benchmark-dashboard/components.json +21 -0
- data/docs/benchmark-dashboard/dist/benchmark-dashboard.css +1 -0
- data/docs/benchmark-dashboard/dist/benchmark-dashboard.iife.js +211 -0
- data/docs/benchmark-dashboard/dist/placeholder-logo.png +0 -0
- data/docs/benchmark-dashboard/dist/placeholder-logo.svg +1 -0
- data/docs/benchmark-dashboard/dist/placeholder-user.jpg +0 -0
- data/docs/benchmark-dashboard/dist/placeholder.jpg +0 -0
- data/docs/benchmark-dashboard/dist/placeholder.svg +1 -0
- data/docs/benchmark-dashboard/embed.tsx +13 -0
- data/docs/benchmark-dashboard/hooks/use-mobile.tsx +19 -0
- data/docs/benchmark-dashboard/hooks/use-toast.ts +194 -0
- data/docs/benchmark-dashboard/lib/benchmark-utils.ts +54 -0
- data/docs/benchmark-dashboard/lib/utils.ts +6 -0
- data/docs/benchmark-dashboard/next.config.mjs +14 -0
- data/docs/benchmark-dashboard/package-lock.json +5859 -0
- data/docs/benchmark-dashboard/package.json +72 -0
- data/docs/benchmark-dashboard/pnpm-lock.yaml +5 -0
- data/docs/benchmark-dashboard/postcss.config.mjs +8 -0
- data/docs/benchmark-dashboard/styles/globals.css +94 -0
- data/docs/benchmark-dashboard/tailwind.config.ts +96 -0
- data/docs/benchmark-dashboard/tsconfig.json +27 -0
- data/docs/benchmark-dashboard/vite.config.ts +24 -0
- data/docs/build.rb +52 -0
- data/docs/content/acknowledgements/_index.md +1 -1
- data/docs/content/benchmarks/index.md +96 -0
- data/docs/content/configuration/_index.md +2 -2
- data/docs/content/getting_started/_index.md +76 -46
- data/docs/content/itsi_scheduler/_index.md +2 -2
- data/docs/hugo.yaml +10 -1
- data/docs/static/results.json +1 -0
- data/docs/static/scripts/benchmark-dashboard.iife.js +211 -0
- data/docs/static/styles/benchmark-dashboard.css +1 -0
- data/examples/api_with_schema_and_controllers/README.md +1 -1
- data/gems/scheduler/Cargo.lock +1 -1
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/server/Cargo.lock +3 -1
- data/gems/server/exe/itsi +8 -5
- data/gems/server/lib/itsi/http_request.rb +31 -39
- data/gems/server/lib/itsi/http_response.rb +5 -0
- data/gems/server/lib/itsi/rack_env_pool.rb +59 -0
- data/gems/server/lib/itsi/server/config/dsl.rb +6 -4
- data/gems/server/lib/itsi/server/config/middleware/compression.md +3 -3
- data/gems/server/lib/itsi/server/config/middleware/endpoint/controller.md +1 -1
- data/gems/server/lib/itsi/server/config/middleware/proxy.md +2 -2
- data/gems/server/lib/itsi/server/config/middleware/proxy.rb +1 -1
- data/gems/server/lib/itsi/server/config/middleware/rackup_file.rb +2 -2
- data/gems/server/lib/itsi/server/config/options/auto_reload_config.rb +11 -6
- data/gems/server/lib/itsi/server/config/options/include.md +1 -0
- data/gems/server/lib/itsi/server/config/options/include.rb +13 -8
- data/gems/server/lib/itsi/server/config/options/pipeline_flush.md +16 -0
- data/gems/server/lib/itsi/server/config/options/pipeline_flush.rb +19 -0
- data/gems/server/lib/itsi/server/config/options/reuse_port.rb +2 -4
- data/gems/server/lib/itsi/server/config/options/writev.md +25 -0
- data/gems/server/lib/itsi/server/config/options/writev.rb +19 -0
- data/gems/server/lib/itsi/server/config.rb +22 -9
- data/gems/server/lib/itsi/server/default_config/Itsi.rb +9 -8
- data/gems/server/lib/itsi/server/grpc/grpc_call.rb +2 -0
- data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +2 -2
- data/gems/server/lib/itsi/server/rack/handler/itsi.rb +3 -1
- data/gems/server/lib/itsi/server/rack_interface.rb +17 -12
- data/gems/server/lib/itsi/server/scheduler_interface.rb +2 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +1 -0
- data/gems/server/lib/ruby_lsp/itsi/addon.rb +12 -13
- data/gems/server/test/helpers/test_helper.rb +12 -13
- data/gems/server/test/middleware/grpc/grpc.rb +13 -14
- data/gems/server/test/middleware/grpc/test_service_impl.rb +3 -3
- data/gems/server/test/middleware/proxy.rb +262 -268
- data/lib/itsi/version.rb +1 -1
- metadata +97 -6
- data/tasks.txt +0 -28
@@ -0,0 +1,1663 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import type React from "react";
|
4
|
+
|
5
|
+
import { useState, useMemo, useCallback, useEffect } from "react";
|
6
|
+
import {
|
7
|
+
Bar,
|
8
|
+
XAxis,
|
9
|
+
YAxis,
|
10
|
+
CartesianGrid,
|
11
|
+
ResponsiveContainer,
|
12
|
+
BarChart,
|
13
|
+
} from "recharts";
|
14
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
15
|
+
import {
|
16
|
+
Select,
|
17
|
+
SelectContent,
|
18
|
+
SelectItem,
|
19
|
+
SelectTrigger,
|
20
|
+
SelectValue,
|
21
|
+
SelectGroup,
|
22
|
+
SelectLabel,
|
23
|
+
} from "@/components/ui/select";
|
24
|
+
import { Label } from "@/components/ui/label";
|
25
|
+
import { Badge } from "@/components/ui/badge";
|
26
|
+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
27
|
+
import {
|
28
|
+
InfoIcon,
|
29
|
+
TrendingUp,
|
30
|
+
TrendingDown,
|
31
|
+
TagIcon,
|
32
|
+
TrophyIcon,
|
33
|
+
} from "lucide-react";
|
34
|
+
|
35
|
+
// Updated types to match the new hierarchical structure with groups
|
36
|
+
type BenchmarkResult = {
|
37
|
+
server: string;
|
38
|
+
version?: string;
|
39
|
+
test_case: string;
|
40
|
+
threads: number;
|
41
|
+
workers: number;
|
42
|
+
http2: boolean;
|
43
|
+
concurrency: number;
|
44
|
+
rss_mb?: number;
|
45
|
+
results: {
|
46
|
+
successRate: number;
|
47
|
+
total: number;
|
48
|
+
slowest: number;
|
49
|
+
fastest: number;
|
50
|
+
average: number;
|
51
|
+
requestsPerSec: number;
|
52
|
+
totalData: number;
|
53
|
+
sizePerRequest: number;
|
54
|
+
sizePerSec: number;
|
55
|
+
errorDistribution: Record<string, number>;
|
56
|
+
p95_latency: number;
|
57
|
+
};
|
58
|
+
timestamp?: string;
|
59
|
+
};
|
60
|
+
|
61
|
+
type ServerData = {
|
62
|
+
server: string;
|
63
|
+
results: BenchmarkResult[];
|
64
|
+
};
|
65
|
+
|
66
|
+
type TestData = {
|
67
|
+
test: string;
|
68
|
+
servers: ServerData[];
|
69
|
+
};
|
70
|
+
|
71
|
+
type GroupData = {
|
72
|
+
group: string;
|
73
|
+
tests: TestData[];
|
74
|
+
};
|
75
|
+
|
76
|
+
type CpuData = {
|
77
|
+
cpu: string;
|
78
|
+
groups: GroupData[];
|
79
|
+
};
|
80
|
+
|
81
|
+
type HierarchicalBenchmarkData = CpuData[];
|
82
|
+
|
83
|
+
type FilterOptions = {
|
84
|
+
cpus: string[];
|
85
|
+
testCases: string[];
|
86
|
+
servers: string[];
|
87
|
+
threads: number[];
|
88
|
+
workers: number[];
|
89
|
+
concurrencyLevels: number[];
|
90
|
+
http2Options: (boolean | "all")[];
|
91
|
+
};
|
92
|
+
|
93
|
+
type FilterState = {
|
94
|
+
cpu: string;
|
95
|
+
testCase: string;
|
96
|
+
threads: number;
|
97
|
+
workers: number;
|
98
|
+
concurrency: number;
|
99
|
+
http2: boolean | "all";
|
100
|
+
xAxis: string;
|
101
|
+
metric: string;
|
102
|
+
visibleServers: string[];
|
103
|
+
};
|
104
|
+
|
105
|
+
type BenchmarkDashboardProps = {
|
106
|
+
data: HierarchicalBenchmarkData;
|
107
|
+
};
|
108
|
+
|
109
|
+
export function BenchmarkDashboard({ data }: BenchmarkDashboardProps) {
|
110
|
+
// Helper function to format server names (replace __ with +)
|
111
|
+
const formatServerName = useCallback((serverName: string): string => {
|
112
|
+
return serverName.replace(/__/g, "+");
|
113
|
+
}, []);
|
114
|
+
|
115
|
+
// Flatten the hierarchical data into the format we need for processing
|
116
|
+
const flattenedData = useMemo(() => {
|
117
|
+
const flattened: BenchmarkResult[] = [];
|
118
|
+
|
119
|
+
if (!data || !Array.isArray(data)) {
|
120
|
+
return flattened;
|
121
|
+
}
|
122
|
+
|
123
|
+
data.forEach((cpuData) => {
|
124
|
+
if (!cpuData?.groups || !Array.isArray(cpuData.groups)) {
|
125
|
+
return;
|
126
|
+
}
|
127
|
+
|
128
|
+
cpuData.groups.forEach((groupData) => {
|
129
|
+
if (!groupData?.tests || !Array.isArray(groupData.tests)) {
|
130
|
+
return;
|
131
|
+
}
|
132
|
+
|
133
|
+
groupData.tests.forEach((testData) => {
|
134
|
+
if (!testData?.servers || !Array.isArray(testData.servers)) {
|
135
|
+
return;
|
136
|
+
}
|
137
|
+
|
138
|
+
testData.servers.forEach((serverData) => {
|
139
|
+
if (!serverData?.results || !Array.isArray(serverData.results)) {
|
140
|
+
return;
|
141
|
+
}
|
142
|
+
|
143
|
+
serverData.results.forEach((result) => {
|
144
|
+
// Add CPU and group information to each result
|
145
|
+
flattened.push({
|
146
|
+
...result,
|
147
|
+
cpu: cpuData.cpu,
|
148
|
+
group: groupData.group,
|
149
|
+
} as BenchmarkResult & { cpu: string; group: string });
|
150
|
+
});
|
151
|
+
});
|
152
|
+
});
|
153
|
+
});
|
154
|
+
});
|
155
|
+
|
156
|
+
return flattened;
|
157
|
+
}, [data]);
|
158
|
+
|
159
|
+
// Extract all possible filter options from hierarchical data
|
160
|
+
const allFilterOptions: FilterOptions = useMemo(() => {
|
161
|
+
const defaultOptions: FilterOptions = {
|
162
|
+
cpus: [],
|
163
|
+
testCases: [],
|
164
|
+
servers: [],
|
165
|
+
threads: [],
|
166
|
+
workers: [],
|
167
|
+
concurrencyLevels: [],
|
168
|
+
http2Options: ["all"],
|
169
|
+
};
|
170
|
+
|
171
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
172
|
+
return defaultOptions;
|
173
|
+
}
|
174
|
+
|
175
|
+
return {
|
176
|
+
cpus: data.map((cpuData) => cpuData.cpu).filter(Boolean),
|
177
|
+
testCases: [
|
178
|
+
...new Set(flattenedData.map((item) => item.test_case).filter(Boolean)),
|
179
|
+
],
|
180
|
+
servers: [
|
181
|
+
...new Set(flattenedData.map((item) => item.server).filter(Boolean)),
|
182
|
+
],
|
183
|
+
threads: [
|
184
|
+
...new Set(
|
185
|
+
flattenedData
|
186
|
+
.map((item) => item.threads)
|
187
|
+
.filter((t) => typeof t === "number"),
|
188
|
+
),
|
189
|
+
].sort((a, b) => a - b),
|
190
|
+
workers: [
|
191
|
+
...new Set(
|
192
|
+
flattenedData
|
193
|
+
.map((item) => item.workers)
|
194
|
+
.filter((w) => typeof w === "number"),
|
195
|
+
),
|
196
|
+
].sort((a, b) => a - b),
|
197
|
+
concurrencyLevels: [
|
198
|
+
...new Set(
|
199
|
+
flattenedData
|
200
|
+
.map((item) => item.concurrency)
|
201
|
+
.filter((c) => typeof c === "number"),
|
202
|
+
),
|
203
|
+
].sort((a, b) => a - b),
|
204
|
+
http2Options: [
|
205
|
+
"all",
|
206
|
+
...new Set(
|
207
|
+
flattenedData
|
208
|
+
.map((item) => item.http2)
|
209
|
+
.filter((h) => typeof h === "boolean"),
|
210
|
+
),
|
211
|
+
],
|
212
|
+
};
|
213
|
+
}, [data, flattenedData]);
|
214
|
+
|
215
|
+
// Generate consistent colors for all servers upfront
|
216
|
+
const serverColors = useMemo(() => {
|
217
|
+
const itsiColor = "#ff7f0e";
|
218
|
+
|
219
|
+
const palette = [
|
220
|
+
"#1f77b4", // blue
|
221
|
+
"#2ca02c", // green
|
222
|
+
"#d62728", // red (not orange)
|
223
|
+
"#9467bd", // purple
|
224
|
+
"#8c564b", // brown
|
225
|
+
"#e377c2", // pink
|
226
|
+
"#7f7f7f", // gray
|
227
|
+
"#bcbd22", // lime
|
228
|
+
"#17becf", // cyan
|
229
|
+
"#393b79", // indigo
|
230
|
+
"#a55194", // magenta
|
231
|
+
];
|
232
|
+
|
233
|
+
const colorMap: Record<string, string> = {};
|
234
|
+
const servers = [...allFilterOptions.servers];
|
235
|
+
|
236
|
+
const itsiIndex = servers.findIndex((s) => s === "itsi");
|
237
|
+
if (itsiIndex !== -1) {
|
238
|
+
colorMap["itsi"] = itsiColor;
|
239
|
+
servers.splice(itsiIndex, 1);
|
240
|
+
}
|
241
|
+
|
242
|
+
servers.forEach((server, index) => {
|
243
|
+
colorMap[server] = palette[index % palette.length];
|
244
|
+
});
|
245
|
+
|
246
|
+
return colorMap;
|
247
|
+
}, [allFilterOptions.servers]);
|
248
|
+
|
249
|
+
// Helper function to parse URL search parameters
|
250
|
+
const parseUrlParams = useCallback((): Partial<FilterState> | null => {
|
251
|
+
if (typeof window === "undefined") return null;
|
252
|
+
|
253
|
+
try {
|
254
|
+
const urlParams = new URLSearchParams(window.location.search);
|
255
|
+
const filters: Partial<FilterState> = {};
|
256
|
+
|
257
|
+
// Parse each parameter
|
258
|
+
const cpu = urlParams.get("cpu");
|
259
|
+
const testCase = urlParams.get("testCase");
|
260
|
+
const threads = urlParams.get("threads");
|
261
|
+
const workers = urlParams.get("workers");
|
262
|
+
const concurrency = urlParams.get("concurrency");
|
263
|
+
const http2 = urlParams.get("http2");
|
264
|
+
const xAxis = urlParams.get("xAxis");
|
265
|
+
const metric = urlParams.get("metric");
|
266
|
+
|
267
|
+
if (cpu) filters.cpu = cpu;
|
268
|
+
if (testCase) filters.testCase = testCase;
|
269
|
+
if (threads) filters.threads = Number.parseInt(threads);
|
270
|
+
if (workers) filters.workers = Number.parseInt(workers);
|
271
|
+
if (concurrency) filters.concurrency = Number.parseInt(concurrency);
|
272
|
+
if (http2) {
|
273
|
+
if (http2 === "all") {
|
274
|
+
filters.http2 = "all";
|
275
|
+
} else {
|
276
|
+
filters.http2 = http2 === "true";
|
277
|
+
}
|
278
|
+
}
|
279
|
+
if (xAxis) filters.xAxis = xAxis;
|
280
|
+
if (metric) filters.metric = metric;
|
281
|
+
|
282
|
+
const visibleServersParam = urlParams.get("visibleServers");
|
283
|
+
if (visibleServersParam) {
|
284
|
+
try {
|
285
|
+
filters.visibleServers = visibleServersParam
|
286
|
+
.split(",")
|
287
|
+
.filter(Boolean);
|
288
|
+
} catch (e) {
|
289
|
+
// Ignore parsing errors
|
290
|
+
}
|
291
|
+
}
|
292
|
+
|
293
|
+
return Object.keys(filters).length > 0 ? filters : null;
|
294
|
+
} catch (error) {
|
295
|
+
console.warn("Failed to parse URL parameters:", error);
|
296
|
+
}
|
297
|
+
|
298
|
+
return null;
|
299
|
+
}, []);
|
300
|
+
|
301
|
+
// Helper function to update URL search parameters
|
302
|
+
const updateUrlParams = useCallback((filters: FilterState) => {
|
303
|
+
if (typeof window === "undefined") return;
|
304
|
+
|
305
|
+
try {
|
306
|
+
const url = new URL(window.location.href);
|
307
|
+
|
308
|
+
// Set each parameter
|
309
|
+
url.searchParams.set("cpu", filters.cpu);
|
310
|
+
url.searchParams.set("testCase", filters.testCase);
|
311
|
+
url.searchParams.set("threads", filters.threads.toString());
|
312
|
+
url.searchParams.set("workers", filters.workers.toString());
|
313
|
+
url.searchParams.set("concurrency", filters.concurrency.toString());
|
314
|
+
url.searchParams.set("http2", filters.http2.toString());
|
315
|
+
url.searchParams.set("xAxis", filters.xAxis);
|
316
|
+
url.searchParams.set("metric", filters.metric);
|
317
|
+
|
318
|
+
// Set visible servers as comma-separated list
|
319
|
+
if (filters.visibleServers && filters.visibleServers.length > 0) {
|
320
|
+
url.searchParams.set(
|
321
|
+
"visibleServers",
|
322
|
+
filters.visibleServers.join(","),
|
323
|
+
);
|
324
|
+
} else {
|
325
|
+
url.searchParams.delete("visibleServers");
|
326
|
+
}
|
327
|
+
|
328
|
+
// Update the URL without triggering a page reload
|
329
|
+
window.history.replaceState(null, "", url.toString());
|
330
|
+
} catch (error) {
|
331
|
+
console.warn("Failed to update URL parameters:", error);
|
332
|
+
}
|
333
|
+
}, []);
|
334
|
+
|
335
|
+
// Helper function to validate and sanitize filter state from URL
|
336
|
+
const validateAndSanitizeFilters = useCallback(
|
337
|
+
(urlFilters: Partial<FilterState>): FilterState => {
|
338
|
+
const defaultFilters: FilterState = {
|
339
|
+
cpu: allFilterOptions.cpus[0] || "",
|
340
|
+
testCase: allFilterOptions.testCases[0] || "",
|
341
|
+
threads: allFilterOptions.threads[0] || 1,
|
342
|
+
workers: allFilterOptions.workers[0] || 1,
|
343
|
+
concurrency: allFilterOptions.concurrencyLevels[0] || 10,
|
344
|
+
http2: "all", // Default to "all"
|
345
|
+
xAxis: "concurrency",
|
346
|
+
metric: "rps",
|
347
|
+
visibleServers: allFilterOptions.servers, // Default to all servers visible
|
348
|
+
};
|
349
|
+
|
350
|
+
// Validate each field and fall back to defaults if invalid
|
351
|
+
const validatedFilters: FilterState = {
|
352
|
+
cpu: allFilterOptions.cpus.includes(urlFilters.cpu || "")
|
353
|
+
? urlFilters.cpu!
|
354
|
+
: defaultFilters.cpu,
|
355
|
+
testCase: allFilterOptions.testCases.includes(urlFilters.testCase || "")
|
356
|
+
? urlFilters.testCase!
|
357
|
+
: defaultFilters.testCase,
|
358
|
+
threads: allFilterOptions.threads.includes(urlFilters.threads || 0)
|
359
|
+
? urlFilters.threads!
|
360
|
+
: defaultFilters.threads,
|
361
|
+
workers: allFilterOptions.workers.includes(urlFilters.workers || 0)
|
362
|
+
? urlFilters.workers!
|
363
|
+
: defaultFilters.workers,
|
364
|
+
concurrency: allFilterOptions.concurrencyLevels.includes(
|
365
|
+
urlFilters.concurrency || 0,
|
366
|
+
)
|
367
|
+
? urlFilters.concurrency!
|
368
|
+
: defaultFilters.concurrency,
|
369
|
+
http2: allFilterOptions.http2Options.includes(urlFilters.http2 as any)
|
370
|
+
? (urlFilters.http2 as boolean | "all")
|
371
|
+
: defaultFilters.http2,
|
372
|
+
xAxis: ["concurrency", "threads", "workers"].includes(
|
373
|
+
urlFilters.xAxis || "",
|
374
|
+
)
|
375
|
+
? urlFilters.xAxis!
|
376
|
+
: defaultFilters.xAxis,
|
377
|
+
metric: ["rps", "p95_latency", "errorRate"].includes(
|
378
|
+
urlFilters.metric || "",
|
379
|
+
)
|
380
|
+
? urlFilters.metric!
|
381
|
+
: defaultFilters.metric,
|
382
|
+
visibleServers: Array.isArray(urlFilters.visibleServers)
|
383
|
+
? urlFilters.visibleServers.filter((server) =>
|
384
|
+
allFilterOptions.servers.includes(server),
|
385
|
+
)
|
386
|
+
: defaultFilters.visibleServers,
|
387
|
+
};
|
388
|
+
|
389
|
+
return validatedFilters;
|
390
|
+
},
|
391
|
+
[allFilterOptions],
|
392
|
+
);
|
393
|
+
|
394
|
+
// Initialize filter state with URL parameters or defaults
|
395
|
+
const [filters, setFilters] = useState<FilterState>(() => {
|
396
|
+
const urlFilters = parseUrlParams();
|
397
|
+
if (urlFilters && allFilterOptions.cpus.length > 0) {
|
398
|
+
return validateAndSanitizeFilters(urlFilters);
|
399
|
+
}
|
400
|
+
|
401
|
+
const preferredTestCase = allFilterOptions.testCases.includes("hello_world")
|
402
|
+
? "hello_world"
|
403
|
+
: allFilterOptions.testCases[0] || "";
|
404
|
+
|
405
|
+
return {
|
406
|
+
cpu: allFilterOptions.cpus[0] || "",
|
407
|
+
testCase: preferredTestCase || "",
|
408
|
+
threads: allFilterOptions.threads[0] || 1,
|
409
|
+
workers: allFilterOptions.workers[0] || 1,
|
410
|
+
concurrency: allFilterOptions.concurrencyLevels[0] || 10,
|
411
|
+
http2: "all", // Default to "all"
|
412
|
+
xAxis: "concurrency",
|
413
|
+
metric: "rps",
|
414
|
+
visibleServers: allFilterOptions.servers,
|
415
|
+
};
|
416
|
+
});
|
417
|
+
|
418
|
+
// Track which servers are visible
|
419
|
+
const [visibleServers, setVisibleServers] = useState<Record<string, boolean>>(
|
420
|
+
() => {
|
421
|
+
const initialVisibleServers: Record<string, boolean> = {};
|
422
|
+
allFilterOptions.servers.forEach((server) => {
|
423
|
+
initialVisibleServers[server] = true;
|
424
|
+
});
|
425
|
+
|
426
|
+
const urlFilters = parseUrlParams();
|
427
|
+
if (
|
428
|
+
urlFilters?.visibleServers &&
|
429
|
+
Array.isArray(urlFilters.visibleServers)
|
430
|
+
) {
|
431
|
+
allFilterOptions.servers.forEach((server) => {
|
432
|
+
initialVisibleServers[server] =
|
433
|
+
urlFilters.visibleServers!.includes(server);
|
434
|
+
});
|
435
|
+
}
|
436
|
+
|
437
|
+
return initialVisibleServers;
|
438
|
+
},
|
439
|
+
);
|
440
|
+
|
441
|
+
// Currently hovered data point
|
442
|
+
const [hoveredPoint, setHoveredPoint] = useState<BenchmarkResult | null>(
|
443
|
+
null,
|
444
|
+
);
|
445
|
+
const [activeDataKey, setActiveDataKey] = useState<string | null>(null);
|
446
|
+
|
447
|
+
// Track legend interactions to disable animations
|
448
|
+
// const [isLegendInteracting, setIsLegendInteracting] = useState(false)
|
449
|
+
|
450
|
+
// Update URL parameters when filters change
|
451
|
+
useEffect(() => {
|
452
|
+
updateUrlParams(filters);
|
453
|
+
}, [filters, updateUrlParams]);
|
454
|
+
|
455
|
+
// Handle initial load from URL parameters after data is available
|
456
|
+
useEffect(() => {
|
457
|
+
if (allFilterOptions.cpus.length > 0) {
|
458
|
+
const urlFilters = parseUrlParams();
|
459
|
+
if (urlFilters) {
|
460
|
+
const validatedFilters = validateAndSanitizeFilters(urlFilters);
|
461
|
+
setFilters(validatedFilters);
|
462
|
+
|
463
|
+
// Update visibleServers based on URL params after filters are set
|
464
|
+
const initialVisibleServers: Record<string, boolean> = {};
|
465
|
+
allFilterOptions.servers.forEach((server) => {
|
466
|
+
initialVisibleServers[server] = true;
|
467
|
+
});
|
468
|
+
|
469
|
+
if (
|
470
|
+
urlFilters?.visibleServers &&
|
471
|
+
Array.isArray(urlFilters.visibleServers)
|
472
|
+
) {
|
473
|
+
allFilterOptions.servers.forEach((server) => {
|
474
|
+
initialVisibleServers[server] =
|
475
|
+
urlFilters.visibleServers!.includes(server);
|
476
|
+
});
|
477
|
+
}
|
478
|
+
setVisibleServers(initialVisibleServers);
|
479
|
+
}
|
480
|
+
}
|
481
|
+
}, [allFilterOptions, parseUrlParams, validateAndSanitizeFilters]);
|
482
|
+
|
483
|
+
// Get dynamic filter options based on selected CPU and test case
|
484
|
+
const filterOptions = useMemo(() => {
|
485
|
+
// First filter by CPU
|
486
|
+
const cpuFilteredData = flattenedData.filter(
|
487
|
+
(item) => (item as any).cpu === filters.cpu,
|
488
|
+
);
|
489
|
+
|
490
|
+
// Get available test cases for the selected CPU
|
491
|
+
const availableTestCases = [
|
492
|
+
...new Set(cpuFilteredData.map((item) => item.test_case).filter(Boolean)),
|
493
|
+
];
|
494
|
+
|
495
|
+
// Then filter by test case to get the remaining filter options
|
496
|
+
const testCaseFilteredData = cpuFilteredData.filter(
|
497
|
+
(item) => item.test_case === filters.testCase,
|
498
|
+
);
|
499
|
+
|
500
|
+
return {
|
501
|
+
cpus: allFilterOptions.cpus,
|
502
|
+
testCases: availableTestCases,
|
503
|
+
servers: [
|
504
|
+
...new Set(
|
505
|
+
testCaseFilteredData.map((item) => item.server).filter(Boolean),
|
506
|
+
),
|
507
|
+
],
|
508
|
+
threads: [
|
509
|
+
...new Set(
|
510
|
+
testCaseFilteredData
|
511
|
+
.map((item) => item.threads)
|
512
|
+
.filter((t) => typeof t === "number"),
|
513
|
+
),
|
514
|
+
].sort((a, b) => a - b),
|
515
|
+
workers: [
|
516
|
+
...new Set(
|
517
|
+
testCaseFilteredData
|
518
|
+
.map((item) => item.workers)
|
519
|
+
.filter((w) => typeof w === "number"),
|
520
|
+
),
|
521
|
+
].sort((a, b) => a - b),
|
522
|
+
concurrencyLevels: [
|
523
|
+
...new Set(
|
524
|
+
testCaseFilteredData
|
525
|
+
.map((item) => item.concurrency)
|
526
|
+
.filter((c) => typeof c === "number"),
|
527
|
+
),
|
528
|
+
].sort((a, b) => a - b),
|
529
|
+
http2Options: [
|
530
|
+
"all",
|
531
|
+
...new Set(
|
532
|
+
testCaseFilteredData
|
533
|
+
.map((item) => item.http2)
|
534
|
+
.filter((h) => typeof h === "boolean"),
|
535
|
+
),
|
536
|
+
],
|
537
|
+
};
|
538
|
+
}, [flattenedData, filters.cpu, filters.testCase, allFilterOptions.cpus]);
|
539
|
+
|
540
|
+
// Get grouped test cases for the dropdown
|
541
|
+
const groupedTestCases = useMemo(() => {
|
542
|
+
// First filter by CPU to get available data
|
543
|
+
const cpuFilteredData = flattenedData.filter(
|
544
|
+
(item) => (item as any).cpu === filters.cpu,
|
545
|
+
);
|
546
|
+
|
547
|
+
// Group test cases by their group
|
548
|
+
const groupedTests: Record<string, string[]> = {};
|
549
|
+
cpuFilteredData.forEach((item) => {
|
550
|
+
const group = (item as any).group;
|
551
|
+
if (!group || !item.test_case) return;
|
552
|
+
|
553
|
+
if (!groupedTests[group]) {
|
554
|
+
groupedTests[group] = [];
|
555
|
+
}
|
556
|
+
if (!groupedTests[group].includes(item.test_case)) {
|
557
|
+
groupedTests[group].push(item.test_case);
|
558
|
+
}
|
559
|
+
});
|
560
|
+
|
561
|
+
// Sort test cases within each group
|
562
|
+
Object.keys(groupedTests).forEach((group) => {
|
563
|
+
groupedTests[group].sort();
|
564
|
+
});
|
565
|
+
|
566
|
+
// Sort groups: "rack" first, then alphabetically
|
567
|
+
const sortedGroups = Object.keys(groupedTests).sort((a, b) => {
|
568
|
+
if (a === "rack") return -1;
|
569
|
+
if (b === "rack") return 1;
|
570
|
+
return a.localeCompare(b);
|
571
|
+
});
|
572
|
+
|
573
|
+
return { groupedTests, sortedGroups };
|
574
|
+
}, [flattenedData, filters.cpu]);
|
575
|
+
|
576
|
+
// Update filters when CPU or test case changes to ensure valid selections
|
577
|
+
useEffect(() => {
|
578
|
+
setFilters((prev) => {
|
579
|
+
const newFilters = { ...prev };
|
580
|
+
|
581
|
+
// If test case is not available for selected CPU, select first available
|
582
|
+
if (!filterOptions.testCases.includes(prev.testCase)) {
|
583
|
+
newFilters.testCase = filterOptions.testCases[0] || "";
|
584
|
+
}
|
585
|
+
|
586
|
+
// Update other filters with first available value if current value is not valid
|
587
|
+
// BUT skip the filter that matches the current X-axis
|
588
|
+
if (
|
589
|
+
prev.xAxis !== "threads" &&
|
590
|
+
!filterOptions.threads.includes(prev.threads)
|
591
|
+
) {
|
592
|
+
newFilters.threads = filterOptions.threads[0] || 1;
|
593
|
+
}
|
594
|
+
|
595
|
+
if (
|
596
|
+
prev.xAxis !== "workers" &&
|
597
|
+
!filterOptions.workers.includes(prev.workers)
|
598
|
+
) {
|
599
|
+
newFilters.workers = filterOptions.workers[0] || 1;
|
600
|
+
}
|
601
|
+
|
602
|
+
if (
|
603
|
+
prev.xAxis !== "concurrency" &&
|
604
|
+
!filterOptions.concurrencyLevels.includes(prev.concurrency)
|
605
|
+
) {
|
606
|
+
newFilters.concurrency = filterOptions.concurrencyLevels[0] || 10;
|
607
|
+
}
|
608
|
+
|
609
|
+
if (!filterOptions.http2Options.includes(prev.http2)) {
|
610
|
+
newFilters.http2 = filterOptions.http2Options[0] || "all";
|
611
|
+
}
|
612
|
+
|
613
|
+
return newFilters;
|
614
|
+
});
|
615
|
+
}, [filters.cpu, filterOptions]);
|
616
|
+
|
617
|
+
// Apply filters to flattened data
|
618
|
+
const filteredData = useMemo(() => {
|
619
|
+
return flattenedData.filter((item) => {
|
620
|
+
const itemWithCpu = item as any;
|
621
|
+
if (itemWithCpu.cpu !== filters.cpu) return false;
|
622
|
+
if (item.test_case !== filters.testCase) return false;
|
623
|
+
|
624
|
+
// Don't filter by the parameter that's being used as X-axis
|
625
|
+
if (filters.xAxis !== "threads" && item.threads !== filters.threads)
|
626
|
+
return false;
|
627
|
+
if (filters.xAxis !== "workers" && item.workers !== filters.workers)
|
628
|
+
return false;
|
629
|
+
if (
|
630
|
+
filters.xAxis !== "concurrency" &&
|
631
|
+
item.concurrency !== filters.concurrency
|
632
|
+
)
|
633
|
+
return false;
|
634
|
+
|
635
|
+
// Handle "all" protocol option
|
636
|
+
if (filters.http2 !== "all" && item.http2 !== filters.http2) return false;
|
637
|
+
|
638
|
+
return true;
|
639
|
+
});
|
640
|
+
}, [flattenedData, filters]);
|
641
|
+
|
642
|
+
// Prepare data for chart based on selected x-axis
|
643
|
+
const chartData = useMemo(() => {
|
644
|
+
// Group data by the selected x-axis
|
645
|
+
const groupedByXAxis: Record<string, BenchmarkResult[]> = {};
|
646
|
+
|
647
|
+
filteredData.forEach((item) => {
|
648
|
+
const xAxisValue = String(item[filters.xAxis as keyof BenchmarkResult]);
|
649
|
+
if (!groupedByXAxis[xAxisValue]) {
|
650
|
+
groupedByXAxis[xAxisValue] = [];
|
651
|
+
}
|
652
|
+
groupedByXAxis[xAxisValue].push(item);
|
653
|
+
});
|
654
|
+
|
655
|
+
// Convert to format suitable for chart
|
656
|
+
return Object.entries(groupedByXAxis)
|
657
|
+
.map(([xAxisValue, items]) => {
|
658
|
+
const point: Record<string, any> = { [filters.xAxis]: xAxisValue };
|
659
|
+
|
660
|
+
// Group items by server and protocol when "all" is selected
|
661
|
+
items.forEach((item) => {
|
662
|
+
if (visibleServers[item.server]) {
|
663
|
+
// Single metric based on selection
|
664
|
+
let metricValue: number;
|
665
|
+
switch (filters.metric) {
|
666
|
+
case "rps":
|
667
|
+
metricValue = item.results.requestsPerSec;
|
668
|
+
break;
|
669
|
+
case "p95_latency":
|
670
|
+
metricValue = item.results.p95_latency;
|
671
|
+
break;
|
672
|
+
case "errorRate":
|
673
|
+
metricValue = 1 - item.results.successRate;
|
674
|
+
break;
|
675
|
+
default:
|
676
|
+
metricValue = item.results.requestsPerSec;
|
677
|
+
}
|
678
|
+
|
679
|
+
// Create unique key for server+protocol combination when showing all protocols
|
680
|
+
let dataKey: string;
|
681
|
+
if (filters.http2 === "all") {
|
682
|
+
const protocolSuffix = item.http2 ? " (HTTP/2)" : " (HTTP/1.1)";
|
683
|
+
dataKey = `${formatServerName(item.server)}${protocolSuffix}`;
|
684
|
+
} else {
|
685
|
+
dataKey = formatServerName(item.server);
|
686
|
+
}
|
687
|
+
|
688
|
+
point[dataKey] = metricValue;
|
689
|
+
// Store the full item for hover details
|
690
|
+
point[`${dataKey}_data`] = item;
|
691
|
+
}
|
692
|
+
});
|
693
|
+
|
694
|
+
return point;
|
695
|
+
})
|
696
|
+
.sort((a, b) => {
|
697
|
+
// Sort numerically if the x-axis is a number
|
698
|
+
const aVal = a[filters.xAxis];
|
699
|
+
const bVal = b[filters.xAxis];
|
700
|
+
if (!isNaN(Number(aVal)) && !isNaN(Number(bVal))) {
|
701
|
+
return Number(aVal) - Number(bVal);
|
702
|
+
}
|
703
|
+
// Otherwise sort alphabetically
|
704
|
+
return String(aVal).localeCompare(String(bVal));
|
705
|
+
});
|
706
|
+
}, [filteredData, filters, visibleServers, formatServerName]);
|
707
|
+
|
708
|
+
// Get metric info for display
|
709
|
+
const metricInfo = useMemo(() => {
|
710
|
+
// Helper function to format large numbers compactly
|
711
|
+
const formatCompact = (value: number, decimals = 1): string => {
|
712
|
+
if (value >= 1000000) {
|
713
|
+
return `${(value / 1000000).toFixed(decimals)}M`;
|
714
|
+
} else if (value >= 1000) {
|
715
|
+
return `${(value / 1000).toFixed(decimals)}K`;
|
716
|
+
}
|
717
|
+
return value.toFixed(decimals);
|
718
|
+
};
|
719
|
+
|
720
|
+
switch (filters.metric) {
|
721
|
+
case "rps":
|
722
|
+
return {
|
723
|
+
label: "Requests per Second",
|
724
|
+
isBetter: "higher",
|
725
|
+
icon: TrendingUp,
|
726
|
+
formatter: (value: number) => formatCompact(value, 1),
|
727
|
+
};
|
728
|
+
case "p95_latency":
|
729
|
+
return {
|
730
|
+
label: "P95 Latency (ms)",
|
731
|
+
isBetter: "lower",
|
732
|
+
icon: TrendingDown,
|
733
|
+
formatter: (value: number) =>
|
734
|
+
value >= 1000 ? formatCompact(value, 1) : value.toFixed(2),
|
735
|
+
};
|
736
|
+
case "errorRate":
|
737
|
+
return {
|
738
|
+
label: "Error Rate",
|
739
|
+
isBetter: "lower",
|
740
|
+
icon: TrendingDown,
|
741
|
+
formatter: (value: number) => `${(value * 100).toFixed(1)}%`,
|
742
|
+
};
|
743
|
+
default:
|
744
|
+
return {
|
745
|
+
label: "Requests per Second",
|
746
|
+
isBetter: "higher",
|
747
|
+
icon: TrendingUp,
|
748
|
+
formatter: (value: number) => formatCompact(value, 1),
|
749
|
+
};
|
750
|
+
}
|
751
|
+
}, [filters.metric]);
|
752
|
+
|
753
|
+
// Handle filter changes
|
754
|
+
const handleFilterChange = (key: keyof FilterState, value: any) => {
|
755
|
+
setFilters((prev) => ({ ...prev, [key]: value }));
|
756
|
+
};
|
757
|
+
|
758
|
+
// Toggle server visibility when clicking on legend
|
759
|
+
const handleLegendClick = useCallback(
|
760
|
+
(server: string, event?: React.MouseEvent) => {
|
761
|
+
// Disable animations during legend interaction
|
762
|
+
// setIsLegendInteracting(true)
|
763
|
+
|
764
|
+
// Check if Ctrl (Windows/Linux) or Cmd (Mac) key is pressed
|
765
|
+
const isExclusiveMode = event?.ctrlKey || event?.metaKey;
|
766
|
+
|
767
|
+
if (isExclusiveMode) {
|
768
|
+
// Ctrl/Cmd + click: Show only this server (hide all others)
|
769
|
+
const newVisibleServers: Record<string, boolean> = {};
|
770
|
+
allFilterOptions.servers.forEach((s) => {
|
771
|
+
newVisibleServers[s] = s === server;
|
772
|
+
});
|
773
|
+
setVisibleServers(newVisibleServers);
|
774
|
+
} else {
|
775
|
+
// Normal click: Toggle this server
|
776
|
+
setVisibleServers((prev) => ({
|
777
|
+
...prev,
|
778
|
+
[server]: !prev[server],
|
779
|
+
}));
|
780
|
+
}
|
781
|
+
|
782
|
+
// Re-enable animations after a short delay
|
783
|
+
// setTimeout(() => {
|
784
|
+
// setIsLegendInteracting(false)
|
785
|
+
// }, 100)
|
786
|
+
},
|
787
|
+
[allFilterOptions.servers],
|
788
|
+
);
|
789
|
+
|
790
|
+
// Calculate summary statistics
|
791
|
+
const summaryStats = useMemo(() => {
|
792
|
+
if (filteredData.length === 0) return null;
|
793
|
+
|
794
|
+
const values = filteredData.map((item) => {
|
795
|
+
switch (filters.metric) {
|
796
|
+
case "rps":
|
797
|
+
return item.results.requestsPerSec;
|
798
|
+
case "p95_latency":
|
799
|
+
return item.results.p95_latency;
|
800
|
+
case "errorRate":
|
801
|
+
return 1 - item.results.successRate;
|
802
|
+
default:
|
803
|
+
return item.results.requestsPerSec;
|
804
|
+
}
|
805
|
+
});
|
806
|
+
|
807
|
+
return {
|
808
|
+
count: filteredData.length,
|
809
|
+
min: Math.min(...values),
|
810
|
+
max: Math.max(...values),
|
811
|
+
avg: values.reduce((sum, val) => sum + val, 0) / values.length,
|
812
|
+
};
|
813
|
+
}, [filteredData, filters.metric]);
|
814
|
+
|
815
|
+
const topPerformers = useMemo(() => {
|
816
|
+
if (filteredData.length === 0) return [];
|
817
|
+
|
818
|
+
const currentGroup = (filteredData[0] as any).group;
|
819
|
+
if (!currentGroup) return [];
|
820
|
+
|
821
|
+
const performanceMap: Record<
|
822
|
+
string,
|
823
|
+
Record<string, Record<boolean, number[]>>
|
824
|
+
> = {};
|
825
|
+
|
826
|
+
flattenedData
|
827
|
+
.filter((item) => {
|
828
|
+
const itemWithCpu = item as any;
|
829
|
+
return (
|
830
|
+
itemWithCpu.cpu === filters.cpu && itemWithCpu.group === currentGroup
|
831
|
+
);
|
832
|
+
})
|
833
|
+
.forEach((item) => {
|
834
|
+
const key = `${item.test_case}_${item.threads}_${item.workers}_${item.concurrency}`;
|
835
|
+
|
836
|
+
if (!performanceMap[key]) {
|
837
|
+
performanceMap[key] = {};
|
838
|
+
}
|
839
|
+
|
840
|
+
if (!performanceMap[key][item.server]) {
|
841
|
+
performanceMap[key][item.server] = { true: [], false: [] };
|
842
|
+
}
|
843
|
+
|
844
|
+
performanceMap[key][item.server][item.http2].push(
|
845
|
+
item.results.requestsPerSec,
|
846
|
+
);
|
847
|
+
});
|
848
|
+
|
849
|
+
const winCounts: Record<string, number> = {};
|
850
|
+
|
851
|
+
Object.values(performanceMap).forEach((serverResults) => {
|
852
|
+
const allScores: [string, number][] = [];
|
853
|
+
|
854
|
+
for (const [server, variants] of Object.entries(serverResults)) {
|
855
|
+
for (const [http2, values] of Object.entries(variants)) {
|
856
|
+
const parsedHttp2 = http2 === "true"; // keys are string
|
857
|
+
if (values.length > 0) {
|
858
|
+
const avg = values.reduce((sum, v) => sum + v, 0) / values.length;
|
859
|
+
allScores.push([`${server}___${parsedHttp2}`, avg]);
|
860
|
+
}
|
861
|
+
}
|
862
|
+
}
|
863
|
+
|
864
|
+
if (allScores.length === 0) return;
|
865
|
+
|
866
|
+
const bestScore = Math.max(...allScores.map(([, v]) => v));
|
867
|
+
const winningServers = new Set(
|
868
|
+
allScores
|
869
|
+
.filter(([, v]) => v === bestScore)
|
870
|
+
.map(([k]) => k.split("___")[0]), // extract server
|
871
|
+
);
|
872
|
+
|
873
|
+
for (const server of winningServers) {
|
874
|
+
winCounts[server] = (winCounts[server] || 0) + 1;
|
875
|
+
}
|
876
|
+
});
|
877
|
+
|
878
|
+
return Object.entries(winCounts)
|
879
|
+
.sort(([, a], [, b]) => b - a)
|
880
|
+
.slice(0, 8)
|
881
|
+
.map(([server, count]) => ({ server, count }));
|
882
|
+
}, [flattenedData, filters.cpu, filteredData]);
|
883
|
+
|
884
|
+
// Handle chart hover
|
885
|
+
const handleChartHover = (props: any) => {
|
886
|
+
if (props.activePayload && props.activePayload.length > 0) {
|
887
|
+
const { dataKey, payload } = props.activePayload[0];
|
888
|
+
const serverData = payload[`${dataKey}_data`];
|
889
|
+
|
890
|
+
if (serverData) {
|
891
|
+
setHoveredPoint(serverData);
|
892
|
+
setActiveDataKey(dataKey);
|
893
|
+
return;
|
894
|
+
}
|
895
|
+
}
|
896
|
+
|
897
|
+
setHoveredPoint(null);
|
898
|
+
setActiveDataKey(null);
|
899
|
+
};
|
900
|
+
|
901
|
+
// Get visible servers and their data keys for the chart
|
902
|
+
const visibleDataKeys = useMemo(() => {
|
903
|
+
const keys: string[] = [];
|
904
|
+
|
905
|
+
filterOptions.servers.forEach((server) => {
|
906
|
+
if (visibleServers[server]) {
|
907
|
+
if (filters.http2 === "all") {
|
908
|
+
// Check if this server has both HTTP/1.1 and HTTP/2 data
|
909
|
+
const serverData = filteredData.filter(
|
910
|
+
(item) => item.server === server,
|
911
|
+
);
|
912
|
+
const hasHttp1 = serverData.some((item) => !item.http2);
|
913
|
+
const hasHttp2 = serverData.some((item) => item.http2);
|
914
|
+
|
915
|
+
if (hasHttp1) keys.push(`${formatServerName(server)} (HTTP/1.1)`);
|
916
|
+
if (hasHttp2) keys.push(`${formatServerName(server)} (HTTP/2)`);
|
917
|
+
} else {
|
918
|
+
keys.push(formatServerName(server));
|
919
|
+
}
|
920
|
+
}
|
921
|
+
});
|
922
|
+
|
923
|
+
return keys;
|
924
|
+
}, [
|
925
|
+
filterOptions.servers,
|
926
|
+
visibleServers,
|
927
|
+
filters.http2,
|
928
|
+
filteredData,
|
929
|
+
formatServerName,
|
930
|
+
]);
|
931
|
+
|
932
|
+
// Get protocol display text
|
933
|
+
const getProtocolDisplay = () => {
|
934
|
+
if (filters.http2 === "all") return "All Protocols";
|
935
|
+
return filters.http2 ? "HTTP/2" : "HTTP/1.1";
|
936
|
+
};
|
937
|
+
|
938
|
+
// Show loading state if no data
|
939
|
+
const visibleServersForUrl = useMemo(() => {
|
940
|
+
return Object.keys(visibleServers).filter(
|
941
|
+
(server) => visibleServers[server],
|
942
|
+
);
|
943
|
+
}, [visibleServers]);
|
944
|
+
|
945
|
+
// Update URL parameters when visible servers change
|
946
|
+
useEffect(() => {
|
947
|
+
updateUrlParams({ ...filters, visibleServers: visibleServersForUrl });
|
948
|
+
}, [visibleServersForUrl, filters, updateUrlParams]);
|
949
|
+
|
950
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
951
|
+
return (
|
952
|
+
<div className="flex h-64 items-center justify-center">
|
953
|
+
<p className="text-gray-500">No benchmark data available</p>
|
954
|
+
</div>
|
955
|
+
);
|
956
|
+
}
|
957
|
+
|
958
|
+
return (
|
959
|
+
<div className="grid grid-cols-1 gap-2">
|
960
|
+
<div className="grid grid-cols-1 md:grid-cols-12 gap-2">
|
961
|
+
{/* Left column: Filters */}
|
962
|
+
<div className="md:col-span-3">
|
963
|
+
<Card className="shadow-sm h-full">
|
964
|
+
<CardHeader className="py-2 px-3">
|
965
|
+
<CardTitle className="text-sm">Filters</CardTitle>
|
966
|
+
</CardHeader>
|
967
|
+
<CardContent className="p-2">
|
968
|
+
<div className="space-y-2">
|
969
|
+
{/* CPU filter */}
|
970
|
+
<div className="space-y-1">
|
971
|
+
<Label htmlFor="cpu-filter" className="text-xs">
|
972
|
+
CPU
|
973
|
+
</Label>
|
974
|
+
<Select
|
975
|
+
value={filters.cpu}
|
976
|
+
onValueChange={(value) => handleFilterChange("cpu", value)}
|
977
|
+
>
|
978
|
+
<SelectTrigger id="cpu-filter" className="h-7 text-xs">
|
979
|
+
<SelectValue placeholder="Select CPU" />
|
980
|
+
</SelectTrigger>
|
981
|
+
<SelectContent>
|
982
|
+
{filterOptions.cpus.map((cpu) => (
|
983
|
+
<SelectItem key={cpu} value={cpu} className="text-xs">
|
984
|
+
{cpu}
|
985
|
+
</SelectItem>
|
986
|
+
))}
|
987
|
+
</SelectContent>
|
988
|
+
</Select>
|
989
|
+
</div>
|
990
|
+
|
991
|
+
{/* Test case filter with groups */}
|
992
|
+
<div className="space-y-1">
|
993
|
+
<Label htmlFor="test-case-filter" className="text-xs">
|
994
|
+
Test Case
|
995
|
+
</Label>
|
996
|
+
<Select
|
997
|
+
value={filters.testCase}
|
998
|
+
onValueChange={(value) =>
|
999
|
+
handleFilterChange("testCase", value)
|
1000
|
+
}
|
1001
|
+
>
|
1002
|
+
<SelectTrigger
|
1003
|
+
id="test-case-filter"
|
1004
|
+
className="h-7 text-xs"
|
1005
|
+
>
|
1006
|
+
<SelectValue placeholder="Select test" />
|
1007
|
+
</SelectTrigger>
|
1008
|
+
<SelectContent>
|
1009
|
+
{groupedTestCases.sortedGroups.map((group) => (
|
1010
|
+
<SelectGroup key={group}>
|
1011
|
+
<SelectLabel className="text-xs font-medium text-muted-foreground pl-2">
|
1012
|
+
{group}
|
1013
|
+
</SelectLabel>
|
1014
|
+
{groupedTestCases.groupedTests[group].map(
|
1015
|
+
(testCase) => (
|
1016
|
+
<SelectItem
|
1017
|
+
key={testCase}
|
1018
|
+
value={testCase}
|
1019
|
+
className="text-xs pl-6"
|
1020
|
+
>
|
1021
|
+
{testCase}
|
1022
|
+
</SelectItem>
|
1023
|
+
),
|
1024
|
+
)}
|
1025
|
+
</SelectGroup>
|
1026
|
+
))}
|
1027
|
+
</SelectContent>
|
1028
|
+
</Select>
|
1029
|
+
</div>
|
1030
|
+
|
1031
|
+
<div className="grid grid-cols-2 gap-2">
|
1032
|
+
{/* Threads filter - disabled if xAxis is threads */}
|
1033
|
+
<div className="space-y-1">
|
1034
|
+
<Label htmlFor="threads-filter" className="text-xs">
|
1035
|
+
Threads
|
1036
|
+
</Label>
|
1037
|
+
<Select
|
1038
|
+
value={filters.threads.toString()}
|
1039
|
+
onValueChange={(value) =>
|
1040
|
+
handleFilterChange("threads", Number.parseInt(value))
|
1041
|
+
}
|
1042
|
+
disabled={filters.xAxis === "threads"}
|
1043
|
+
>
|
1044
|
+
<SelectTrigger
|
1045
|
+
id="threads-filter"
|
1046
|
+
className="h-7 text-xs"
|
1047
|
+
>
|
1048
|
+
<SelectValue
|
1049
|
+
placeholder={
|
1050
|
+
filters.xAxis === "threads"
|
1051
|
+
? "On X-Axis"
|
1052
|
+
: "Select threads"
|
1053
|
+
}
|
1054
|
+
/>
|
1055
|
+
</SelectTrigger>
|
1056
|
+
<SelectContent>
|
1057
|
+
{filterOptions.threads.length > 0 ? (
|
1058
|
+
filterOptions.threads.map((thread) => (
|
1059
|
+
<SelectItem
|
1060
|
+
key={thread}
|
1061
|
+
value={thread.toString()}
|
1062
|
+
className="text-xs"
|
1063
|
+
>
|
1064
|
+
{thread}
|
1065
|
+
</SelectItem>
|
1066
|
+
))
|
1067
|
+
) : (
|
1068
|
+
<SelectItem value="1" className="text-xs">
|
1069
|
+
1
|
1070
|
+
</SelectItem>
|
1071
|
+
)}
|
1072
|
+
</SelectContent>
|
1073
|
+
</Select>
|
1074
|
+
</div>
|
1075
|
+
|
1076
|
+
{/* Workers filter - disabled if xAxis is workers */}
|
1077
|
+
<div className="space-y-1">
|
1078
|
+
<Label htmlFor="workers-filter" className="text-xs">
|
1079
|
+
Workers
|
1080
|
+
</Label>
|
1081
|
+
<Select
|
1082
|
+
value={filters.workers.toString()}
|
1083
|
+
onValueChange={(value) =>
|
1084
|
+
handleFilterChange("workers", Number.parseInt(value))
|
1085
|
+
}
|
1086
|
+
disabled={filters.xAxis === "workers"}
|
1087
|
+
>
|
1088
|
+
<SelectTrigger
|
1089
|
+
id="workers-filter"
|
1090
|
+
className="h-7 text-xs"
|
1091
|
+
>
|
1092
|
+
<SelectValue
|
1093
|
+
placeholder={
|
1094
|
+
filters.xAxis === "workers"
|
1095
|
+
? "On X-Axis"
|
1096
|
+
: "Select workers"
|
1097
|
+
}
|
1098
|
+
/>
|
1099
|
+
</SelectTrigger>
|
1100
|
+
<SelectContent>
|
1101
|
+
{filterOptions.workers.length > 0 ? (
|
1102
|
+
filterOptions.workers.map((worker) => (
|
1103
|
+
<SelectItem
|
1104
|
+
key={worker}
|
1105
|
+
value={worker.toString()}
|
1106
|
+
className="text-xs"
|
1107
|
+
>
|
1108
|
+
{worker}
|
1109
|
+
</SelectItem>
|
1110
|
+
))
|
1111
|
+
) : (
|
1112
|
+
<SelectItem value="1" className="text-xs">
|
1113
|
+
1
|
1114
|
+
</SelectItem>
|
1115
|
+
)}
|
1116
|
+
</SelectContent>
|
1117
|
+
</Select>
|
1118
|
+
</div>
|
1119
|
+
</div>
|
1120
|
+
|
1121
|
+
<div className="grid grid-cols-2 gap-2">
|
1122
|
+
{/* Concurrency filter - disabled if xAxis is concurrency */}
|
1123
|
+
<div className="space-y-1">
|
1124
|
+
<Label htmlFor="concurrency-filter" className="text-xs">
|
1125
|
+
Concurrency
|
1126
|
+
</Label>
|
1127
|
+
<Select
|
1128
|
+
value={filters.concurrency.toString()}
|
1129
|
+
onValueChange={(value) =>
|
1130
|
+
handleFilterChange(
|
1131
|
+
"concurrency",
|
1132
|
+
Number.parseInt(value),
|
1133
|
+
)
|
1134
|
+
}
|
1135
|
+
disabled={filters.xAxis === "concurrency"}
|
1136
|
+
>
|
1137
|
+
<SelectTrigger
|
1138
|
+
id="concurrency-filter"
|
1139
|
+
className="h-7 text-xs"
|
1140
|
+
>
|
1141
|
+
<SelectValue
|
1142
|
+
placeholder={
|
1143
|
+
filters.xAxis === "concurrency"
|
1144
|
+
? "On X-Axis"
|
1145
|
+
: "Select concurrency"
|
1146
|
+
}
|
1147
|
+
/>
|
1148
|
+
</SelectTrigger>
|
1149
|
+
<SelectContent>
|
1150
|
+
{filterOptions.concurrencyLevels.length > 0 ? (
|
1151
|
+
filterOptions.concurrencyLevels.map((level) => (
|
1152
|
+
<SelectItem
|
1153
|
+
key={level}
|
1154
|
+
value={level.toString()}
|
1155
|
+
className="text-xs"
|
1156
|
+
>
|
1157
|
+
{level}
|
1158
|
+
</SelectItem>
|
1159
|
+
))
|
1160
|
+
) : (
|
1161
|
+
<SelectItem value="10" className="text-xs">
|
1162
|
+
10
|
1163
|
+
</SelectItem>
|
1164
|
+
)}
|
1165
|
+
</SelectContent>
|
1166
|
+
</Select>
|
1167
|
+
</div>
|
1168
|
+
|
1169
|
+
{/* HTTP2 filter */}
|
1170
|
+
<div className="space-y-1">
|
1171
|
+
<Label htmlFor="http2-filter" className="text-xs">
|
1172
|
+
Protocol
|
1173
|
+
</Label>
|
1174
|
+
<Select
|
1175
|
+
value={filters.http2.toString()}
|
1176
|
+
onValueChange={(value) => {
|
1177
|
+
if (value === "all") {
|
1178
|
+
handleFilterChange("http2", "all");
|
1179
|
+
} else {
|
1180
|
+
handleFilterChange("http2", value === "true");
|
1181
|
+
}
|
1182
|
+
}}
|
1183
|
+
>
|
1184
|
+
<SelectTrigger id="http2-filter" className="h-7 text-xs">
|
1185
|
+
<SelectValue placeholder="Select protocol" />
|
1186
|
+
</SelectTrigger>
|
1187
|
+
<SelectContent>
|
1188
|
+
<SelectItem value="all" className="text-xs">
|
1189
|
+
All
|
1190
|
+
</SelectItem>
|
1191
|
+
{filterOptions.http2Options
|
1192
|
+
.filter((option) => option !== "all")
|
1193
|
+
.map((option) => (
|
1194
|
+
<SelectItem
|
1195
|
+
key={String(option)}
|
1196
|
+
value={String(option)}
|
1197
|
+
className="text-xs"
|
1198
|
+
>
|
1199
|
+
{option ? "HTTP/2" : "HTTP/1.1"}
|
1200
|
+
</SelectItem>
|
1201
|
+
))}
|
1202
|
+
</SelectContent>
|
1203
|
+
</Select>
|
1204
|
+
</div>
|
1205
|
+
</div>
|
1206
|
+
|
1207
|
+
{/* X-Axis selection */}
|
1208
|
+
<div className="space-y-1">
|
1209
|
+
<Label htmlFor="x-axis-select" className="text-xs">
|
1210
|
+
X-Axis
|
1211
|
+
</Label>
|
1212
|
+
<Select
|
1213
|
+
value={filters.xAxis}
|
1214
|
+
onValueChange={(value) =>
|
1215
|
+
handleFilterChange("xAxis", value)
|
1216
|
+
}
|
1217
|
+
>
|
1218
|
+
<SelectTrigger id="x-axis-select" className="h-7 text-xs">
|
1219
|
+
<SelectValue placeholder="X-Axis" />
|
1220
|
+
</SelectTrigger>
|
1221
|
+
<SelectContent>
|
1222
|
+
<SelectItem value="concurrency" className="text-xs">
|
1223
|
+
Concurrency
|
1224
|
+
</SelectItem>
|
1225
|
+
<SelectItem value="threads" className="text-xs">
|
1226
|
+
Threads
|
1227
|
+
</SelectItem>
|
1228
|
+
<SelectItem value="workers" className="text-xs">
|
1229
|
+
Workers
|
1230
|
+
</SelectItem>
|
1231
|
+
</SelectContent>
|
1232
|
+
</Select>
|
1233
|
+
</div>
|
1234
|
+
|
1235
|
+
{/* Metric selection */}
|
1236
|
+
<div className="space-y-1">
|
1237
|
+
<Label htmlFor="metric-select" className="text-xs">
|
1238
|
+
Metric
|
1239
|
+
</Label>
|
1240
|
+
<Tabs
|
1241
|
+
value={filters.metric}
|
1242
|
+
onValueChange={(value) =>
|
1243
|
+
handleFilterChange("metric", value)
|
1244
|
+
}
|
1245
|
+
className="w-full"
|
1246
|
+
>
|
1247
|
+
<TabsList className="h-6 w-full">
|
1248
|
+
<TabsTrigger
|
1249
|
+
value="rps"
|
1250
|
+
className="text-xs px-2 py-0 h-4 flex-1"
|
1251
|
+
>
|
1252
|
+
RPS
|
1253
|
+
</TabsTrigger>
|
1254
|
+
<TabsTrigger
|
1255
|
+
value="p95_latency"
|
1256
|
+
className="text-xs px-2 py-0 h-4 flex-1"
|
1257
|
+
>
|
1258
|
+
P95
|
1259
|
+
</TabsTrigger>
|
1260
|
+
<TabsTrigger
|
1261
|
+
value="errorRate"
|
1262
|
+
className="text-xs px-2 py-0 h-4 flex-1"
|
1263
|
+
>
|
1264
|
+
Errors
|
1265
|
+
</TabsTrigger>
|
1266
|
+
</TabsList>
|
1267
|
+
</Tabs>
|
1268
|
+
|
1269
|
+
{/* Better indicator */}
|
1270
|
+
<div className="flex items-center justify-center text-xs text-muted-foreground">
|
1271
|
+
<metricInfo.icon className="h-3 w-3 mr-1" />
|
1272
|
+
<span>{metricInfo.isBetter} is better</span>
|
1273
|
+
</div>
|
1274
|
+
</div>
|
1275
|
+
|
1276
|
+
{/* Compact Summary Stats */}
|
1277
|
+
{summaryStats && (
|
1278
|
+
<div className="pt-1 space-y-1 text-xs">
|
1279
|
+
<div className="flex justify-between">
|
1280
|
+
<span className="text-muted-foreground">Results:</span>
|
1281
|
+
<span className="font-medium">{summaryStats.count}</span>
|
1282
|
+
</div>
|
1283
|
+
<div className="flex justify-between">
|
1284
|
+
<span className="text-muted-foreground">Min:</span>
|
1285
|
+
<span className="font-medium">
|
1286
|
+
{metricInfo.formatter(summaryStats.min)}
|
1287
|
+
</span>
|
1288
|
+
</div>
|
1289
|
+
<div className="flex justify-between">
|
1290
|
+
<span className="text-muted-foreground">Max:</span>
|
1291
|
+
<span className="font-medium">
|
1292
|
+
{metricInfo.formatter(summaryStats.max)}
|
1293
|
+
</span>
|
1294
|
+
</div>
|
1295
|
+
<div className="flex justify-between">
|
1296
|
+
<span className="text-muted-foreground">Avg:</span>
|
1297
|
+
<span className="font-medium">
|
1298
|
+
{metricInfo.formatter(summaryStats.avg)}
|
1299
|
+
</span>
|
1300
|
+
</div>
|
1301
|
+
</div>
|
1302
|
+
)}
|
1303
|
+
</div>
|
1304
|
+
</CardContent>
|
1305
|
+
</Card>
|
1306
|
+
</div>
|
1307
|
+
|
1308
|
+
{/* Right column: Chart and Error Distribution */}
|
1309
|
+
<div className="md:col-span-9">
|
1310
|
+
<div className="space-y-2">
|
1311
|
+
{/* Main Chart */}
|
1312
|
+
<Card className="shadow">
|
1313
|
+
<CardHeader className="pb-0 pt-2 px-3">
|
1314
|
+
<CardTitle className="text-base">
|
1315
|
+
{metricInfo.label}: {filters.cpu} - {filters.testCase}
|
1316
|
+
<span className="text-xs font-normal ml-2 text-muted-foreground">
|
1317
|
+
{filters.xAxis !== "threads" &&
|
1318
|
+
`Threads: ${filters.threads}, `}
|
1319
|
+
{filters.xAxis !== "workers" &&
|
1320
|
+
`Workers: ${filters.workers}, `}
|
1321
|
+
{filters.xAxis !== "concurrency" &&
|
1322
|
+
`Concurrency: ${filters.concurrency}, `}
|
1323
|
+
Protocol: {getProtocolDisplay()}
|
1324
|
+
</span>
|
1325
|
+
</CardTitle>
|
1326
|
+
</CardHeader>
|
1327
|
+
<CardContent className="p-2">
|
1328
|
+
{chartData.length === 0 ? (
|
1329
|
+
<div className="flex h-64 items-center justify-center">
|
1330
|
+
<p className="text-gray-500">
|
1331
|
+
No data available for the selected filters
|
1332
|
+
</p>
|
1333
|
+
</div>
|
1334
|
+
) : (
|
1335
|
+
<>
|
1336
|
+
<div className="h-[280px]">
|
1337
|
+
<ResponsiveContainer width="100%" height="100%">
|
1338
|
+
<BarChart
|
1339
|
+
data={chartData}
|
1340
|
+
margin={{ top: 10, right: 30, left: 30, bottom: 20 }}
|
1341
|
+
barCategoryGap={0}
|
1342
|
+
barGap={0}
|
1343
|
+
>
|
1344
|
+
<CartesianGrid strokeDasharray="3 3" opacity={0.7} />
|
1345
|
+
<XAxis
|
1346
|
+
dataKey={filters.xAxis}
|
1347
|
+
label={{
|
1348
|
+
value:
|
1349
|
+
filters.xAxis.charAt(0).toUpperCase() +
|
1350
|
+
filters.xAxis.slice(1),
|
1351
|
+
position: "insideBottom",
|
1352
|
+
offset: -5,
|
1353
|
+
}}
|
1354
|
+
/>
|
1355
|
+
|
1356
|
+
<YAxis
|
1357
|
+
label={{
|
1358
|
+
value: metricInfo.label,
|
1359
|
+
angle: -90,
|
1360
|
+
position: "insideLeft",
|
1361
|
+
offset: -15,
|
1362
|
+
style: { textAnchor: "middle" },
|
1363
|
+
}}
|
1364
|
+
tickFormatter={metricInfo.formatter}
|
1365
|
+
/>
|
1366
|
+
|
1367
|
+
{/* Render bars for each visible data key */}
|
1368
|
+
{visibleDataKeys.map((dataKey, index) => (
|
1369
|
+
<Bar
|
1370
|
+
key={dataKey}
|
1371
|
+
dataKey={dataKey}
|
1372
|
+
name={dataKey}
|
1373
|
+
fill={
|
1374
|
+
serverColors[
|
1375
|
+
dataKey.split(" ")[0].replace(/\+/g, "__")
|
1376
|
+
] ||
|
1377
|
+
serverColors[
|
1378
|
+
Object.keys(serverColors)[
|
1379
|
+
index % Object.keys(serverColors).length
|
1380
|
+
]
|
1381
|
+
]
|
1382
|
+
}
|
1383
|
+
opacity={activeDataKey === dataKey ? 1 : 0.8}
|
1384
|
+
stackId={index}
|
1385
|
+
isAnimationActive={true}
|
1386
|
+
onMouseOver={(data) =>
|
1387
|
+
handleChartHover({
|
1388
|
+
activePayload: [
|
1389
|
+
{ dataKey, payload: data.payload },
|
1390
|
+
],
|
1391
|
+
})
|
1392
|
+
}
|
1393
|
+
onMouseLeave={() => {
|
1394
|
+
setHoveredPoint(null);
|
1395
|
+
setActiveDataKey(null);
|
1396
|
+
}}
|
1397
|
+
/>
|
1398
|
+
))}
|
1399
|
+
|
1400
|
+
{/* Add invisible bars for zero values to enable hovering */}
|
1401
|
+
{visibleDataKeys.map((dataKey, index) => (
|
1402
|
+
<Bar
|
1403
|
+
key={`${dataKey}_hover`}
|
1404
|
+
dataKey={dataKey}
|
1405
|
+
name={`${dataKey}_hover`}
|
1406
|
+
fill="transparent"
|
1407
|
+
stroke="transparent"
|
1408
|
+
strokeWidth={0}
|
1409
|
+
minPointSize={20}
|
1410
|
+
stackId={`hover_${index}`}
|
1411
|
+
isAnimationActive={true}
|
1412
|
+
onMouseOver={(data) => {
|
1413
|
+
if (data.payload[dataKey] === 0) {
|
1414
|
+
handleChartHover({
|
1415
|
+
activePayload: [
|
1416
|
+
{ dataKey, payload: data.payload },
|
1417
|
+
],
|
1418
|
+
});
|
1419
|
+
}
|
1420
|
+
}}
|
1421
|
+
onMouseLeave={() => {
|
1422
|
+
setHoveredPoint(null);
|
1423
|
+
setActiveDataKey(null);
|
1424
|
+
}}
|
1425
|
+
/>
|
1426
|
+
))}
|
1427
|
+
</BarChart>
|
1428
|
+
</ResponsiveContainer>
|
1429
|
+
</div>
|
1430
|
+
|
1431
|
+
{/* Server Toggle Legend */}
|
1432
|
+
<div className="flex flex-wrap gap-3 justify-center mt-1">
|
1433
|
+
{filterOptions.servers.map((server) => (
|
1434
|
+
<div
|
1435
|
+
key={server}
|
1436
|
+
className={`flex items-center gap-1 px-2 py-0.5 rounded cursor-pointer transition-opacity ${
|
1437
|
+
visibleServers[server]
|
1438
|
+
? "opacity-100"
|
1439
|
+
: "opacity-40"
|
1440
|
+
}`}
|
1441
|
+
onClick={(event) => handleLegendClick(server, event)}
|
1442
|
+
title={`Click to toggle, ${navigator.platform.includes("Mac") ? "Cmd" : "Ctrl"}+click to show only this server`}
|
1443
|
+
>
|
1444
|
+
<div
|
1445
|
+
className="w-2 h-2 rounded-sm"
|
1446
|
+
style={{ backgroundColor: serverColors[server] }}
|
1447
|
+
/>
|
1448
|
+
<span className="text-xs">
|
1449
|
+
{formatServerName(server)}
|
1450
|
+
</span>
|
1451
|
+
</div>
|
1452
|
+
))}
|
1453
|
+
</div>
|
1454
|
+
</>
|
1455
|
+
)}
|
1456
|
+
</CardContent>
|
1457
|
+
</Card>
|
1458
|
+
|
1459
|
+
{/* Top Performers and Benchmark Details in horizontal layout */}
|
1460
|
+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-2">
|
1461
|
+
<div
|
1462
|
+
className={
|
1463
|
+
topPerformers.length > 0 ? "lg:col-span-7" : "lg:col-span-12"
|
1464
|
+
}
|
1465
|
+
>
|
1466
|
+
<Card className="shadow-sm h-full">
|
1467
|
+
<CardHeader className="py-1 px-3">
|
1468
|
+
<CardTitle className="text-sm flex items-center">
|
1469
|
+
{hoveredPoint ? "Benchmark Details" : "Hover Details"}
|
1470
|
+
{!hoveredPoint && (
|
1471
|
+
<span className="ml-2 text-xs font-normal text-muted-foreground flex items-center">
|
1472
|
+
<InfoIcon className="h-3 w-3 mr-1" />
|
1473
|
+
Hover over chart bars to see details
|
1474
|
+
</span>
|
1475
|
+
)}
|
1476
|
+
</CardTitle>
|
1477
|
+
</CardHeader>
|
1478
|
+
<CardContent className="p-2">
|
1479
|
+
{hoveredPoint ? (
|
1480
|
+
<div className="space-y-2">
|
1481
|
+
{/* Header with server and test case */}
|
1482
|
+
<div className="flex flex-wrap items-center gap-2">
|
1483
|
+
<Badge
|
1484
|
+
variant="outline"
|
1485
|
+
className="flex items-center"
|
1486
|
+
>
|
1487
|
+
<div
|
1488
|
+
className="w-2 h-2 mr-1 rounded-full"
|
1489
|
+
style={{
|
1490
|
+
backgroundColor:
|
1491
|
+
serverColors[hoveredPoint.server],
|
1492
|
+
}}
|
1493
|
+
/>
|
1494
|
+
{formatServerName(hoveredPoint.server)}
|
1495
|
+
</Badge>
|
1496
|
+
<Badge variant="outline">
|
1497
|
+
{hoveredPoint.test_case}
|
1498
|
+
</Badge>
|
1499
|
+
<Badge variant="outline">
|
1500
|
+
{filters.xAxis}:{" "}
|
1501
|
+
{String(
|
1502
|
+
hoveredPoint[
|
1503
|
+
filters.xAxis as keyof BenchmarkResult
|
1504
|
+
],
|
1505
|
+
)}
|
1506
|
+
</Badge>
|
1507
|
+
<Badge variant="outline">
|
1508
|
+
{hoveredPoint.http2 ? "HTTP/2" : "HTTP/1.1"}
|
1509
|
+
</Badge>
|
1510
|
+
</div>
|
1511
|
+
|
1512
|
+
{/* Version information in a separate row */}
|
1513
|
+
{hoveredPoint.version && (
|
1514
|
+
<div className="flex items-center text-xs text-muted-foreground">
|
1515
|
+
<TagIcon className="h-3 w-3 mr-1" />
|
1516
|
+
<span className="font-medium overflow-hidden text-ellipsis">
|
1517
|
+
{hoveredPoint.version}
|
1518
|
+
</span>
|
1519
|
+
</div>
|
1520
|
+
)}
|
1521
|
+
|
1522
|
+
{/* Performance metrics */}
|
1523
|
+
<div className="grid grid-cols-2 md:grid-cols-2 gap-x-2 gap-y-1 text-xs">
|
1524
|
+
<div className="flex justify-between">
|
1525
|
+
<span className="text-muted-foreground">RPS:</span>
|
1526
|
+
<span className="font-medium">
|
1527
|
+
{hoveredPoint.results.requestsPerSec.toFixed(2)}
|
1528
|
+
</span>
|
1529
|
+
</div>
|
1530
|
+
<div className="flex justify-between">
|
1531
|
+
<span className="text-muted-foreground">
|
1532
|
+
Success Rate:
|
1533
|
+
</span>
|
1534
|
+
<span
|
1535
|
+
className={`font-medium ${hoveredPoint.results.successRate < 1 ? "text-red-600" : ""}`}
|
1536
|
+
>
|
1537
|
+
{(hoveredPoint.results.successRate * 100).toFixed(
|
1538
|
+
2,
|
1539
|
+
)}
|
1540
|
+
%
|
1541
|
+
</span>
|
1542
|
+
</div>
|
1543
|
+
<br />
|
1544
|
+
</div>
|
1545
|
+
<div className="grid grid-cols-2 md:grid-cols-2 gap-x-2 gap-y-1 text-xs">
|
1546
|
+
<div className="flex justify-between">
|
1547
|
+
<span className="text-muted-foreground">
|
1548
|
+
P95 Latency:
|
1549
|
+
</span>
|
1550
|
+
<span className="font-medium">
|
1551
|
+
{hoveredPoint.results.p95_latency != null
|
1552
|
+
? `${hoveredPoint.results.p95_latency.toFixed(2)} ms`
|
1553
|
+
: "N/A"}
|
1554
|
+
</span>
|
1555
|
+
</div>
|
1556
|
+
<div className="flex justify-between">
|
1557
|
+
<span className="text-muted-foreground">
|
1558
|
+
Avg Latency:
|
1559
|
+
</span>
|
1560
|
+
<span className="font-medium">
|
1561
|
+
{hoveredPoint.results.average != null
|
1562
|
+
? `${hoveredPoint.results.average.toFixed(2)} ms`
|
1563
|
+
: "N/A"}
|
1564
|
+
</span>
|
1565
|
+
</div>
|
1566
|
+
</div>
|
1567
|
+
|
1568
|
+
{/* Error distribution */}
|
1569
|
+
{Object.keys(
|
1570
|
+
hoveredPoint.results.errorDistribution || {},
|
1571
|
+
).length > 0 ? (
|
1572
|
+
<div>
|
1573
|
+
<div className="text-xs font-medium mb-1">
|
1574
|
+
Error Distribution:
|
1575
|
+
</div>
|
1576
|
+
<div className="space-y-1">
|
1577
|
+
{Object.entries(
|
1578
|
+
hoveredPoint.results.errorDistribution || {},
|
1579
|
+
).map(([errorType, count]) => (
|
1580
|
+
<div
|
1581
|
+
key={errorType}
|
1582
|
+
className="flex items-center justify-between text-xs"
|
1583
|
+
>
|
1584
|
+
<span>{errorType}</span>
|
1585
|
+
<Badge
|
1586
|
+
variant="destructive"
|
1587
|
+
className="text-xs py-0"
|
1588
|
+
>
|
1589
|
+
{count}
|
1590
|
+
</Badge>
|
1591
|
+
</div>
|
1592
|
+
))}
|
1593
|
+
</div>
|
1594
|
+
</div>
|
1595
|
+
) : (
|
1596
|
+
<p className="text-xs text-muted-foreground">
|
1597
|
+
No errors reported
|
1598
|
+
</p>
|
1599
|
+
)}
|
1600
|
+
</div>
|
1601
|
+
) : (
|
1602
|
+
<p className="text-xs text-muted-foreground">
|
1603
|
+
Hover over a data point to see benchmark details
|
1604
|
+
</p>
|
1605
|
+
)}
|
1606
|
+
</CardContent>
|
1607
|
+
</Card>
|
1608
|
+
</div>
|
1609
|
+
{topPerformers.length > 0 && (
|
1610
|
+
<div className="lg:col-span-5">
|
1611
|
+
<Card className="shadow-sm h-full">
|
1612
|
+
<CardHeader className="py-1 px-3">
|
1613
|
+
<CardTitle className="text-sm flex items-center">
|
1614
|
+
Top Performers{" "}
|
1615
|
+
{filteredData[0] && (filteredData[0] as any).group && (
|
1616
|
+
<span className="ml-1 text-xs text-muted-foreground">
|
1617
|
+
({(filteredData[0] as any).group})
|
1618
|
+
</span>
|
1619
|
+
)}
|
1620
|
+
</CardTitle>
|
1621
|
+
</CardHeader>
|
1622
|
+
<CardContent className="p-2">
|
1623
|
+
<div className="grid grid-cols-2 gap-x-3 gap-y-1">
|
1624
|
+
{topPerformers.map(({ server, count }) => (
|
1625
|
+
<div
|
1626
|
+
key={server}
|
1627
|
+
className="flex items-center justify-between text-xs"
|
1628
|
+
>
|
1629
|
+
<div className="flex items-center gap-1">
|
1630
|
+
<div
|
1631
|
+
className="w-2 h-2 rounded-full"
|
1632
|
+
style={{
|
1633
|
+
backgroundColor: serverColors[server],
|
1634
|
+
}}
|
1635
|
+
/>
|
1636
|
+
<span className="truncate">
|
1637
|
+
{formatServerName(server)}
|
1638
|
+
</span>
|
1639
|
+
</div>
|
1640
|
+
<span className="text-muted-foreground ml-1">
|
1641
|
+
{count}
|
1642
|
+
</span>
|
1643
|
+
</div>
|
1644
|
+
))}
|
1645
|
+
</div>
|
1646
|
+
{(filteredData[0] as any).group == "rack" && (
|
1647
|
+
<p className="text-[10px] text-muted-foreground mt-2 px-1 leading-snug">
|
1648
|
+
Note: Some servers (e.g. Unicorn, Agoo) don’t
|
1649
|
+
participate in multi-threaded test cases so will
|
1650
|
+
appear less frequently in these results.
|
1651
|
+
</p>
|
1652
|
+
)}
|
1653
|
+
</CardContent>
|
1654
|
+
</Card>
|
1655
|
+
</div>
|
1656
|
+
)}
|
1657
|
+
</div>
|
1658
|
+
</div>
|
1659
|
+
</div>
|
1660
|
+
</div>
|
1661
|
+
</div>
|
1662
|
+
);
|
1663
|
+
}
|