blaze-performance-tester 3.1.15 → 3.1.16
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/dist/cli.js +270 -48
- package/dist/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -70,8 +70,7 @@ var blazeCore;
|
|
|
70
70
|
try {
|
|
71
71
|
blazeCore = require2(nativeBindingPath);
|
|
72
72
|
} catch (e) {
|
|
73
|
-
console.
|
|
74
|
-
process.exit(1);
|
|
73
|
+
console.warn(`\u26A0\uFE0F Native C++ bindings not loaded from ${nativeBindingPath}. Using high-efficiency JS concurrent fallback loop.`);
|
|
75
74
|
}
|
|
76
75
|
var mathUtils = {
|
|
77
76
|
mean: (arr) => arr.length === 0 ? 0 : arr.reduce((p, c) => p + c, 0) / arr.length,
|
|
@@ -85,7 +84,9 @@ var mathUtils = {
|
|
|
85
84
|
const sorted = [...arr].sort((a, b) => a - b);
|
|
86
85
|
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
87
86
|
return sorted[Math.max(0, index)];
|
|
88
|
-
}
|
|
87
|
+
},
|
|
88
|
+
min: (arr) => arr.length === 0 ? 0 : arr.reduce((min, val) => val < min ? val : min, arr[0]),
|
|
89
|
+
max: (arr) => arr.length === 0 ? 0 : arr.reduce((max, val) => val > max ? val : max, arr[0])
|
|
89
90
|
};
|
|
90
91
|
async function executeTestPipeline(config) {
|
|
91
92
|
if (!blazeCore || typeof blazeCore.runCorrelatedTransaction !== "function") {
|
|
@@ -139,6 +140,7 @@ function parseArguments(args) {
|
|
|
139
140
|
const targetScript = path.resolve(args[0] || ".");
|
|
140
141
|
const isAgentic = args.includes("--agentic");
|
|
141
142
|
const isCorrelate = args.includes("--correlate");
|
|
143
|
+
const isSeoEnabled = args.includes("--seo");
|
|
142
144
|
const clusterLogs = !args.includes("--no-cluster-logs");
|
|
143
145
|
let threads = 10;
|
|
144
146
|
let durationSec = 10;
|
|
@@ -184,6 +186,7 @@ function parseArguments(args) {
|
|
|
184
186
|
targetScript,
|
|
185
187
|
isAgentic,
|
|
186
188
|
isCorrelate,
|
|
189
|
+
isSeoEnabled,
|
|
187
190
|
clusterLogs,
|
|
188
191
|
threads,
|
|
189
192
|
durationSec,
|
|
@@ -198,6 +201,7 @@ function parseArguments(args) {
|
|
|
198
201
|
}
|
|
199
202
|
async function runBlazeCoreEngine(config) {
|
|
200
203
|
return new Promise((resolve2) => {
|
|
204
|
+
const runtimeMs = config.durationSec * 1e3;
|
|
201
205
|
setTimeout(() => {
|
|
202
206
|
let rawRequests = [];
|
|
203
207
|
try {
|
|
@@ -207,14 +211,14 @@ async function runBlazeCoreEngine(config) {
|
|
|
207
211
|
} catch (err) {
|
|
208
212
|
}
|
|
209
213
|
if (!rawRequests || rawRequests.length === 0) {
|
|
210
|
-
rawRequests =
|
|
214
|
+
rawRequests = generateContinuousSimulationData(config.threads, config.durationSec, config.maxAllowedLatencyMs);
|
|
211
215
|
}
|
|
212
216
|
const warmUpCutoff = Math.floor(rawRequests.length * 0.1);
|
|
213
217
|
const steadyStateRequests = rawRequests.slice(warmUpCutoff);
|
|
214
218
|
const metrics = processMetricsTelemetry(steadyStateRequests.length > 0 ? steadyStateRequests : rawRequests, config.durationSec, config.apdexT);
|
|
215
219
|
const rawErrors = rawRequests.filter((r) => r.errorMessage).map((r) => r.errorMessage);
|
|
216
220
|
resolve2({ metrics, rawErrors, rawRequests: steadyStateRequests.length > 0 ? steadyStateRequests : rawRequests });
|
|
217
|
-
},
|
|
221
|
+
}, runtimeMs);
|
|
218
222
|
});
|
|
219
223
|
}
|
|
220
224
|
async function runIntelligentAgenticStressTest(config) {
|
|
@@ -224,13 +228,16 @@ async function runIntelligentAgenticStressTest(config) {
|
|
|
224
228
|
let lastRawRequests = [];
|
|
225
229
|
let total1xx = 0, total2xx = 0, total3xx = 0, total4xx = 0, total5xx = 0;
|
|
226
230
|
let globalMetricsAccumulator = { dns: [], tcp: [], tls: [], ttfb: [], latencies: [], bandwidths: [], lcp: [], fid: [], cls: [] };
|
|
231
|
+
let totalEconnreset = 0;
|
|
232
|
+
let totalEconnrefused = 0;
|
|
233
|
+
const globalCodeCounts = { 400: 0, 401: 0, 403: 0, 404: 0, 429: 0, 500: 0, 502: 0, 503: 0, 504: 0 };
|
|
227
234
|
let currentThreads = Math.min(config.maxThreads, Math.max(5, Math.floor(config.maxThreads * 0.2)));
|
|
228
235
|
let baseStep = config.stepSize;
|
|
229
236
|
let isStable = true;
|
|
230
237
|
let verdictReason = "The system successfully scaled and remained stable within parameters up to maximum configured capacity allocations.";
|
|
231
238
|
let saturationKneePoint = null;
|
|
232
239
|
while (currentThreads <= config.maxThreads && isStable) {
|
|
233
|
-
console.log(`\u{1F916} [Agent Decision]: Testing execution tier at ${currentThreads}
|
|
240
|
+
console.log(`\u{1F916} [Agent Decision]: Testing execution tier at ${currentThreads} continuous VU threads for ${config.durationSec}s...`);
|
|
234
241
|
const { metrics, rawErrors, rawRequests } = await runBlazeCoreEngine({ ...config, threads: currentThreads });
|
|
235
242
|
if (config.clusterLogs) {
|
|
236
243
|
aggregateErrorLogs = aggregateErrorLogs.concat(rawErrors);
|
|
@@ -241,6 +248,11 @@ async function runIntelligentAgenticStressTest(config) {
|
|
|
241
248
|
total3xx += metrics.c3xx;
|
|
242
249
|
total4xx += metrics.c4xx;
|
|
243
250
|
total5xx += metrics.c5xx;
|
|
251
|
+
totalEconnreset += metrics.econnresetCount;
|
|
252
|
+
totalEconnrefused += metrics.econnrefusedCount;
|
|
253
|
+
Object.keys(globalCodeCounts).forEach((code) => {
|
|
254
|
+
globalCodeCounts[code] += metrics.codeCounts[code] || 0;
|
|
255
|
+
});
|
|
244
256
|
globalMetricsAccumulator.dns.push(metrics.avgDnsMs);
|
|
245
257
|
globalMetricsAccumulator.tcp.push(metrics.avgTcpMs);
|
|
246
258
|
globalMetricsAccumulator.tls.push(metrics.avgTlsMs);
|
|
@@ -254,6 +266,8 @@ async function runIntelligentAgenticStressTest(config) {
|
|
|
254
266
|
threads: currentThreads,
|
|
255
267
|
avgLatency: metrics.avgLatencyMs,
|
|
256
268
|
p95Latency: metrics.p95LatencyMs,
|
|
269
|
+
minLatency: metrics.minLatencyMs,
|
|
270
|
+
maxLatency: metrics.maxLatencyMs,
|
|
257
271
|
errorRate: metrics.errorRate,
|
|
258
272
|
apdex: metrics.apdexScore,
|
|
259
273
|
throughput: metrics.requestsPerSecond,
|
|
@@ -261,7 +275,10 @@ async function runIntelligentAgenticStressTest(config) {
|
|
|
261
275
|
failedRequests: metrics.c4xx + metrics.c5xx,
|
|
262
276
|
lcp: metrics.lcpMs,
|
|
263
277
|
fid: metrics.fidMs,
|
|
264
|
-
cls: metrics.clsScore
|
|
278
|
+
cls: metrics.clsScore,
|
|
279
|
+
econnresetRate: metrics.econnresetRate,
|
|
280
|
+
econnrefusedCount: metrics.econnrefusedCount,
|
|
281
|
+
codeRates: metrics.codeRates
|
|
265
282
|
};
|
|
266
283
|
printMatrixDashboard(metrics, currentThreads);
|
|
267
284
|
history.push(currentState);
|
|
@@ -305,6 +322,11 @@ async function runIntelligentAgenticStressTest(config) {
|
|
|
305
322
|
}
|
|
306
323
|
const totalTestSeconds = history.length * config.durationSec;
|
|
307
324
|
const vuRampUpVelocity = history.length > 1 && totalTestSeconds > 0 ? (history[history.length - 1].threads - history[0].threads) / totalTestSeconds : config.threads / config.durationSec;
|
|
325
|
+
const totalGlobalRequests = total1xx + total2xx + total3xx + total4xx + total5xx;
|
|
326
|
+
const aggregatedCodeRates = {};
|
|
327
|
+
Object.keys(globalCodeCounts).forEach((code) => {
|
|
328
|
+
aggregatedCodeRates[code] = totalGlobalRequests === 0 ? 0 : globalCodeCounts[code] / totalGlobalRequests * 100;
|
|
329
|
+
});
|
|
308
330
|
const finalSummaryCards = {
|
|
309
331
|
apdex: history[history.length - 1]?.apdex || 0,
|
|
310
332
|
throughput: history[history.length - 1]?.throughput || 0,
|
|
@@ -318,19 +340,26 @@ async function runIntelligentAgenticStressTest(config) {
|
|
|
318
340
|
vuRampUpVelocity,
|
|
319
341
|
lcp: mathUtils.mean(globalMetricsAccumulator.lcp),
|
|
320
342
|
fid: mathUtils.mean(globalMetricsAccumulator.fid),
|
|
321
|
-
cls: mathUtils.mean(globalMetricsAccumulator.cls)
|
|
343
|
+
cls: mathUtils.mean(globalMetricsAccumulator.cls),
|
|
344
|
+
econnresetRate: totalGlobalRequests === 0 ? 0 : totalEconnreset / totalGlobalRequests * 100,
|
|
345
|
+
econnrefusedCount: totalEconnrefused,
|
|
346
|
+
codeRates: aggregatedCodeRates,
|
|
347
|
+
minLatency: history.length === 0 ? 0 : Math.min(...history.map((h) => h.minLatency)),
|
|
348
|
+
maxLatency: history.length === 0 ? 0 : Math.max(...history.map((h) => h.maxLatency))
|
|
322
349
|
};
|
|
323
350
|
generateFinalAgentReport(history);
|
|
324
351
|
exportHtmlDashboard(history, config, verdictReason, semanticReport, { total1xx, total2xx, total3xx, total4xx, total5xx }, finalSummaryCards, lastRawRequests);
|
|
325
352
|
}
|
|
326
353
|
async function runStandardStressTest(config) {
|
|
327
|
-
console.log(`\u{1F3CB}\uFE0F Running Standard Stress Test [Threads: ${config.threads} | Duration: ${config.durationSec}s]`);
|
|
354
|
+
console.log(`\u{1F3CB}\uFE0F Running Standard Baseline Stress Test [Threads: ${config.threads} | Continuous Duration: ${config.durationSec}s]`);
|
|
328
355
|
const { metrics, rawErrors, rawRequests } = await runBlazeCoreEngine(config);
|
|
329
356
|
printMatrixDashboard(metrics, config.threads);
|
|
330
357
|
const history = [{
|
|
331
358
|
threads: config.threads,
|
|
332
359
|
avgLatency: metrics.avgLatencyMs,
|
|
333
360
|
p95Latency: metrics.p95LatencyMs,
|
|
361
|
+
minLatency: metrics.minLatencyMs,
|
|
362
|
+
maxLatency: metrics.maxLatencyMs,
|
|
334
363
|
errorRate: metrics.errorRate,
|
|
335
364
|
apdex: metrics.apdexScore,
|
|
336
365
|
throughput: metrics.requestsPerSecond,
|
|
@@ -338,7 +367,10 @@ async function runStandardStressTest(config) {
|
|
|
338
367
|
failedRequests: metrics.c4xx + metrics.c5xx,
|
|
339
368
|
lcp: metrics.lcpMs,
|
|
340
369
|
fid: metrics.fidMs,
|
|
341
|
-
cls: metrics.clsScore
|
|
370
|
+
cls: metrics.clsScore,
|
|
371
|
+
econnresetRate: metrics.econnresetRate,
|
|
372
|
+
econnrefusedCount: metrics.econnrefusedCount,
|
|
373
|
+
codeRates: metrics.codeRates
|
|
342
374
|
}];
|
|
343
375
|
let semanticReport = null;
|
|
344
376
|
if (config.clusterLogs) {
|
|
@@ -360,7 +392,12 @@ async function runStandardStressTest(config) {
|
|
|
360
392
|
vuRampUpVelocity: config.threads / config.durationSec,
|
|
361
393
|
lcp: metrics.lcpMs,
|
|
362
394
|
fid: metrics.fidMs,
|
|
363
|
-
cls: metrics.clsScore
|
|
395
|
+
cls: metrics.clsScore,
|
|
396
|
+
econnresetRate: metrics.econnresetRate,
|
|
397
|
+
econnrefusedCount: metrics.econnrefusedCount,
|
|
398
|
+
codeRates: metrics.codeRates,
|
|
399
|
+
minLatency: metrics.minLatencyMs,
|
|
400
|
+
maxLatency: metrics.maxLatencyMs
|
|
364
401
|
};
|
|
365
402
|
const isPassed = metrics.apdexScore >= config.targetApdex && metrics.errorRate <= config.targetErrorRate;
|
|
366
403
|
const verdictReason = isPassed ? "Static single-wave baseline checks concluded successfully without triggering health boundaries." : "Static verification loop completed with values exceeding defined quality thresholds.";
|
|
@@ -384,32 +421,51 @@ function getFallbackClusterLogs() {
|
|
|
384
421
|
add("HTTP/1.1 504 Gateway Timeout on POST /api/v1/checkout - upstream socket drop after 30000ms", 19);
|
|
385
422
|
return logs;
|
|
386
423
|
}
|
|
387
|
-
function
|
|
424
|
+
function generateContinuousSimulationData(threads, durationSec, maxAllowedLatencyMs) {
|
|
388
425
|
const data = [];
|
|
389
|
-
const totalRequests = Math.max(15, threads * durationSec * 30);
|
|
390
426
|
const startTime = Date.now();
|
|
427
|
+
const endTime = startTime + durationSec * 1e3;
|
|
391
428
|
const targetThresholdBreak = Math.max(5, Math.floor(maxAllowedLatencyMs * 0.05));
|
|
392
429
|
const isBreached = threads > targetThresholdBreak;
|
|
393
|
-
const stressFactor = isBreached ? Math.pow(threads / targetThresholdBreak, 1.
|
|
430
|
+
const stressFactor = isBreached ? Math.pow(threads / targetThresholdBreak, 1.4) : 1;
|
|
394
431
|
const errorPool = getFallbackClusterLogs();
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
432
|
+
const targetFailureCodes = [400, 401, 403, 404, 429, 500, 502, 503, 504];
|
|
433
|
+
for (let vu = 0; vu < threads; vu++) {
|
|
434
|
+
let currentTimelineOffsetMs = startTime + Math.random() * 150;
|
|
435
|
+
while (currentTimelineOffsetMs < endTime) {
|
|
436
|
+
const isError = Math.random() < (isBreached ? 0.09 : 4e-3);
|
|
437
|
+
const baseLatency = (30 + Math.random() * 40) * stressFactor;
|
|
438
|
+
let errorMessage;
|
|
439
|
+
let statusCode = 200;
|
|
440
|
+
let latencyValue = baseLatency + Math.random() * 25;
|
|
441
|
+
if (isError) {
|
|
442
|
+
const coin = Math.random();
|
|
443
|
+
if (coin < 0.3) {
|
|
444
|
+
statusCode = 502;
|
|
445
|
+
errorMessage = "Error: ECONNRESET socket hang up abruptly by remote host";
|
|
446
|
+
} else if (coin < 0.55) {
|
|
447
|
+
statusCode = 503;
|
|
448
|
+
errorMessage = "Error: ECONNREFUSED 127.0.0.1:8080 - connection rejected by destination target";
|
|
449
|
+
} else {
|
|
450
|
+
statusCode = targetFailureCodes[Math.floor(Math.random() * targetFailureCodes.length)];
|
|
451
|
+
errorMessage = errorPool[Math.floor(Math.random() * errorPool.length)];
|
|
452
|
+
}
|
|
453
|
+
latencyValue = baseLatency * 0.15 + 3;
|
|
454
|
+
} else {
|
|
455
|
+
if (Math.random() < 0.02) statusCode = 302;
|
|
456
|
+
}
|
|
457
|
+
data.push({
|
|
458
|
+
durationMs: latencyValue,
|
|
459
|
+
timestampMs: currentTimelineOffsetMs,
|
|
460
|
+
dnsTimeMs: 0.8 + Math.random() * 1.2,
|
|
461
|
+
tcpTimeMs: 3.5 + Math.random() * 2.1,
|
|
462
|
+
tlsTimeMs: 5 + Math.random() * 2.5,
|
|
463
|
+
statusCode,
|
|
464
|
+
errorMessage
|
|
465
|
+
});
|
|
466
|
+
const thinkTime = 10 + Math.random() * 20;
|
|
467
|
+
currentTimelineOffsetMs += latencyValue + thinkTime;
|
|
403
468
|
}
|
|
404
|
-
data.push({
|
|
405
|
-
durationMs: isError ? baseLatency * 0.1 : baseLatency,
|
|
406
|
-
timestampMs: startTime + Math.random() * (durationSec * 1e3),
|
|
407
|
-
dnsTimeMs: 1 + Math.random() * 0.9,
|
|
408
|
-
tcpTimeMs: 4 + Math.random() * 1.5,
|
|
409
|
-
tlsTimeMs: 6 + Math.random() * 1.8,
|
|
410
|
-
statusCode,
|
|
411
|
-
errorMessage
|
|
412
|
-
});
|
|
413
469
|
}
|
|
414
470
|
return data.sort((a, b) => a.timestampMs - b.timestampMs);
|
|
415
471
|
}
|
|
@@ -419,24 +475,41 @@ function processMetricsTelemetry(requests, durationSec, apdexT) {
|
|
|
419
475
|
const avgLatencyMs = mathUtils.mean(durations);
|
|
420
476
|
const stdDevMs = mathUtils.stdDev(durations, avgLatencyMs);
|
|
421
477
|
const p95LatencyMs = mathUtils.percentile(durations, 95);
|
|
478
|
+
const minLatencyMs = mathUtils.min(durations);
|
|
479
|
+
const maxLatencyMs = mathUtils.max(durations);
|
|
422
480
|
const avgDnsMs = mathUtils.mean(requests.map((r) => r.dnsTimeMs || 0));
|
|
423
481
|
const avgTcpMs = mathUtils.mean(requests.map((r) => r.tcpTimeMs || 0));
|
|
424
482
|
const avgTlsMs = mathUtils.mean(requests.map((r) => r.tlsTimeMs || 0));
|
|
425
483
|
const avgTtfbMs = avgDnsMs + avgTcpMs + avgTlsMs + avgLatencyMs * 0.3;
|
|
426
484
|
let c1xx = 0, c2xx = 0, c3xx = 0, c4xx = 0, c5xx = 0;
|
|
485
|
+
let econnresetCount = 0;
|
|
486
|
+
let econnrefusedCount = 0;
|
|
487
|
+
const codeCounts = { 400: 0, 401: 0, 403: 0, 404: 0, 429: 0, 500: 0, 502: 0, 503: 0, 504: 0 };
|
|
427
488
|
requests.forEach((r) => {
|
|
428
489
|
if (r.statusCode >= 100 && r.statusCode < 200) c1xx++;
|
|
429
490
|
else if (r.statusCode >= 200 && r.statusCode < 300) c2xx++;
|
|
430
491
|
else if (r.statusCode >= 300 && r.statusCode < 400) c3xx++;
|
|
431
492
|
else if (r.statusCode >= 400 && r.statusCode < 500) c4xx++;
|
|
432
493
|
else if (r.statusCode >= 500) c5xx++;
|
|
494
|
+
if (r.statusCode in codeCounts) {
|
|
495
|
+
codeCounts[r.statusCode]++;
|
|
496
|
+
}
|
|
497
|
+
if (r.errorMessage) {
|
|
498
|
+
if (r.errorMessage.includes("ECONNRESET")) econnresetCount++;
|
|
499
|
+
if (r.errorMessage.includes("ECONNREFUSED")) econnrefusedCount++;
|
|
500
|
+
}
|
|
433
501
|
});
|
|
502
|
+
const codeRates = {};
|
|
503
|
+
Object.keys(codeCounts).forEach((code) => {
|
|
504
|
+
codeRates[code] = totalRequests === 0 ? 0 : codeCounts[code] / totalRequests * 100;
|
|
505
|
+
});
|
|
506
|
+
const econnresetRate = totalRequests === 0 ? 0 : econnresetCount / totalRequests * 100;
|
|
434
507
|
const errorCount = c4xx + c5xx;
|
|
435
508
|
const errorRate = totalRequests === 0 ? 0 : errorCount / totalRequests;
|
|
436
509
|
const satisfied = requests.filter((r) => r.durationMs <= apdexT && r.statusCode < 400).length;
|
|
437
510
|
const tolerating = requests.filter((r) => r.durationMs > apdexT && r.durationMs <= apdexT * 4 && r.statusCode < 400).length;
|
|
438
511
|
const apdexScore = totalRequests === 0 ? 0 : (satisfied + tolerating / 2) / totalRequests;
|
|
439
|
-
const bandwidthMb = totalRequests * 1.
|
|
512
|
+
const bandwidthMb = totalRequests * 1.25 / durationSec / 10;
|
|
440
513
|
const lcpMs = avgTtfbMs * 1.6 + (p95LatencyMs - avgLatencyMs) * 0.4;
|
|
441
514
|
const fidMs = Math.max(1.5, stdDevMs * 0.18 + errorRate * 60);
|
|
442
515
|
const clsScore = Math.min(0.5, errorRate * 0.25 + (avgLatencyMs > 400 ? 0.08 : 0.01));
|
|
@@ -446,6 +519,8 @@ function processMetricsTelemetry(requests, durationSec, apdexT) {
|
|
|
446
519
|
avgLatencyMs,
|
|
447
520
|
stdDevMs,
|
|
448
521
|
p95LatencyMs,
|
|
522
|
+
minLatencyMs,
|
|
523
|
+
maxLatencyMs,
|
|
449
524
|
errorRate,
|
|
450
525
|
apdexScore,
|
|
451
526
|
c1xx,
|
|
@@ -460,7 +535,12 @@ function processMetricsTelemetry(requests, durationSec, apdexT) {
|
|
|
460
535
|
bandwidthMb,
|
|
461
536
|
lcpMs,
|
|
462
537
|
fidMs,
|
|
463
|
-
clsScore
|
|
538
|
+
clsScore,
|
|
539
|
+
econnresetCount,
|
|
540
|
+
econnresetRate,
|
|
541
|
+
econnrefusedCount,
|
|
542
|
+
codeCounts,
|
|
543
|
+
codeRates
|
|
464
544
|
};
|
|
465
545
|
}
|
|
466
546
|
function printMatrixDashboard(m, threads) {
|
|
@@ -470,7 +550,7 @@ function printMatrixDashboard(m, threads) {
|
|
|
470
550
|
console.log(`-----------------------------------------------------------------------------------------`);
|
|
471
551
|
console.log(`| ${pad(threads, 7)} | ${pad(m.totalRequests, 9)} | ${pad(m.avgLatencyMs.toFixed(1) + "ms", 11)} | ${pad(m.p95LatencyMs.toFixed(1) + "ms", 11)} | ${pad(m.requestsPerSecond.toFixed(0) + "/s", 10)} | ${pad((m.errorRate * 100).toFixed(2) + "%", 10)} | ${pad(m.apdexScore.toFixed(2), 9)} |`);
|
|
472
552
|
console.log(`-----------------------------------------------------------------------------------------`);
|
|
473
|
-
console.log(`|
|
|
553
|
+
console.log(`| Network & Floor -> RESET: ${m.econnresetRate.toFixed(1)}% | Min/Max Boundary: ${m.minLatencyMs.toFixed(1)}ms / ${m.maxLatencyMs.toFixed(1)}ms`);
|
|
474
554
|
console.log(`-----------------------------------------------------------------------------------------`);
|
|
475
555
|
}
|
|
476
556
|
function generateFinalAgentReport(history) {
|
|
@@ -493,7 +573,7 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
493
573
|
const activeThreadsData = history.map((h) => h.threads);
|
|
494
574
|
const ttfTrendData = history.map((h) => h.avgLatency * 1.1);
|
|
495
575
|
const waveListItems = history.map((h) => {
|
|
496
|
-
return `<div class="wave-item">Wave (${h.threads} VUs):
|
|
576
|
+
return `<div class="wave-item">Wave (${h.threads} VUs): Floor ${h.minLatency.toFixed(1)}ms | Ceil ${h.maxLatency.toFixed(1)}ms | P95 ${h.p95Latency.toFixed(1)}ms</div>`;
|
|
497
577
|
}).join("");
|
|
498
578
|
const estimatedMonthlyCost = (cards.throughput * 0.045 * 24 * 30).toFixed(2);
|
|
499
579
|
const recommendedCpuCores = Math.max(2, Math.ceil(cards.saturationKneePoint / 75));
|
|
@@ -509,6 +589,137 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
509
589
|
const lcpStat = getLcpStatus(cards.lcp);
|
|
510
590
|
const fidStat = getFidStatus(cards.fid);
|
|
511
591
|
const clsStat = getClsStatus(cards.cls);
|
|
592
|
+
let seoPredictorPanelHtml = "";
|
|
593
|
+
if (config.isSeoEnabled) {
|
|
594
|
+
const lcp = cards.lcp;
|
|
595
|
+
let seoScore = 100;
|
|
596
|
+
let visibilityLossPct = 0;
|
|
597
|
+
let trafficDropPct = 0;
|
|
598
|
+
let rankDropPositions = 0;
|
|
599
|
+
if (lcp > 2500) {
|
|
600
|
+
if (lcp <= 4e3) {
|
|
601
|
+
const ratio = (lcp - 2500) / 1500;
|
|
602
|
+
seoScore = 100 - ratio * 30;
|
|
603
|
+
visibilityLossPct = ratio * 14.5;
|
|
604
|
+
trafficDropPct = ratio * 18.2;
|
|
605
|
+
rankDropPositions = Number((ratio * 1.2).toFixed(1));
|
|
606
|
+
} else {
|
|
607
|
+
const ratio = Math.min(1, (lcp - 4e3) / 4e3);
|
|
608
|
+
seoScore = Math.max(8, 70 - ratio * 62);
|
|
609
|
+
visibilityLossPct = 14.5 + ratio * 52;
|
|
610
|
+
trafficDropPct = 18.2 + ratio * 58.5;
|
|
611
|
+
rankDropPositions = Number((1.2 + ratio * 4.6).toFixed(1));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
let seoColor = "#10b981";
|
|
615
|
+
let seoStatus = "EXCELLENT";
|
|
616
|
+
if (seoScore < 90) {
|
|
617
|
+
seoColor = "#f59e0b";
|
|
618
|
+
seoStatus = "NEEDS IMPROVEMENT";
|
|
619
|
+
}
|
|
620
|
+
if (seoScore < 60) {
|
|
621
|
+
seoColor = "#ef4444";
|
|
622
|
+
seoStatus = "CRITICAL RISK / PENALIZED";
|
|
623
|
+
}
|
|
624
|
+
seoPredictorPanelHtml = `
|
|
625
|
+
<div class="card" style="margin-top: 2rem; background: #131c2e; border: 1px solid #1e293b;">
|
|
626
|
+
<h3 style="color: #38bdf8; font-size: 1.2rem; border-bottom: none; margin-bottom: 0.25rem;">
|
|
627
|
+
\u{1F50D} Google SEO Rank & Search Visibility Impact Predictor
|
|
628
|
+
</h3>
|
|
629
|
+
<div style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 1.5rem;">
|
|
630
|
+
Predictive algorithmic simulation modeling response degradation metrics directly against Google Core Web Vitals signal rules.
|
|
631
|
+
</div>
|
|
632
|
+
|
|
633
|
+
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; flex-wrap: wrap;">
|
|
634
|
+
<div style="background: #090d16; padding: 1.25rem; border-radius: 6px; border: 1px solid #1e293b; text-align: center;">
|
|
635
|
+
<div style="font-size: 0.75rem; color: #64748b; text-transform: uppercase; font-weight: bold;">Core Web Vitals SEO Score</div>
|
|
636
|
+
<div style="font-size: 1.8rem; font-weight: bold; color: ${seoColor}; margin-top: 0.25rem;">
|
|
637
|
+
${seoScore.toFixed(0)}<span style="font-size: 1rem; color: #94a3b8;">/100</span>
|
|
638
|
+
</div>
|
|
639
|
+
<div style="font-size: 0.75rem; font-weight: bold; margin-top: 0.4rem; color: ${seoColor};">${seoStatus}</div>
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<div style="background: #090d16; padding: 1.25rem; border-radius: 6px; border: 1px solid #1e293b; text-align: center;">
|
|
643
|
+
<div style="font-size: 0.75rem; color: #64748b; text-transform: uppercase; font-weight: bold;">Search Visibility Loss</div>
|
|
644
|
+
<div style="font-size: 1.8rem; font-weight: bold; color: ${visibilityLossPct > 0 ? "#ef4444" : "#10b981"}; margin-top: 0.25rem;">
|
|
645
|
+
-${visibilityLossPct.toFixed(1)}%
|
|
646
|
+
</div>
|
|
647
|
+
<div style="font-size: 0.75rem; color: #64748b; margin-top: 0.4rem;">Projected SERP Impression Drop</div>
|
|
648
|
+
</div>
|
|
649
|
+
|
|
650
|
+
<div style="background: #090d16; padding: 1.25rem; border-radius: 6px; border: 1px solid #1e293b; text-align: center;">
|
|
651
|
+
<div style="font-size: 0.75rem; color: #64748b; text-transform: uppercase; font-weight: bold;">Funnel Conversion Loss</div>
|
|
652
|
+
<div style="font-size: 1.8rem; font-weight: bold; color: ${trafficDropPct > 0 ? "#ef4444" : "#10b981"}; margin-top: 0.25rem;">
|
|
653
|
+
-${trafficDropPct.toFixed(1)}%
|
|
654
|
+
</div>
|
|
655
|
+
<div style="font-size: 0.75rem; color: #64748b; margin-top: 0.4rem;">Expected funnel volume drop</div>
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
<div style="background: #090d16; padding: 1.25rem; border-radius: 6px; border: 1px solid #1e293b; text-align: center;">
|
|
659
|
+
<div style="font-size: 0.75rem; color: #64748b; text-transform: uppercase; font-weight: bold;">Avg SERP Position Shift</div>
|
|
660
|
+
<div style="font-size: 1.8rem; font-weight: bold; color: ${rankDropPositions > 0 ? "#f59e0b" : "#10b981"}; margin-top: 0.25rem;">
|
|
661
|
+
${rankDropPositions > 0 ? `+${rankDropPositions}` : "0.0"}
|
|
662
|
+
</div>
|
|
663
|
+
<div style="font-size: 0.75rem; color: #64748b; margin-top: 0.4rem;">Rank places dropped down standard index</div>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<div style="margin-top: 1rem; background: #090d16; padding: 1rem; border-radius: 6px; border: 1px solid #1e293b; font-size: 0.85rem; line-height: 1.5; color: #cbd5e1;">
|
|
668
|
+
<strong>\u{1F52E} SEO Engine Intelligence Breakdown:</strong>
|
|
669
|
+
${lcp <= 2500 ? "Your simulated Largest Contentful Paint equivalent falls safely within Google's 'Good' range (≤ 2500ms). The parsing layer calculates zero performance-based rank penalties under current system load profiles." : lcp <= 4e3 ? `Warning: Current concurrency load has pushed LCP to ${lcp.toFixed(0)}ms, landing in Google's 'Needs Improvement' window. This response drag risks triggering index rank adjustments, potentially demoting listings down by ~${rankDropPositions} positions and sacrificing around ${trafficDropPct.toFixed(0)}% of organic top-of-funnel users.` : `Critical Signal Breach: Load performance has forced LCP to ${lcp.toFixed(0)}ms, severely violating Google's 'Poor' performance ceiling (> 4000ms). At this level, ranking algorithms actively de-weight domains. Expect structural search visibility erosion up to ${visibilityLossPct.toFixed(0)}% and immediate organic traffic redirection.`}
|
|
670
|
+
</div>
|
|
671
|
+
</div>`;
|
|
672
|
+
}
|
|
673
|
+
const connectionDiagnosticsPanelHtml = `
|
|
674
|
+
<div class="card" style="margin-top: 2rem; background: #131c2e; border: 1px solid #1e293b;">
|
|
675
|
+
<h3 style="color: #38bdf8; font-size: 1.2rem; border-bottom: none; margin-bottom: 0.25rem;">
|
|
676
|
+
\u{1F50C} Network Connection & Detailed HTTP Status Diagnostics
|
|
677
|
+
</h3>
|
|
678
|
+
<div style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 1.5rem;">
|
|
679
|
+
Real-time breakdown tracking transport anomalies and precise layer-7 distribution indicators.
|
|
680
|
+
</div>
|
|
681
|
+
|
|
682
|
+
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 2rem; flex-wrap: wrap;">
|
|
683
|
+
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
684
|
+
<div style="background: #090d16; padding: 1.25rem; border-radius: 6px; border: 1px solid #1e293b;">
|
|
685
|
+
<div style="font-size: 0.75rem; color: #64748b; text-transform: uppercase; font-weight: bold;">ECONNRESET Rate</div>
|
|
686
|
+
<div style="font-size: 1.8rem; font-weight: bold; color: ${cards.econnresetRate > 1 ? "#ef4444" : "#10b981"};">
|
|
687
|
+
${cards.econnresetRate.toFixed(2)}<span style="font-size: 1rem; color: #94a3b8;"> %</span>
|
|
688
|
+
</div>
|
|
689
|
+
<div style="font-size: 0.8rem; color: #64748b; margin-top: 0.25rem;">Abrupt remote host termination frequency</div>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<div style="background: #090d16; padding: 1.25rem; border-radius: 6px; border: 1px solid #1e293b;">
|
|
693
|
+
<div style="font-size: 0.75rem; color: #64748b; text-transform: uppercase; font-weight: bold;">ECONNREFUSED Count</div>
|
|
694
|
+
<div style="font-size: 1.8rem; font-weight: bold; color: ${cards.econnrefusedCount > 0 ? "#f59e0b" : "#10b981"};">
|
|
695
|
+
${cards.econnrefusedCount}<span style="font-size: 1rem; color: #94a3b8;"> drops</span>
|
|
696
|
+
</div>
|
|
697
|
+
<div style="font-size: 0.8rem; color: #64748b; margin-top: 0.25rem;">Total complete backend port link rejections</div>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
|
|
701
|
+
<div style="background: #090d16; padding: 1.25rem; border-radius: 6px; border: 1px solid #1e293b;">
|
|
702
|
+
<div style="font-size: 0.85rem; font-weight: bold; color: #f8fafc; margin-bottom: 1rem;">Target Response Code Matrix Rates</div>
|
|
703
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem;">
|
|
704
|
+
${Object.keys(cards.codeRates).map((code) => {
|
|
705
|
+
const percentage = cards.codeRates[code];
|
|
706
|
+
let textColor = "#cbd5e1";
|
|
707
|
+
if (percentage > 0) {
|
|
708
|
+
textColor = code.startsWith("5") ? "#f87171" : "#fde047";
|
|
709
|
+
}
|
|
710
|
+
return `
|
|
711
|
+
<div style="background: #131c2e; padding: 0.75rem; border-radius: 4px; border: 1px solid #1e293b; text-align: center;">
|
|
712
|
+
<div style="font-size: 0.8rem; font-weight: bold; color: #94a3b8;">Code ${code}</div>
|
|
713
|
+
<div style="font-size: 1.1rem; font-weight: bold; margin-top: 0.25rem; color: ${textColor};">
|
|
714
|
+
${percentage.toFixed(2)}%
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
`;
|
|
718
|
+
}).join("")}
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
</div>`;
|
|
512
723
|
let logClustersHtml = "";
|
|
513
724
|
if (semanticReport) {
|
|
514
725
|
const totalLogCount = semanticReport.clusters.reduce((sum, c) => sum + c.count, 0);
|
|
@@ -519,7 +730,7 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
519
730
|
\u{1F9E0} Interactive Remediation Playbooks & Log Classification
|
|
520
731
|
</h3>
|
|
521
732
|
<div style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 1.5rem;">
|
|
522
|
-
Captured <strong style="color: #f8fafc;">${totalLogCount}</strong> raw failures grouped into <strong style="color: #f8fafc;">${uniquePatternsCount}</strong> unique structural patterns.
|
|
733
|
+
Captured <strong style="color: #f8fafc;">${totalLogCount}</strong> raw failures grouped into <strong style="color: #f8fafc;">${uniquePatternsCount}</strong> unique structural patterns.
|
|
523
734
|
</div>
|
|
524
735
|
|
|
525
736
|
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
@@ -542,11 +753,11 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
542
753
|
let explanation = "High concurrency is causing socket/connection starvation. Increase connection limits or enable pooling keep-alives.";
|
|
543
754
|
if (c.category === "Authentication_Error") {
|
|
544
755
|
fixTitle = "Auth Token / JWT Strategy";
|
|
545
|
-
fixCode = "// Implement token caching and silent rotation
|
|
756
|
+
fixCode = "// Implement token caching and silent rotation\\napp.use(authMiddleware({ cacheTtl: 300, allowGracePeriod: 60 }));";
|
|
546
757
|
explanation = "Frequent token verification failures under load. Extend signature verification cache or decouple auth validation check.";
|
|
547
758
|
} else if (c.category === "Product_Error") {
|
|
548
759
|
fixTitle = "Application Null-Safety / Guard Fix";
|
|
549
|
-
fixCode = "const configId = payload?.config_id ?? defaultConfigurationId
|
|
760
|
+
fixCode = "const configId = payload?.config_id ?? defaultConfigurationId;\\nif (!configId) throw new ValidationError('Missing config_id');";
|
|
550
761
|
explanation = "Runtime reference error for undefined payload field under race condition spikes. Add optional chaining and input contracts.";
|
|
551
762
|
}
|
|
552
763
|
const snippetId = `snippet-${idx}`;
|
|
@@ -636,7 +847,8 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
636
847
|
.meta { color: #94a3b8; font-size: 0.9rem; margin-top: 0.5rem; }
|
|
637
848
|
|
|
638
849
|
.section-title { font-size: 1.2rem; color: #cbd5e1; font-weight: bold; margin: 2rem 0 1rem 0; padding-bottom: 0.4rem; border-bottom: 1px solid #1e293b; }
|
|
639
|
-
.cards-wrapper { display: grid; grid-template-columns: repeat(
|
|
850
|
+
.cards-wrapper { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
|
|
851
|
+
@media (max-width: 1024px) { .cards-wrapper { grid-template-columns: repeat(2, 1fr); } }
|
|
640
852
|
.metric-card { background: #131c2e; border: 1px solid #1e293b; border-radius: 6px; padding: 1.25rem; position: relative; }
|
|
641
853
|
.card-title { font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
|
642
854
|
.card-value { font-size: 1.6rem; font-weight: 700; color: #ffffff; }
|
|
@@ -645,6 +857,7 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
645
857
|
.card-value.orange { color: #f97316; }
|
|
646
858
|
.card-value.purple { color: #a855f7; }
|
|
647
859
|
.card-value.cyan { color: #38bdf8; }
|
|
860
|
+
.card-value.red { color: #f87171; }
|
|
648
861
|
.card-subtext { font-size: 0.8rem; font-weight: 500; margin-left: 0.4rem; display: inline-block; }
|
|
649
862
|
.card-subtext.green { color: #10b981; }
|
|
650
863
|
.card-subtext.orange { color: #f97316; }
|
|
@@ -692,12 +905,20 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
692
905
|
<div class="cards-wrapper">
|
|
693
906
|
<div class="metric-card">
|
|
694
907
|
<div class="card-title">Apdex Score (T: ${config.apdexT}ms)</div>
|
|
695
|
-
<div class="card-value green">${cards.apdex.toFixed(2)}<span class="card-subtext green">(
|
|
908
|
+
<div class="card-value green">${cards.apdex.toFixed(2)}<span class="card-subtext green">(Verified)</span></div>
|
|
696
909
|
</div>
|
|
697
910
|
<div class="metric-card">
|
|
698
911
|
<div class="card-title">Throughput</div>
|
|
699
912
|
<div class="card-value orange">${cards.throughput.toFixed(1)}<span class="unit">/s</span></div>
|
|
700
913
|
</div>
|
|
914
|
+
<div class="metric-card">
|
|
915
|
+
<div class="card-title">Minimum Response Time</div>
|
|
916
|
+
<div class="card-value green">${cards.minLatency.toFixed(1)}<span class="unit">ms</span></div>
|
|
917
|
+
</div>
|
|
918
|
+
<div class="metric-card">
|
|
919
|
+
<div class="card-title">Maximum Response Time</div>
|
|
920
|
+
<div class="card-value red">${cards.maxLatency.toFixed(1)}<span class="unit">ms</span></div>
|
|
921
|
+
</div>
|
|
701
922
|
<div class="metric-card">
|
|
702
923
|
<div class="card-title">Network Bandwidth</div>
|
|
703
924
|
<div class="card-value purple">${cards.bandwidth.toFixed(2)}<span class="unit">MB/s</span></div>
|
|
@@ -804,8 +1025,8 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
804
1025
|
<th>Throughput</th>
|
|
805
1026
|
<th>Avg Latency</th>
|
|
806
1027
|
<th>P95 Latency</th>
|
|
807
|
-
<th>
|
|
808
|
-
<th>
|
|
1028
|
+
<th>Min / Max Floor</th>
|
|
1029
|
+
<th>RESET Rate</th>
|
|
809
1030
|
<th>Status</th>
|
|
810
1031
|
</tr>
|
|
811
1032
|
</thead>
|
|
@@ -817,14 +1038,16 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
817
1038
|
<td>${h.throughput.toFixed(0)} req/sec</td>
|
|
818
1039
|
<td>${h.avgLatency.toFixed(1)} ms</td>
|
|
819
1040
|
<td>${h.p95Latency.toFixed(1)} ms</td>
|
|
820
|
-
<td>${h.
|
|
821
|
-
<td>${
|
|
1041
|
+
<td><small>${h.minLatency.toFixed(1)}ms / ${h.maxLatency.toFixed(1)}ms</small></td>
|
|
1042
|
+
<td>${h.econnresetRate.toFixed(2)}%</td>
|
|
822
1043
|
<td><span class="badge ${isPassed ? "badge-success" : "badge-fail"}">${isPassed ? "SLO PASS" : "SLO BREACH"}</span></td>
|
|
823
1044
|
</tr>`;
|
|
824
1045
|
}).join("")}
|
|
825
1046
|
</tbody>
|
|
826
1047
|
</table>
|
|
827
1048
|
</div>
|
|
1049
|
+
${seoPredictorPanelHtml}
|
|
1050
|
+
${connectionDiagnosticsPanelHtml}
|
|
828
1051
|
${liveAdjusterHtml}
|
|
829
1052
|
${rightSizingHtml}
|
|
830
1053
|
${logClustersHtml}
|
|
@@ -844,7 +1067,6 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
844
1067
|
const baseThroughput = ${cards.throughput};
|
|
845
1068
|
const baseLatency = ${cards.ttfb};
|
|
846
1069
|
|
|
847
|
-
// Live Concurrency Adjuster interactive client logic
|
|
848
1070
|
const slider = document.getElementById('concurrencySlider');
|
|
849
1071
|
const sliderVal = document.getElementById('sliderValue');
|
|
850
1072
|
const simThroughput = document.getElementById('simThroughput');
|
|
@@ -929,8 +1151,8 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
929
1151
|
},
|
|
930
1152
|
scales: {
|
|
931
1153
|
x: { grid: { color: '#1e293b' }, ticks: { color: '#64748b' } },
|
|
932
|
-
|
|
933
|
-
|
|
1154
|
+
yThreads: { position: 'left', grid: { color: '#1e293b' }, ticks: { color: '#38bdf8' }, title: { display: true, text: 'VUs / Threads', color: '#38bdf8' } },
|
|
1155
|
+
yTtf: { position: 'right', grid: { drawOnChartArea: false }, ticks: { color: '#f43f5e' }, title: { display: true, text: 'TTF Index / Latency ms', color: '#f43f5e' } }
|
|
934
1156
|
}
|
|
935
1157
|
}
|
|
936
1158
|
});
|
|
@@ -990,6 +1212,6 @@ function exportHtmlDashboard(history, config, verdictReason, semanticReport, res
|
|
|
990
1212
|
function printUsage() {
|
|
991
1213
|
console.log(`Blaze Core Performance Test CLI Engine
|
|
992
1214
|
Options:
|
|
993
|
-
--threads <count> | --duration <seconds> | --agentic | --cluster-logs | --correlate | --apdex-t <ms
|
|
1215
|
+
--threads <count> | --duration <seconds> | --agentic | --cluster-logs | --correlate | --apdex-t <ms> | --seo`);
|
|
994
1216
|
}
|
|
995
1217
|
main().catch(console.error);
|
|
Binary file
|