@rulebricks/cli 2.0.0 → 2.0.2
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.
- package/README.md +3 -3
- package/benchmarks/README.md +98 -0
- package/benchmarks/Test Flow.rbf +4088 -0
- package/benchmarks/benchmark-flow.json +26 -0
- package/benchmarks/lib/payload.js +101 -0
- package/benchmarks/lib/report.js +929 -0
- package/benchmarks/qps-test.js +136 -0
- package/benchmarks/run-qps-test.sh +115 -0
- package/benchmarks/run-throughput-test.sh +123 -0
- package/benchmarks/throughput-report.html +632 -0
- package/benchmarks/throughput-results.json +298 -0
- package/benchmarks/throughput-test.js +159 -0
- package/dist/commands/benchmark.d.ts +11 -0
- package/dist/commands/benchmark.js +173 -0
- package/dist/commands/deploy.js +15 -4
- package/dist/commands/destroy.js +2 -2
- package/dist/commands/logs.js +1 -0
- package/dist/components/Wizard/steps/BenchmarkSteps.d.ts +31 -0
- package/dist/components/Wizard/steps/BenchmarkSteps.js +304 -0
- package/dist/components/Wizard/steps/DatabaseStep.js +49 -35
- package/dist/index.js +42 -6
- package/dist/lib/benchmark.d.ts +63 -0
- package/dist/lib/benchmark.js +466 -0
- package/dist/lib/dns.d.ts +3 -1
- package/dist/lib/dns.js +138 -56
- package/dist/lib/helm.d.ts +14 -1
- package/dist/lib/helm.js +36 -1
- package/dist/lib/kubernetes.js +2 -0
- package/dist/types/index.d.ts +90 -0
- package/dist/types/index.js +51 -0
- package/package.json +8 -6
- package/terraform/aws/main.tf +22 -0
- package/terraform/azure/main.tf +45 -0
- package/terraform/gcp/main.tf +34 -0
- /package/{email-templates → templates}/email_change.html +0 -0
- /package/{email-templates → templates}/invite.html +0 -0
- /package/{email-templates → templates}/password_change.html +0 -0
- /package/{email-templates → templates}/verify.html +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root_group": {
|
|
3
|
+
"name": "",
|
|
4
|
+
"path": "",
|
|
5
|
+
"id": "d41d8cd98f00b204e9800998ecf8427e",
|
|
6
|
+
"groups": [],
|
|
7
|
+
"checks": [
|
|
8
|
+
{
|
|
9
|
+
"name": "status is 200",
|
|
10
|
+
"path": "::status is 200",
|
|
11
|
+
"id": "6210a8cd14cd70477eba5c5e4cb3fb5f",
|
|
12
|
+
"passes": 24000,
|
|
13
|
+
"fails": 0
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "valid response",
|
|
17
|
+
"path": "::valid response",
|
|
18
|
+
"id": "74e6940490a7b557672e61f02e36f4e8",
|
|
19
|
+
"passes": 24000,
|
|
20
|
+
"fails": 0
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "no error in response",
|
|
24
|
+
"path": "::no error in response",
|
|
25
|
+
"id": "04d657588d6c66446d4937ecba2292a2",
|
|
26
|
+
"passes": 24000,
|
|
27
|
+
"fails": 0
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"options": {
|
|
32
|
+
"summaryTrendStats": [
|
|
33
|
+
"avg",
|
|
34
|
+
"min",
|
|
35
|
+
"med",
|
|
36
|
+
"max",
|
|
37
|
+
"p(90)",
|
|
38
|
+
"p(95)"
|
|
39
|
+
],
|
|
40
|
+
"summaryTimeUnit": "",
|
|
41
|
+
"noColor": false
|
|
42
|
+
},
|
|
43
|
+
"state": {
|
|
44
|
+
"isStdOutTTY": true,
|
|
45
|
+
"isStdErrTTY": true,
|
|
46
|
+
"testRunDurationMs": 300227.96
|
|
47
|
+
},
|
|
48
|
+
"metrics": {
|
|
49
|
+
"request_duration": {
|
|
50
|
+
"type": "trend",
|
|
51
|
+
"contains": "default",
|
|
52
|
+
"values": {
|
|
53
|
+
"p(95)": 318,
|
|
54
|
+
"avg": 207.51141666666666,
|
|
55
|
+
"min": 98,
|
|
56
|
+
"med": 195,
|
|
57
|
+
"max": 1305,
|
|
58
|
+
"p(90)": 285
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"http_req_sending": {
|
|
62
|
+
"values": {
|
|
63
|
+
"p(90)": 0.443,
|
|
64
|
+
"p(95)": 0.575,
|
|
65
|
+
"avg": 0.3505282157261422,
|
|
66
|
+
"min": 0.029,
|
|
67
|
+
"med": 0.265,
|
|
68
|
+
"max": 197.681
|
|
69
|
+
},
|
|
70
|
+
"type": "trend",
|
|
71
|
+
"contains": "time"
|
|
72
|
+
},
|
|
73
|
+
"http_req_failed": {
|
|
74
|
+
"type": "rate",
|
|
75
|
+
"contains": "default",
|
|
76
|
+
"values": {
|
|
77
|
+
"rate": 0,
|
|
78
|
+
"passes": 0,
|
|
79
|
+
"fails": 30001
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"checks": {
|
|
83
|
+
"type": "rate",
|
|
84
|
+
"contains": "default",
|
|
85
|
+
"values": {
|
|
86
|
+
"rate": 1,
|
|
87
|
+
"passes": 72000,
|
|
88
|
+
"fails": 0
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"successes": {
|
|
92
|
+
"type": "rate",
|
|
93
|
+
"contains": "default",
|
|
94
|
+
"values": {
|
|
95
|
+
"rate": 1,
|
|
96
|
+
"passes": 24000,
|
|
97
|
+
"fails": 0
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"http_req_blocked": {
|
|
101
|
+
"contains": "time",
|
|
102
|
+
"values": {
|
|
103
|
+
"med": 0.001,
|
|
104
|
+
"max": 446.733,
|
|
105
|
+
"p(90)": 0.001,
|
|
106
|
+
"p(95)": 0.002,
|
|
107
|
+
"avg": 1.6850438318703274,
|
|
108
|
+
"min": 0
|
|
109
|
+
},
|
|
110
|
+
"type": "trend"
|
|
111
|
+
},
|
|
112
|
+
"http_req_receiving": {
|
|
113
|
+
"type": "trend",
|
|
114
|
+
"contains": "time",
|
|
115
|
+
"values": {
|
|
116
|
+
"avg": 2.110890670310989,
|
|
117
|
+
"min": 0.019,
|
|
118
|
+
"med": 0.203,
|
|
119
|
+
"max": 206.634,
|
|
120
|
+
"p(90)": 3.99,
|
|
121
|
+
"p(95)": 5.81
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"http_req_duration": {
|
|
125
|
+
"values": {
|
|
126
|
+
"avg": 204.29118289390323,
|
|
127
|
+
"min": 97.82,
|
|
128
|
+
"med": 193.159,
|
|
129
|
+
"max": 990.433,
|
|
130
|
+
"p(90)": 283.411,
|
|
131
|
+
"p(95)": 316.351
|
|
132
|
+
},
|
|
133
|
+
"type": "trend",
|
|
134
|
+
"contains": "time"
|
|
135
|
+
},
|
|
136
|
+
"vus_max": {
|
|
137
|
+
"type": "gauge",
|
|
138
|
+
"contains": "default",
|
|
139
|
+
"values": {
|
|
140
|
+
"value": 200,
|
|
141
|
+
"min": 200,
|
|
142
|
+
"max": 200
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"errors{phase:measurement}": {
|
|
146
|
+
"values": {
|
|
147
|
+
"rate": 0,
|
|
148
|
+
"passes": 0,
|
|
149
|
+
"fails": 24000
|
|
150
|
+
},
|
|
151
|
+
"thresholds": {
|
|
152
|
+
"rate<0.05": {
|
|
153
|
+
"ok": true
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"type": "rate",
|
|
157
|
+
"contains": "default"
|
|
158
|
+
},
|
|
159
|
+
"http_reqs": {
|
|
160
|
+
"contains": "default",
|
|
161
|
+
"values": {
|
|
162
|
+
"count": 30001,
|
|
163
|
+
"rate": 99.9274018315949
|
|
164
|
+
},
|
|
165
|
+
"type": "counter"
|
|
166
|
+
},
|
|
167
|
+
"data_received": {
|
|
168
|
+
"type": "counter",
|
|
169
|
+
"contains": "data",
|
|
170
|
+
"values": {
|
|
171
|
+
"count": 409778192,
|
|
172
|
+
"rate": 1364890.172121211
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"iterations": {
|
|
176
|
+
"type": "counter",
|
|
177
|
+
"contains": "default",
|
|
178
|
+
"values": {
|
|
179
|
+
"count": 30001,
|
|
180
|
+
"rate": 99.9274018315949
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"data_sent": {
|
|
184
|
+
"type": "counter",
|
|
185
|
+
"contains": "data",
|
|
186
|
+
"values": {
|
|
187
|
+
"count": 104185619,
|
|
188
|
+
"rate": 347021.7064393336
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
"http_req_tls_handshaking": {
|
|
192
|
+
"type": "trend",
|
|
193
|
+
"contains": "time",
|
|
194
|
+
"values": {
|
|
195
|
+
"avg": 0.9106618112729572,
|
|
196
|
+
"min": 0,
|
|
197
|
+
"med": 0,
|
|
198
|
+
"max": 306.889,
|
|
199
|
+
"p(90)": 0,
|
|
200
|
+
"p(95)": 0
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"http_req_waiting": {
|
|
204
|
+
"contains": "time",
|
|
205
|
+
"values": {
|
|
206
|
+
"p(90)": 280.835,
|
|
207
|
+
"p(95)": 313.283,
|
|
208
|
+
"avg": 201.82976400786623,
|
|
209
|
+
"min": 94.379,
|
|
210
|
+
"med": 190.745,
|
|
211
|
+
"max": 933.167
|
|
212
|
+
},
|
|
213
|
+
"type": "trend"
|
|
214
|
+
},
|
|
215
|
+
"iteration_duration": {
|
|
216
|
+
"type": "trend",
|
|
217
|
+
"contains": "time",
|
|
218
|
+
"values": {
|
|
219
|
+
"p(95)": 324.982625,
|
|
220
|
+
"avg": 209.38244802586436,
|
|
221
|
+
"min": 100.226958,
|
|
222
|
+
"med": 196.689375,
|
|
223
|
+
"max": 1306.521125,
|
|
224
|
+
"p(90)": 288.073125
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
"errors": {
|
|
228
|
+
"type": "rate",
|
|
229
|
+
"contains": "default",
|
|
230
|
+
"values": {
|
|
231
|
+
"rate": 0,
|
|
232
|
+
"passes": 0,
|
|
233
|
+
"fails": 24000
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
"http_req_duration{phase:measurement}": {
|
|
237
|
+
"type": "trend",
|
|
238
|
+
"contains": "time",
|
|
239
|
+
"values": {
|
|
240
|
+
"p(95)": 316.0782,
|
|
241
|
+
"avg": 205.77155337500048,
|
|
242
|
+
"min": 97.82,
|
|
243
|
+
"med": 194.721,
|
|
244
|
+
"max": 990.433,
|
|
245
|
+
"p(90)": 283.5602
|
|
246
|
+
},
|
|
247
|
+
"thresholds": {
|
|
248
|
+
"p(95)<2000": {
|
|
249
|
+
"ok": true
|
|
250
|
+
},
|
|
251
|
+
"p(99)<5000": {
|
|
252
|
+
"ok": true
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
"http_req_connecting": {
|
|
257
|
+
"type": "trend",
|
|
258
|
+
"contains": "time",
|
|
259
|
+
"values": {
|
|
260
|
+
"avg": 0.7645731142295258,
|
|
261
|
+
"min": 0,
|
|
262
|
+
"med": 0,
|
|
263
|
+
"max": 257.371,
|
|
264
|
+
"p(90)": 0,
|
|
265
|
+
"p(95)": 0
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
"http_req_duration{expected_response:true}": {
|
|
269
|
+
"contains": "time",
|
|
270
|
+
"values": {
|
|
271
|
+
"avg": 204.29118289390323,
|
|
272
|
+
"min": 97.82,
|
|
273
|
+
"med": 193.159,
|
|
274
|
+
"max": 990.433,
|
|
275
|
+
"p(90)": 283.411,
|
|
276
|
+
"p(95)": 316.351
|
|
277
|
+
},
|
|
278
|
+
"type": "trend"
|
|
279
|
+
},
|
|
280
|
+
"vus": {
|
|
281
|
+
"type": "gauge",
|
|
282
|
+
"contains": "default",
|
|
283
|
+
"values": {
|
|
284
|
+
"max": 71,
|
|
285
|
+
"value": 21,
|
|
286
|
+
"min": 13
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
"total_payloads": {
|
|
290
|
+
"contains": "default",
|
|
291
|
+
"values": {
|
|
292
|
+
"count": 1200000,
|
|
293
|
+
"rate": 3996.9628411690906
|
|
294
|
+
},
|
|
295
|
+
"type": "counter"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throughput (Solutions Per Second) Benchmark Test
|
|
3
|
+
*
|
|
4
|
+
* Measures rule engine capacity by sending bulk payload requests at a constant rate.
|
|
5
|
+
* This test helps you understand:
|
|
6
|
+
* - How many rule evaluations your deployment can process per second
|
|
7
|
+
* - Maximum throughput for batch workloads
|
|
8
|
+
* - Engine performance under sustained load
|
|
9
|
+
*
|
|
10
|
+
* Test Structure:
|
|
11
|
+
* - 1 minute warm-up phase (allows cluster to scale, excluded from results)
|
|
12
|
+
* - 4 minutes measurement phase (steady-state performance)
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* k6 run -e API_URL=https://your-instance.com/api/v1/flows/flow_id \
|
|
16
|
+
* -e API_KEY=your-api-key \
|
|
17
|
+
* throughput-test.js
|
|
18
|
+
*
|
|
19
|
+
* Optional environment variables:
|
|
20
|
+
* TEST_DURATION - Measurement duration after warm-up (default: 4m)
|
|
21
|
+
* TARGET_RPS - Target bulk requests per second (default: 100)
|
|
22
|
+
* BULK_SIZE - Number of payloads per request (default: 50)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { check } from "k6";
|
|
26
|
+
import http from "k6/http";
|
|
27
|
+
import { Counter, Rate, Trend } from "k6/metrics";
|
|
28
|
+
import {
|
|
29
|
+
generateBulkPayload,
|
|
30
|
+
getConfig,
|
|
31
|
+
createRequestParams,
|
|
32
|
+
} from "./lib/payload.js";
|
|
33
|
+
import {
|
|
34
|
+
generateThroughputReport,
|
|
35
|
+
generateThroughputConsoleSummary,
|
|
36
|
+
} from "./lib/report.js";
|
|
37
|
+
|
|
38
|
+
// Load configuration
|
|
39
|
+
const config = getConfig({
|
|
40
|
+
testDuration: "4m",
|
|
41
|
+
targetRps: 100,
|
|
42
|
+
bulkSize: 50,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Custom metrics (only for measurement phase)
|
|
46
|
+
const errorRate = new Rate("errors");
|
|
47
|
+
const successRate = new Rate("successes");
|
|
48
|
+
const requestDuration = new Trend("request_duration");
|
|
49
|
+
const droppedRequests = new Counter("dropped_requests");
|
|
50
|
+
const totalPayloads = new Counter("total_payloads");
|
|
51
|
+
const failedPayloads = new Counter("failed_payloads");
|
|
52
|
+
|
|
53
|
+
// k6 options with warm-up and measurement phases
|
|
54
|
+
export const options = {
|
|
55
|
+
scenarios: {
|
|
56
|
+
// Warm-up phase: 1 minute to let cluster scale
|
|
57
|
+
warm_up: {
|
|
58
|
+
executor: "constant-arrival-rate",
|
|
59
|
+
rate: config.targetRps,
|
|
60
|
+
timeUnit: "1s",
|
|
61
|
+
duration: "1m",
|
|
62
|
+
preAllocatedVUs: Math.min(config.targetRps, 100),
|
|
63
|
+
maxVUs: Math.min(config.targetRps * 3, 500),
|
|
64
|
+
exec: "warmUp",
|
|
65
|
+
tags: { phase: "warmup" },
|
|
66
|
+
},
|
|
67
|
+
// Measurement phase: actual test after warm-up
|
|
68
|
+
throughput_test: {
|
|
69
|
+
executor: "constant-arrival-rate",
|
|
70
|
+
rate: config.targetRps,
|
|
71
|
+
timeUnit: "1s",
|
|
72
|
+
duration: config.testDuration,
|
|
73
|
+
preAllocatedVUs: Math.min(config.targetRps, 100),
|
|
74
|
+
maxVUs: Math.min(config.targetRps * 3, 500),
|
|
75
|
+
startTime: "1m", // Start after warm-up
|
|
76
|
+
exec: "measureTest",
|
|
77
|
+
tags: { phase: "measurement" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
thresholds: {
|
|
81
|
+
// Only apply thresholds to measurement phase
|
|
82
|
+
"http_req_duration{phase:measurement}": ["p(95)<2000", "p(99)<5000"],
|
|
83
|
+
"errors{phase:measurement}": ["rate<0.05"],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Request parameters (longer timeout for bulk requests)
|
|
88
|
+
const params = createRequestParams(config.apiKey, "30s");
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Warm-up function - same as test but metrics tagged differently
|
|
92
|
+
*/
|
|
93
|
+
export function warmUp() {
|
|
94
|
+
const bulkPayload = generateBulkPayload(config.bulkSize);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
http.post(config.apiUrl, JSON.stringify(bulkPayload), params);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// Ignore errors during warm-up
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Measurement test function - sends bulk requests
|
|
105
|
+
*/
|
|
106
|
+
export function measureTest() {
|
|
107
|
+
const bulkPayload = generateBulkPayload(config.bulkSize);
|
|
108
|
+
const start = Date.now();
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const response = http.post(
|
|
112
|
+
config.apiUrl,
|
|
113
|
+
JSON.stringify(bulkPayload),
|
|
114
|
+
params
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const duration = Date.now() - start;
|
|
118
|
+
requestDuration.add(duration);
|
|
119
|
+
|
|
120
|
+
const success = check(response, {
|
|
121
|
+
"status is 200": (r) => r.status === 200,
|
|
122
|
+
"valid response": (r) => r.body && r.body.length > 0,
|
|
123
|
+
"no error in response": (r) => {
|
|
124
|
+
try {
|
|
125
|
+
const body = JSON.parse(r.body);
|
|
126
|
+
return !body.error;
|
|
127
|
+
} catch (e) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
errorRate.add(!success);
|
|
134
|
+
successRate.add(success);
|
|
135
|
+
totalPayloads.add(config.bulkSize);
|
|
136
|
+
|
|
137
|
+
if (!success) {
|
|
138
|
+
droppedRequests.add(1);
|
|
139
|
+
failedPayloads.add(config.bulkSize);
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
errorRate.add(1);
|
|
143
|
+
successRate.add(0);
|
|
144
|
+
droppedRequests.add(1);
|
|
145
|
+
totalPayloads.add(config.bulkSize);
|
|
146
|
+
failedPayloads.add(config.bulkSize);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate summary report
|
|
152
|
+
*/
|
|
153
|
+
export function handleSummary(data) {
|
|
154
|
+
return {
|
|
155
|
+
stdout: generateThroughputConsoleSummary(data, config),
|
|
156
|
+
"throughput-report.html": generateThroughputReport(data, config),
|
|
157
|
+
"throughput-results.json": JSON.stringify(data, null, 2),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive wizard for configuring and running k6 load tests
|
|
5
|
+
* against Rulebricks deployments.
|
|
6
|
+
*/
|
|
7
|
+
interface BenchmarkCommandProps {
|
|
8
|
+
name?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function BenchmarkCommand(props: BenchmarkCommandProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Benchmark Command
|
|
4
|
+
*
|
|
5
|
+
* Interactive wizard for configuring and running k6 load tests
|
|
6
|
+
* against Rulebricks deployments.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useEffect, useCallback } from "react";
|
|
9
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
10
|
+
import { BorderBox, Spinner, ThemeProvider, useTheme, Logo, } from "../components/common/index.js";
|
|
11
|
+
import { DeploymentSelectStep, ApiKeyStep, FlowSlugStep, TestModeStep, PresetsStep, ReviewStep, createInitialBenchmarkState, } from "../components/Wizard/steps/BenchmarkSteps.js";
|
|
12
|
+
import { isK6Installed, getK6InstallInstructions, runBenchmark, buildApiUrl, openInBrowser, formatDuration, } from "../lib/benchmark.js";
|
|
13
|
+
function BenchmarkCommandInner({ name }) {
|
|
14
|
+
const { exit } = useApp();
|
|
15
|
+
const { colors } = useTheme();
|
|
16
|
+
const [step, setStep] = useState("preflight");
|
|
17
|
+
const [wizardState, setWizardState] = useState(createInitialBenchmarkState());
|
|
18
|
+
const [error, setError] = useState(null);
|
|
19
|
+
const [k6Output, setK6Output] = useState([]);
|
|
20
|
+
const [result, setResult] = useState(null);
|
|
21
|
+
// Preflight check for k6
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (step !== "preflight")
|
|
24
|
+
return;
|
|
25
|
+
(async () => {
|
|
26
|
+
const installed = await isK6Installed();
|
|
27
|
+
if (!installed) {
|
|
28
|
+
setError(`k6 is not installed.\n\n${getK6InstallInstructions()}\n\nVisit https://k6.io/docs/get-started/installation/ for more options.`);
|
|
29
|
+
setStep("error");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// If a deployment name was provided via CLI, skip the selection step
|
|
33
|
+
if (name) {
|
|
34
|
+
setWizardState((s) => ({ ...s, deploymentName: name }));
|
|
35
|
+
// We still need to load the deployment URL, which the DeploymentSelectStep does
|
|
36
|
+
// So we'll go to that step anyway to validate and load the URL
|
|
37
|
+
}
|
|
38
|
+
setStep("select-deployment");
|
|
39
|
+
})();
|
|
40
|
+
}, [step, name]);
|
|
41
|
+
// Handle wizard step completion
|
|
42
|
+
const handleStepComplete = useCallback((data) => {
|
|
43
|
+
setWizardState((s) => ({ ...s, ...data }));
|
|
44
|
+
// Progress to next step
|
|
45
|
+
switch (step) {
|
|
46
|
+
case "select-deployment":
|
|
47
|
+
setStep("api-key");
|
|
48
|
+
break;
|
|
49
|
+
case "api-key":
|
|
50
|
+
setStep("flow-slug");
|
|
51
|
+
break;
|
|
52
|
+
case "flow-slug":
|
|
53
|
+
setStep("test-mode");
|
|
54
|
+
break;
|
|
55
|
+
case "test-mode":
|
|
56
|
+
setStep("presets");
|
|
57
|
+
break;
|
|
58
|
+
case "presets":
|
|
59
|
+
setStep("review");
|
|
60
|
+
break;
|
|
61
|
+
case "review":
|
|
62
|
+
setStep("running");
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}, [step]);
|
|
66
|
+
// Handle going back
|
|
67
|
+
const handleBack = useCallback(() => {
|
|
68
|
+
switch (step) {
|
|
69
|
+
case "select-deployment":
|
|
70
|
+
exit();
|
|
71
|
+
break;
|
|
72
|
+
case "api-key":
|
|
73
|
+
setStep("select-deployment");
|
|
74
|
+
break;
|
|
75
|
+
case "flow-slug":
|
|
76
|
+
setStep("api-key");
|
|
77
|
+
break;
|
|
78
|
+
case "test-mode":
|
|
79
|
+
setStep("flow-slug");
|
|
80
|
+
break;
|
|
81
|
+
case "presets":
|
|
82
|
+
setStep("test-mode");
|
|
83
|
+
break;
|
|
84
|
+
case "review":
|
|
85
|
+
setStep("presets");
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}, [step, exit]);
|
|
89
|
+
// Run the benchmark
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (step !== "running")
|
|
92
|
+
return;
|
|
93
|
+
(async () => {
|
|
94
|
+
const config = {
|
|
95
|
+
deploymentName: wizardState.deploymentName,
|
|
96
|
+
apiUrl: buildApiUrl(wizardState.deploymentUrl, wizardState.flowSlug),
|
|
97
|
+
apiKey: wizardState.apiKey,
|
|
98
|
+
testMode: wizardState.testMode,
|
|
99
|
+
testDuration: wizardState.testDuration,
|
|
100
|
+
targetRps: wizardState.targetRps,
|
|
101
|
+
bulkSize: wizardState.testMode === "throughput"
|
|
102
|
+
? wizardState.bulkSize
|
|
103
|
+
: undefined,
|
|
104
|
+
};
|
|
105
|
+
const benchmarkResult = await runBenchmark(config, {
|
|
106
|
+
onOutput: (line) => {
|
|
107
|
+
setK6Output((prev) => {
|
|
108
|
+
// Keep only last 15 lines to avoid memory issues
|
|
109
|
+
const newOutput = [...prev, line];
|
|
110
|
+
if (newOutput.length > 15) {
|
|
111
|
+
return newOutput.slice(-15);
|
|
112
|
+
}
|
|
113
|
+
return newOutput;
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
setResult(benchmarkResult);
|
|
118
|
+
if (benchmarkResult.success) {
|
|
119
|
+
setStep("complete");
|
|
120
|
+
// Try to open report in browser
|
|
121
|
+
try {
|
|
122
|
+
await openInBrowser(benchmarkResult.reportPath);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Ignore browser open errors
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
setError(benchmarkResult.error || "Benchmark failed");
|
|
130
|
+
setStep("error");
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
}, [step, wizardState]);
|
|
134
|
+
// Handle key input for error/complete screens
|
|
135
|
+
useInput((input, key) => {
|
|
136
|
+
if (key.escape && (step === "error" || step === "complete")) {
|
|
137
|
+
exit();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Render preflight check
|
|
141
|
+
if (step === "preflight") {
|
|
142
|
+
return (_jsx(BorderBox, { title: "Benchmark", children: _jsx(Box, { marginY: 1, children: _jsx(Spinner, { label: "Checking prerequisites..." }) }) }));
|
|
143
|
+
}
|
|
144
|
+
// Render error screen
|
|
145
|
+
if (step === "error") {
|
|
146
|
+
return (_jsx(BorderBox, { title: "Benchmark Failed", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "Error" }), _jsx(Text, { color: colors.error, children: error }), result?.outputDir && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, dimColor: true, children: ["Output directory: ", result.outputDir] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
|
|
147
|
+
}
|
|
148
|
+
// Render running screen
|
|
149
|
+
if (step === "running") {
|
|
150
|
+
const expectedThroughput = wizardState.testMode === "throughput"
|
|
151
|
+
? wizardState.targetRps * wizardState.bulkSize
|
|
152
|
+
: wizardState.targetRps;
|
|
153
|
+
return (_jsx(BorderBox, { title: "Running Benchmark", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Spinner, { label: `Running ${wizardState.testMode.toUpperCase()} test against ${wizardState.deploymentName}...` }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.muted, children: ["Target: ", expectedThroughput.toLocaleString(), " ", wizardState.testMode === "throughput" ? "solutions" : "requests", "/sec"] }), _jsxs(Text, { color: colors.muted, children: ["Duration: 1m warm-up + ", formatDuration(wizardState.testDuration)] })] }), k6Output.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: k6Output.map((line, i) => (_jsx(Text, { color: colors.muted, dimColor: true, children: line.length > 80 ? line.slice(0, 77) + "..." : line }, i))) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.warning, dimColor: true, children: "This may take several minutes. Please wait..." }) })] }) }));
|
|
154
|
+
}
|
|
155
|
+
// Render complete screen
|
|
156
|
+
if (step === "complete" && result) {
|
|
157
|
+
const metrics = result.metrics;
|
|
158
|
+
return (_jsx(BorderBox, { title: "Benchmark Complete", children: _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "Benchmark completed successfully!" }), metrics && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Results Summary:" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Success Rate: " }), _jsxs(Text, { color: metrics.successRate >= 99
|
|
159
|
+
? colors.success
|
|
160
|
+
: metrics.successRate >= 95
|
|
161
|
+
? colors.warning
|
|
162
|
+
: colors.error, bold: true, children: [metrics.successRate.toFixed(1), "%"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Actual RPS: " }), _jsx(Text, { color: colors.accent, bold: true, children: metrics.actualRps.toFixed(1) })] }), metrics.actualThroughput && (_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Throughput: " }), _jsxs(Text, { color: colors.accent, bold: true, children: [metrics.actualThroughput.toFixed(0), " solutions/sec"] })] })), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "P95 Latency: " }), _jsxs(Text, { color: metrics.p95Latency < 200
|
|
163
|
+
? colors.success
|
|
164
|
+
: metrics.p95Latency < 500
|
|
165
|
+
? colors.warning
|
|
166
|
+
: colors.error, children: [metrics.p95Latency.toFixed(0), "ms"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "P99 Latency: " }), _jsxs(Text, { children: [metrics.p99Latency.toFixed(0), "ms"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Total Requests: " }), _jsx(Text, { children: metrics.totalRequests.toLocaleString() })] })] })] })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: colors.muted, children: "Results saved to:" }), _jsx(Text, { color: colors.accent, children: result.outputDir })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.muted, children: ["Report: ", result.reportPath.split("/").pop()] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, children: "The HTML report should open in your browser automatically." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, dimColor: true, children: "Press Esc to exit" }) })] }) }));
|
|
167
|
+
}
|
|
168
|
+
// Render wizard steps
|
|
169
|
+
return (_jsxs(_Fragment, { children: [step === "select-deployment" && (_jsx(DeploymentSelectStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "api-key" && (_jsx(ApiKeyStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "flow-slug" && (_jsx(FlowSlugStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "test-mode" && (_jsx(TestModeStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "presets" && (_jsx(PresetsStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState })), step === "review" && (_jsx(ReviewStep, { onComplete: handleStepComplete, onBack: handleBack, state: wizardState }))] }));
|
|
170
|
+
}
|
|
171
|
+
export function BenchmarkCommand(props) {
|
|
172
|
+
return (_jsxs(ThemeProvider, { theme: "status", children: [_jsx(Logo, {}), _jsx(BenchmarkCommandInner, { ...props })] }));
|
|
173
|
+
}
|