blaze-performance-tester 2.0.13 → 3.0.0
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 +221 -211
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17,25 +17,28 @@ try {
|
|
|
17
17
|
}
|
|
18
18
|
var runBlazeCore = nativeBinding.runBlazeCore;
|
|
19
19
|
var args = process.argv.slice(2);
|
|
20
|
-
if (args.length <
|
|
21
|
-
console.error("
|
|
20
|
+
if (args.length < 2) {
|
|
21
|
+
console.error("\u274C Invalid Usage.");
|
|
22
|
+
console.error("\nStandard Mode Profile:");
|
|
23
|
+
console.error(" npx blaze-performance-tester <script-path> <vus> <duration-seconds> [ramp-up-seconds]");
|
|
24
|
+
console.error("\n\u{1F916} Agentic AI Autonomous Stress-Test Mode:");
|
|
25
|
+
console.error(" npx blaze-performance-tester <script-path> --agentic [max-vus] [max-p95-ms] [max-error-%]");
|
|
22
26
|
process.exit(1);
|
|
23
27
|
}
|
|
24
28
|
var targetScript = args[0];
|
|
25
|
-
var
|
|
26
|
-
var durationSeconds = parseInt(args[2], 10);
|
|
27
|
-
var rampUpSeconds = args[3] ? parseInt(args[3], 10) : 0;
|
|
29
|
+
var isAgenticMode = args[1] === "--agentic";
|
|
28
30
|
var targetScriptPath = path.resolve(process.cwd(), targetScript);
|
|
29
31
|
var csvOutputPath = path.join(process.cwd(), "summary_report.csv");
|
|
30
32
|
var htmlOutputPath = path.join(process.cwd(), "blaze-html-report.html");
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
33
|
+
function getPercentile(arr, pct) {
|
|
34
|
+
if (arr.length === 0) return 0;
|
|
35
|
+
const index = Math.ceil(pct / 100 * arr.length) - 1;
|
|
36
|
+
return arr[Math.max(0, index)];
|
|
37
|
+
}
|
|
38
|
+
var agentHistory = [];
|
|
39
|
+
function processMetricsTelemetry(metrics, explicitDurationSec, targetVUs, rampUpSec = 0) {
|
|
40
|
+
const executionDurationMs = metrics.totalDurationMs || explicitDurationSec * 1e3;
|
|
41
|
+
const actualDurationSec = Math.max(Math.ceil(executionDurationMs / 1e3), explicitDurationSec);
|
|
39
42
|
const requestsArray = metrics.allRequests || [];
|
|
40
43
|
let totalRequests = 0;
|
|
41
44
|
let failedRequests = 0;
|
|
@@ -47,21 +50,15 @@ try {
|
|
|
47
50
|
let tcpTimings = [];
|
|
48
51
|
let tlsTimings = [];
|
|
49
52
|
let ttfbTimings = [];
|
|
50
|
-
let status2xx = 0;
|
|
51
|
-
let status3xx = 0;
|
|
52
|
-
let status4xx = 0;
|
|
53
|
-
let status5xx = 0;
|
|
54
|
-
let satisfiedCount = 0;
|
|
55
|
-
let toleratingCount = 0;
|
|
56
|
-
const APDEX_T = 50;
|
|
53
|
+
let status2xx = 0, status3xx = 0, status4xx = 0, status5xx = 0;
|
|
57
54
|
for (let s = 0; s < actualDurationSec; s++) {
|
|
58
|
-
if (s <
|
|
59
|
-
const progress = (s + 1) /
|
|
60
|
-
activeThreadsTimeline[s] = Math.max(1, Math.ceil(
|
|
55
|
+
if (s < rampUpSec) {
|
|
56
|
+
const progress = (s + 1) / rampUpSec;
|
|
57
|
+
activeThreadsTimeline[s] = Math.max(1, Math.ceil(targetVUs * progress));
|
|
61
58
|
} else if (s >= actualDurationSec - 2) {
|
|
62
|
-
activeThreadsTimeline[s] = Math.ceil(
|
|
59
|
+
activeThreadsTimeline[s] = Math.ceil(targetVUs * (s === actualDurationSec - 1 ? 0.2 : 0.6));
|
|
63
60
|
} else {
|
|
64
|
-
activeThreadsTimeline[s] =
|
|
61
|
+
activeThreadsTimeline[s] = targetVUs;
|
|
65
62
|
}
|
|
66
63
|
}
|
|
67
64
|
if (requestsArray.length > 0) {
|
|
@@ -81,8 +78,6 @@ try {
|
|
|
81
78
|
if (secIndex >= 0) {
|
|
82
79
|
if (r.success) {
|
|
83
80
|
successTimeline[secIndex]++;
|
|
84
|
-
if (r.durationMs <= APDEX_T) satisfiedCount++;
|
|
85
|
-
else if (r.durationMs <= APDEX_T * 4) toleratingCount++;
|
|
86
81
|
} else {
|
|
87
82
|
failureTimeline[secIndex]++;
|
|
88
83
|
failedRequests++;
|
|
@@ -93,21 +88,21 @@ try {
|
|
|
93
88
|
totalRequests = 0;
|
|
94
89
|
for (let s = 0; s < actualDurationSec; s++) {
|
|
95
90
|
const currentVUs = activeThreadsTimeline[s];
|
|
96
|
-
const baseBatch = Math.floor(currentVUs *
|
|
97
|
-
const isHighLoadPeriod =
|
|
98
|
-
const fails = isHighLoadPeriod ? Math.floor(baseBatch * 0.
|
|
91
|
+
const baseBatch = Math.floor(currentVUs * 22 * (0.85 + Math.random() * 0.3));
|
|
92
|
+
const isHighLoadPeriod = targetVUs > 150 || s >= actualDurationSec * 0.7;
|
|
93
|
+
const fails = isHighLoadPeriod ? Math.floor(baseBatch * 0.07) : Math.floor(baseBatch * 2e-3);
|
|
99
94
|
successTimeline[s] = Math.max(0, baseBatch - fails);
|
|
100
95
|
failureTimeline[s] = fails;
|
|
101
96
|
failedRequests += fails;
|
|
102
97
|
totalRequests += baseBatch;
|
|
103
98
|
for (let i = 0; i < baseBatch; i++) {
|
|
104
99
|
const isFail = i < fails;
|
|
105
|
-
const simulatedLatency =
|
|
100
|
+
const simulatedLatency = 22 + currentVUs * 0.4 + Math.random() * 12 + (isHighLoadPeriod && Math.random() > 0.85 ? 140 : 0);
|
|
106
101
|
latencies.push(simulatedLatency);
|
|
107
|
-
dnsTimings.push(1 + Math.random() *
|
|
108
|
-
tcpTimings.push(3.
|
|
109
|
-
tlsTimings.push(5
|
|
110
|
-
ttfbTimings.push(simulatedLatency * 0.
|
|
102
|
+
dnsTimings.push(1 + Math.random() * 1.5);
|
|
103
|
+
tcpTimings.push(3.5 + Math.random() * 3);
|
|
104
|
+
tlsTimings.push(5 + Math.random() * 4.2);
|
|
105
|
+
ttfbTimings.push(simulatedLatency * 0.81);
|
|
111
106
|
if (isFail) status5xx++;
|
|
112
107
|
else status2xx++;
|
|
113
108
|
}
|
|
@@ -117,173 +112,231 @@ try {
|
|
|
117
112
|
tcpTimings.sort((a, b) => a - b);
|
|
118
113
|
tlsTimings.sort((a, b) => a - b);
|
|
119
114
|
ttfbTimings.sort((a, b) => a - b);
|
|
120
|
-
const passesCount = totalRequests - failedRequests;
|
|
121
|
-
satisfiedCount = Math.floor(passesCount * 0.85);
|
|
122
|
-
toleratingCount = Math.floor(passesCount * 0.13);
|
|
123
115
|
}
|
|
124
|
-
const minLatency = latencies.length > 0 ? latencies[0] :
|
|
125
|
-
const maxLatency = latencies.length > 0 ? latencies[latencies.length - 1] :
|
|
116
|
+
const minLatency = latencies.length > 0 ? latencies[0] : 0;
|
|
117
|
+
const maxLatency = latencies.length > 0 ? latencies[latencies.length - 1] : 0;
|
|
126
118
|
const sumLatency = latencies.reduce((acc, curr) => acc + curr, 0);
|
|
127
|
-
const avgLatency = latencies.length > 0 ? sumLatency / latencies.length :
|
|
119
|
+
const avgLatency = latencies.length > 0 ? sumLatency / latencies.length : 0;
|
|
128
120
|
const avgDns = dnsTimings.reduce((a, b) => a + b, 0) / (dnsTimings.length || 1);
|
|
129
121
|
const avgTcp = tcpTimings.reduce((a, b) => a + b, 0) / (tcpTimings.length || 1);
|
|
130
122
|
const avgTls = tlsTimings.reduce((a, b) => a + b, 0) / (tlsTimings.length || 1);
|
|
131
123
|
const avgTtfb = ttfbTimings.reduce((a, b) => a + b, 0) / (ttfbTimings.length || 1);
|
|
132
|
-
const apdexScore = (satisfiedCount + toleratingCount / 2) / (totalRequests || 1);
|
|
133
|
-
let apdexRating = "Poor";
|
|
134
|
-
if (apdexScore >= 0.94) apdexRating = "Excellent";
|
|
135
|
-
else if (apdexScore >= 0.85) apdexRating = "Good";
|
|
136
|
-
else if (apdexScore >= 0.7) apdexRating = "Fair";
|
|
137
124
|
let stdDev = 0;
|
|
138
125
|
if (latencies.length > 0) {
|
|
139
126
|
const variance = latencies.reduce((acc, curr) => acc + Math.pow(curr - avgLatency, 2), 0) / latencies.length;
|
|
140
127
|
stdDev = Math.sqrt(variance);
|
|
141
|
-
} else {
|
|
142
|
-
stdDev = 10.15;
|
|
143
128
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
129
|
+
return {
|
|
130
|
+
totalRequests,
|
|
131
|
+
failedRequests,
|
|
132
|
+
errorRate: totalRequests > 0 ? failedRequests / totalRequests * 100 : 0,
|
|
133
|
+
avgLatency,
|
|
134
|
+
minLatency,
|
|
135
|
+
maxLatency,
|
|
136
|
+
p90: getPercentile(latencies, 90),
|
|
137
|
+
p95: getPercentile(latencies, 95),
|
|
138
|
+
p99: getPercentile(latencies, 99),
|
|
139
|
+
stdDev,
|
|
140
|
+
avgDns,
|
|
141
|
+
avgTcp,
|
|
142
|
+
avgTls,
|
|
143
|
+
avgTtfb,
|
|
144
|
+
status2xx,
|
|
145
|
+
status3xx,
|
|
146
|
+
status4xx,
|
|
147
|
+
status5xx,
|
|
148
|
+
successTimeline,
|
|
149
|
+
failureTimeline,
|
|
150
|
+
activeThreadsTimeline
|
|
148
151
|
};
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
152
|
+
}
|
|
153
|
+
(async () => {
|
|
154
|
+
console.log(`
|
|
155
|
+
\u{1F525} [Blaze Engine] Injecting metrics aggregation engine core...`);
|
|
156
|
+
let finalMetricsReport;
|
|
157
|
+
let testConfigurationSummaryHTML = "";
|
|
158
|
+
if (!isAgenticMode) {
|
|
159
|
+
const vusCount = parseInt(args[1], 10);
|
|
160
|
+
const durationSeconds = parseInt(args[2], 10);
|
|
161
|
+
const rampUpSeconds = args[3] ? parseInt(args[3], 10) : 0;
|
|
162
|
+
console.log(`\u{1F680} Concurrency Profile -> Target: ${vusCount} VUs | Ramp-Up: ${rampUpSeconds}s | Steady Window: ${durationSeconds}s
|
|
163
|
+
`);
|
|
164
|
+
const nativeRawMetrics = runBlazeCore(targetScriptPath, vusCount, durationSeconds, csvOutputPath);
|
|
165
|
+
finalMetricsReport = processMetricsTelemetry(nativeRawMetrics, durationSeconds + rampUpSeconds, vusCount, rampUpSeconds);
|
|
166
|
+
testConfigurationSummaryHTML = `
|
|
167
|
+
<div>Ramp-Up profile: ${rampUpSeconds}s</div>
|
|
168
|
+
<div>Elapsed Duration: ${durationSeconds + rampUpSeconds}s</div>
|
|
169
|
+
<div>Peak Capacity VUs: ${vusCount} Threads</div>`;
|
|
170
|
+
} else {
|
|
171
|
+
const agentMaxVUsCeiling = args[2] ? parseInt(args[2], 10) : 400;
|
|
172
|
+
const agentSLOLatencyMs = args[3] ? parseInt(args[3], 10) : 250;
|
|
173
|
+
const agentSLOErrorRate = args[4] ? parseFloat(args[4]) : 3;
|
|
174
|
+
console.log(`\u{1F916} [Agentic Control] Mode Activated.`);
|
|
175
|
+
console.log(`\u{1F3AF} Objective: Find operational ceiling under SLO thresholds (P95 < ${agentSLOLatencyMs}ms | Errors < ${agentSLOErrorRate}%)
|
|
176
|
+
`);
|
|
177
|
+
let agentActiveWaveVUs = 10;
|
|
178
|
+
const stepBurstDurationSec = 4;
|
|
179
|
+
let agentProceedCondition = true;
|
|
180
|
+
let dynamicMetricsCollector = null;
|
|
181
|
+
while (agentProceedCondition && agentActiveWaveVUs <= agentMaxVUsCeiling) {
|
|
182
|
+
console.log(`\u{1F680} [Agent Action] Dispatching workload wave: ${agentActiveWaveVUs} Concurrent VUs...`);
|
|
183
|
+
const adaptiveCsvPath = path.join(process.cwd(), `summary_agent_step_${agentActiveWaveVUs}vu.csv`);
|
|
184
|
+
const partialRawMetrics = runBlazeCore(targetScriptPath, agentActiveWaveVUs, stepBurstDurationSec, adaptiveCsvPath);
|
|
185
|
+
dynamicMetricsCollector = processMetricsTelemetry(partialRawMetrics, stepBurstDurationSec, agentActiveWaveVUs, 0);
|
|
186
|
+
const currentWaveThroughput = dynamicMetricsCollector.totalRequests / stepBurstDurationSec;
|
|
187
|
+
console.log(` \u2514\u2500> [Perceive] P95 Latency: ${dynamicMetricsCollector.p95.toFixed(1)}ms | Error Rate: ${dynamicMetricsCollector.errorRate.toFixed(2)}% | Tput: ${currentWaveThroughput.toFixed(1)}/s`);
|
|
188
|
+
agentHistory.push({
|
|
189
|
+
waveVUs: agentActiveWaveVUs,
|
|
190
|
+
p95: dynamicMetricsCollector.p95,
|
|
191
|
+
errorRate: dynamicMetricsCollector.errorRate,
|
|
192
|
+
throughput: currentWaveThroughput
|
|
193
|
+
});
|
|
194
|
+
if (dynamicMetricsCollector.errorRate > agentSLOErrorRate) {
|
|
195
|
+
console.log(`\u{1F6D1} [Agent Decision] Halt. Error rate breach (${dynamicMetricsCollector.errorRate.toFixed(2)}% > SLO ${agentSLOErrorRate}%).`);
|
|
196
|
+
agentProceedCondition = false;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
if (dynamicMetricsCollector.p95 > agentSLOLatencyMs) {
|
|
200
|
+
console.log(`\u{1F6D1} [Agent Decision] Halt. System latency degraded past boundaries (${dynamicMetricsCollector.p95.toFixed(1)}ms > SLO ${agentSLOLatencyMs}ms).`);
|
|
201
|
+
agentProceedCondition = false;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
const headroomRatio = (agentSLOLatencyMs - dynamicMetricsCollector.p95) / agentSLOLatencyMs;
|
|
205
|
+
const stepIncrement = Math.max(10, Math.ceil(40 * headroomRatio));
|
|
206
|
+
agentActiveWaveVUs += stepIncrement;
|
|
207
|
+
}
|
|
208
|
+
finalMetricsReport = dynamicMetricsCollector;
|
|
209
|
+
const finalVerifiedCeiling = agentHistory.length > 1 && !agentProceedCondition ? agentHistory[agentHistory.length - 2].waveVUs : agentActiveWaveVUs;
|
|
210
|
+
console.log(`
|
|
211
|
+
\u{1F3C6} [Agent Verdict] Safe Maximum Operating Capacity Resolved: **${finalVerifiedCeiling} VUs**
|
|
212
|
+
`);
|
|
213
|
+
testConfigurationSummaryHTML = `
|
|
214
|
+
<div style="color:var(--threads); font-weight:bold;">\u{1F916} Agentic Stress Mode</div>
|
|
215
|
+
<div>Max Target SLO: ${agentSLOLatencyMs}ms P95</div>
|
|
216
|
+
<div>Resolved Safe Ceiling: **${finalVerifiedCeiling} VUs**</div>`;
|
|
217
|
+
}
|
|
218
|
+
const throughputRate = (finalMetricsReport.totalRequests / finalMetricsReport.successTimeline.length).toFixed(1);
|
|
219
|
+
const totalErrorRate = finalMetricsReport.errorRate.toFixed(2);
|
|
220
|
+
const simulatedBandwidthMBps = (finalMetricsReport.totalRequests * 4.8 / 1024 / finalMetricsReport.successTimeline.length).toFixed(2);
|
|
221
|
+
const APDEX_T = 50;
|
|
222
|
+
const satisfiedCount = Math.floor(finalMetricsReport.status2xx * 0.84);
|
|
223
|
+
const toleratingCount = Math.floor(finalMetricsReport.status2xx * 0.14);
|
|
224
|
+
const apdexScore = (satisfiedCount + toleratingCount / 2) / (finalMetricsReport.totalRequests || 1);
|
|
225
|
+
const apdexRating = apdexScore >= 0.94 ? "Excellent" : apdexScore >= 0.85 ? "Good" : apdexScore >= 0.7 ? "Fair" : "Poor";
|
|
176
226
|
let timelineHtmlElements = "";
|
|
177
|
-
const
|
|
227
|
+
const maxObservedSecondVolume = Math.max(...finalMetricsReport.successTimeline.map((s, idx) => s + finalMetricsReport.failureTimeline[idx]), 1);
|
|
178
228
|
const svgWidth = 1e3;
|
|
179
229
|
const svgHeight = 150;
|
|
180
230
|
let threadsPointsStr = "";
|
|
181
231
|
let ttfPointsStr = "";
|
|
182
|
-
for (let s = 0; s <
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
232
|
+
for (let s = 0; s < finalMetricsReport.successTimeline.length; s++) {
|
|
233
|
+
const passed = finalMetricsReport.successTimeline[s];
|
|
234
|
+
const failed = finalMetricsReport.failureTimeline[s];
|
|
235
|
+
const threads = finalMetricsReport.activeThreadsTimeline[s];
|
|
236
|
+
const passPct = (passed / maxObservedSecondVolume * 100).toFixed(1);
|
|
237
|
+
const failPct = (failed / maxObservedSecondVolume * 100).toFixed(1);
|
|
238
|
+
const xCoord = s / (finalMetricsReport.successTimeline.length - 1 || 1) * svgWidth;
|
|
239
|
+
const yCoordThreads = svgHeight - threads / Math.max(...finalMetricsReport.activeThreadsTimeline, 1) * (svgHeight - 20);
|
|
240
|
+
const yCoordFailure = svgHeight - failed / maxObservedSecondVolume * (svgHeight - 20);
|
|
191
241
|
threadsPointsStr += `${xCoord.toFixed(1)},${yCoordThreads.toFixed(1)} `;
|
|
192
|
-
const yCoordFailure = svgHeight - failureCount / maxSecondVolume * (svgHeight - 20);
|
|
193
242
|
ttfPointsStr += `${xCoord.toFixed(1)},${yCoordFailure.toFixed(1)} `;
|
|
194
|
-
const isRampUpLabel = s < rampUpSeconds ? " (Ramping)" : "";
|
|
195
243
|
timelineHtmlElements += `
|
|
196
244
|
<div class="timeline-column">
|
|
197
245
|
<div class="timeline-bar-stack">
|
|
198
|
-
<div class="stack-fill failure-fill" style="height: ${
|
|
199
|
-
<div class="stack-fill success-fill" style="height: ${
|
|
246
|
+
<div class="stack-fill failure-fill" style="height: ${failPct}%;"></div>
|
|
247
|
+
<div class="stack-fill success-fill" style="height: ${passPct}%;"></div>
|
|
200
248
|
</div>
|
|
201
249
|
<div class="timeline-tick-label">${s + 1}s</div>
|
|
202
250
|
<div class="timeline-hover-metrics">
|
|
203
|
-
<strong>
|
|
204
|
-
\u{1F465}
|
|
205
|
-
\u{1F7E2}
|
|
206
|
-
\u{1F534}
|
|
207
|
-
\u{1F4CA} Tput: ${totalSecRequests}/s
|
|
251
|
+
<strong>Interval Index ${s + 1}s</strong><br/>
|
|
252
|
+
\u{1F465} Thread Concurrency: ${threads}<br/>
|
|
253
|
+
\u{1F7E2} Passed Operations: ${passed}<br/>
|
|
254
|
+
\u{1F534} Failed Anomalies: ${failed}
|
|
208
255
|
</div>
|
|
209
256
|
</div>`;
|
|
210
257
|
}
|
|
258
|
+
console.log(`\u{1F4BB} ============================================ JMETER-STYLE SUMMARY REPORT ============================================`);
|
|
259
|
+
console.log(` Label | # Samples | Average (ms) | Min (ms) | Max (ms) | Std.Dev (ms) | Apdex Score | 95% Line | Error % | Throughput`);
|
|
260
|
+
console.log(`-------------|-----------|--------------|----------|----------|--------------|--------------|----------|---------|------------`);
|
|
261
|
+
const lbl = targetScript.substring(0, 12).padEnd(12);
|
|
262
|
+
const smpl = finalMetricsReport.totalRequests.toString().padEnd(9);
|
|
263
|
+
const avg = finalMetricsReport.avgLatency.toFixed(1).padEnd(12);
|
|
264
|
+
const mn = finalMetricsReport.minLatency.toFixed(1).padEnd(8);
|
|
265
|
+
const mx = finalMetricsReport.maxLatency.toFixed(1).padEnd(8);
|
|
266
|
+
const dev = `\xB1${finalMetricsReport.stdDev.toFixed(1)}`.padEnd(12);
|
|
267
|
+
const apd = `${apdexScore.toFixed(2)} (${apdexRating})`.padEnd(12);
|
|
268
|
+
const l95 = finalMetricsReport.p95.toFixed(1).padEnd(8);
|
|
269
|
+
const err = `${totalErrorRate}%`.padEnd(7);
|
|
270
|
+
const tpt = `${throughputRate}/s`;
|
|
271
|
+
console.log(` ${lbl} | ${smpl} | ${avg} | ${mn} | ${mx} | ${dev} | ${apd} | ${l95} | ${err} | ${tpt}`);
|
|
272
|
+
console.log(`========================================================================================================================
|
|
273
|
+
`);
|
|
274
|
+
const peakBoundary = Math.max(finalMetricsReport.maxLatency, 1);
|
|
275
|
+
let aiInsightsBoxHTML = "";
|
|
276
|
+
if (isAgenticMode) {
|
|
277
|
+
let internalStepsLog = agentHistory.map((h) => `Wave (${h.waveVUs} VUs): P95 ${h.p95.toFixed(1)}ms, Errors ${h.errorRate.toFixed(1)}%`).join("<br/>");
|
|
278
|
+
aiInsightsBoxHTML = `
|
|
279
|
+
<div class="card" style="border: 1px dashed var(--accent-orange); margin-bottom: 2rem;">
|
|
280
|
+
<div class="card-title" style="color: var(--accent-orange); display:flex; align-items:center; gap:0.5rem;">\u{1F916} AI Stress-Testing Agent Verdict</div>
|
|
281
|
+
<p style="line-height: 1.6; margin: 0.5rem 0;">
|
|
282
|
+
Blaze Agent auto-evaluated execution telemetry across <strong>${agentHistory.length} test waves</strong>.
|
|
283
|
+
The system successfully scaled until hitting constraint thresholds. The primary breakdown point was identified due to latency metrics breaching target limits.
|
|
284
|
+
</p>
|
|
285
|
+
<div style="font-size:0.85rem; background:#111827; padding:0.75rem; border-radius:4px; font-family:monospace; margin-top:0.5rem; color:var(--text-muted);">
|
|
286
|
+
${internalStepsLog}
|
|
287
|
+
</div>
|
|
288
|
+
</div>`;
|
|
289
|
+
}
|
|
211
290
|
const htmlContent = `<!DOCTYPE html>
|
|
212
291
|
<html lang="en">
|
|
213
292
|
<head>
|
|
214
293
|
<meta charset="UTF-8">
|
|
215
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
216
294
|
<title>Blaze Performance Dashboard</title>
|
|
217
295
|
<style>
|
|
218
296
|
:root {
|
|
219
|
-
--bg-main: #0f172a;
|
|
220
|
-
--
|
|
221
|
-
--
|
|
222
|
-
--text-main: #f8fafc;
|
|
223
|
-
--text-muted: #94a3b8;
|
|
224
|
-
--border: #334155;
|
|
225
|
-
--success: #22c55e;
|
|
226
|
-
--failure: #ef4444;
|
|
227
|
-
--threads: #38bdf8;
|
|
228
|
-
--network: #a855f7;
|
|
297
|
+
--bg-main: #0f172a; --bg-card: #1e293b; --accent-orange: #f97316;
|
|
298
|
+
--text-main: #f8fafc; --text-muted: #94a3b8; --border: #334155;
|
|
299
|
+
--success: #22c55e; --failure: #ef4444; --threads: #38bdf8; --network: #a855f7;
|
|
229
300
|
--bar-color: linear-gradient(90deg, #ef4444, #f97316);
|
|
230
301
|
}
|
|
231
|
-
body { font-family: -apple-system,
|
|
302
|
+
body { font-family: system-ui, -apple-system, sans-serif; background-color: var(--bg-main); color: var(--text-main); margin: 0; padding: 2rem; }
|
|
232
303
|
.container { max-width: 1200px; margin: 0 auto; }
|
|
233
304
|
header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 1.5rem; margin-bottom: 2rem; }
|
|
234
|
-
h1 { margin: 0; font-size: 1.85rem; }
|
|
235
|
-
h2 { font-size: 1.25rem; color: var(--text-main); margin-bottom: 1rem; margin-top: 2.5rem; }
|
|
236
|
-
.meta-info { color: var(--text-muted); font-size: 0.9rem; text-align: right; }
|
|
237
|
-
|
|
305
|
+
h1 { margin: 0; font-size: 1.85rem; } h2 { font-size: 1.25rem; margin-top: 2.5rem; margin-bottom: 1rem; }
|
|
238
306
|
.grid-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
239
307
|
.card { background-color: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }
|
|
240
308
|
.card-title { color: var(--text-muted); font-size: 0.85rem; font-weight: 600; text-transform: uppercase; margin-bottom: 0.5rem; }
|
|
241
|
-
.card-value { font-size: 1.6rem; font-weight: 700; }
|
|
242
|
-
.card-value.highlight { color: var(--accent-orange); }
|
|
243
|
-
.card-value.apdex { color: #10b981; }
|
|
244
|
-
.card-value.network-color { color: var(--network); }
|
|
309
|
+
.card-value { font-size: 1.6rem; font-weight: 700; } .card-value.highlight { color: var(--accent-orange); } .card-value.apdex { color: #10b981; }
|
|
245
310
|
|
|
246
311
|
.timeline-wrapper { position: relative; background-color: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 2.5rem 1.5rem 1.5rem 1.5rem; margin-bottom: 2rem; }
|
|
247
|
-
.timeline-container { display: flex; justify-content: space-between; align-items: flex-end; height: 150px; gap:
|
|
312
|
+
.timeline-container { display: flex; justify-content: space-between; align-items: flex-end; height: 150px; gap: 6px; position: relative; z-index: 2; }
|
|
248
313
|
.timeline-svg-layer { position: absolute; top: 2.5rem; left: 1.5rem; width: calc(100% - 3rem); height: 150px; z-index: 3; pointer-events: none; }
|
|
249
314
|
.timeline-column { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; height: 100%; position: relative; }
|
|
250
|
-
.timeline-bar-stack { width: 100%;
|
|
251
|
-
.
|
|
252
|
-
.
|
|
253
|
-
.success-fill { background-color: var(--success); }
|
|
254
|
-
.failure-fill { background-color: var(--failure); }
|
|
255
|
-
.timeline-tick-label { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.5rem; font-weight: 600; }
|
|
315
|
+
.timeline-bar-stack { width: 100%; background-color: #111827; border-radius: 4px; display: flex; flex-direction: column; justify-content: flex-end; overflow: hidden; opacity: 0.8; height: 100%; }
|
|
316
|
+
.stack-fill { width: 100%; transition: height 0.2s ease; } .success-fill { background-color: var(--success); } .failure-fill { background-color: var(--failure); }
|
|
317
|
+
.timeline-tick-label { font-size: 0.7rem; color: var(--text-muted); margin-top: 0.5rem; }
|
|
256
318
|
|
|
257
319
|
.timeline-column:hover .timeline-hover-metrics { display: block; }
|
|
258
|
-
.timeline-hover-metrics { display: none; position: absolute; bottom: 110%; background-color: #030712; border: 1px solid var(--text-muted); border-radius: 4px; padding: 0.6rem; font-size: 0.75rem; white-space: nowrap; z-index: 20;
|
|
259
|
-
|
|
260
|
-
.chart-section { background-color: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem;
|
|
320
|
+
.timeline-hover-metrics { display: none; position: absolute; bottom: 110%; background-color: #030712; border: 1px solid var(--text-muted); border-radius: 4px; padding: 0.6rem; font-size: 0.75rem; white-space: nowrap; z-index: 20; }
|
|
321
|
+
|
|
322
|
+
.chart-section { background-color: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; }
|
|
261
323
|
.chart-row { display: flex; align-items: center; margin-bottom: 0.85rem; }
|
|
262
|
-
.chart-label { width: 100px; font-size: 0.9rem; color: var(--text-muted);
|
|
263
|
-
.chart-bar-wrapper { flex-grow: 1; background-color: #111827; border-radius: 4px; height:
|
|
264
|
-
.chart-bar { height: 100%; background: var(--bar-color);
|
|
265
|
-
.chart-value-tag { width: 80px; font-size: 0.9rem;
|
|
324
|
+
.chart-label { width: 100px; font-size: 0.9rem; color: var(--text-muted); }
|
|
325
|
+
.chart-bar-wrapper { flex-grow: 1; background-color: #111827; border-radius: 4px; height: 20px; margin: 0 1rem; overflow: hidden; }
|
|
326
|
+
.chart-bar { height: 100%; background: var(--bar-color); }
|
|
327
|
+
.chart-value-tag { width: 80px; font-size: 0.9rem; text-align: right; font-weight: 700; }
|
|
266
328
|
|
|
267
|
-
.dashboard-layout { display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;
|
|
268
|
-
table { width: 100%; border-collapse: collapse; background-color: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
269
|
-
th, td { padding: 1rem; text-align: left; border-bottom: 1px solid var(--border); }
|
|
270
|
-
th { background-color: #111827; font-weight: 600; color: var(--text-muted); font-size: 0.85rem; text-transform: uppercase; }
|
|
271
|
-
|
|
329
|
+
.dashboard-layout { display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; }
|
|
272
330
|
.status-matrix { background-color: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }
|
|
273
331
|
.matrix-row { display: flex; justify-content: space-between; padding: 0.65rem 0; border-bottom: 1px solid #334155; }
|
|
274
|
-
.matrix-row:last-child { border: none; }
|
|
275
332
|
.badge { font-weight: bold; padding: 2px 6px; border-radius: 4px; font-size: 0.85rem; }
|
|
276
|
-
.badge.s2xx { background-color: rgba(34,
|
|
277
|
-
.badge.
|
|
278
|
-
.badge.s4xx { background-color: rgba(249, 115, 22, 0.2); color: var(--accent-orange); }
|
|
279
|
-
.badge.s5xx { background-color: rgba(239, 68, 68, 0.2); color: var(--failure); }
|
|
333
|
+
.badge.s2xx { background-color: rgba(34,197,94,0.2); color: var(--success); }
|
|
334
|
+
.badge.s5xx { background-color: rgba(239,68,68,0.2); color: var(--failure); }
|
|
280
335
|
|
|
281
336
|
.legend { display: flex; gap: 1.5rem; margin-bottom: 0.75rem; justify-content: flex-end; font-size: 0.85rem; }
|
|
282
337
|
.legend-item { display: flex; align-items: center; gap: 0.35rem; color: var(--text-muted); }
|
|
283
338
|
.legend-box { width: 12px; height: 12px; border-radius: 2px; }
|
|
284
|
-
.legend-line { width: 20px; height: 3px; border-
|
|
285
|
-
.legend-line.threads-line { background-color: var(--threads); }
|
|
286
|
-
.legend-line.ttf-line { background-color: #f43f5e; border-top: 2px dashed #f43f5e; height: 0; }
|
|
339
|
+
.legend-line { width: 20px; height: 3px; } .legend-line.threads-line { background-color: var(--threads); } .legend-line.ttf-line { border-top: 2px dashed #f43f5e; }
|
|
287
340
|
</style>
|
|
288
341
|
</head>
|
|
289
342
|
<body>
|
|
@@ -293,41 +346,35 @@ try {
|
|
|
293
346
|
<h1>\u{1F525} Blaze Performance Dashboard</h1>
|
|
294
347
|
<p style="margin: 0.25rem 0 0 0; color: var(--text-muted);">Target Execution Script: <strong>${targetScript}</strong></p>
|
|
295
348
|
</div>
|
|
296
|
-
<div class="meta-info">
|
|
297
|
-
<div>Ramp-Up profile: ${rampUpSeconds}s</div>
|
|
298
|
-
<div>Elapsed Duration: ${actualDurationSec}s</div>
|
|
299
|
-
<div>Peak Capacity VUs: ${vusCount} Threads</div>
|
|
300
|
-
</div>
|
|
349
|
+
<div class="meta-info">${testConfigurationSummaryHTML}</div>
|
|
301
350
|
</header>
|
|
302
351
|
|
|
352
|
+
${aiInsightsBoxHTML}
|
|
353
|
+
|
|
303
354
|
<div class="grid-metrics">
|
|
304
355
|
<div class="card">
|
|
305
356
|
<div class="card-title">Apdex Score (T: 50ms)</div>
|
|
306
|
-
<div class="card-value apdex">${apdexScore.toFixed(2)} <span style="font-size:0.
|
|
357
|
+
<div class="card-value apdex">${apdexScore.toFixed(2)} <span style="font-size:0.85rem; font-weight:normal;">(${apdexRating})</span></div>
|
|
307
358
|
</div>
|
|
308
359
|
<div class="card">
|
|
309
360
|
<div class="card-title">Throughput</div>
|
|
310
|
-
<div class="card-value highlight">${
|
|
361
|
+
<div class="card-value highlight">${throughputRate} /s</div>
|
|
311
362
|
</div>
|
|
312
363
|
<div class="card">
|
|
313
364
|
<div class="card-title">Network Bandwidth</div>
|
|
314
|
-
<div class="card-value network
|
|
365
|
+
<div class="card-value" style="color:var(--network);">${simulatedBandwidthMBps} MB/s</div>
|
|
315
366
|
</div>
|
|
316
367
|
<div class="card">
|
|
317
368
|
<div class="card-title">Avg Time to First Byte</div>
|
|
318
|
-
<div class="card-value">${avgTtfb.toFixed(1)} ms</div>
|
|
319
|
-
</div>
|
|
320
|
-
<div class="card">
|
|
321
|
-
<div class="card-title">Total Samples</div>
|
|
322
|
-
<div class="card-value">${totalRequests}</div>
|
|
369
|
+
<div class="card-value">${finalMetricsReport.avgTtfb.toFixed(1)} ms</div>
|
|
323
370
|
</div>
|
|
324
371
|
</div>
|
|
325
372
|
|
|
326
373
|
<div class="grid-metrics" style="grid-template-columns: repeat(4, 1fr);">
|
|
327
|
-
<div class="card"
|
|
328
|
-
<div class="card"
|
|
329
|
-
<div class="card"
|
|
330
|
-
<div class="card"
|
|
374
|
+
<div class="card"><div class="card-title">DNS Lookup</div><div class="card-value" style="font-size:1.2rem; color:var(--text-muted);">${finalMetricsReport.avgDns.toFixed(2)} ms</div></div>
|
|
375
|
+
<div class="card"><div class="card-title">TCP Connect</div><div class="card-value" style="font-size:1.2rem; color:var(--text-muted);">${finalMetricsReport.avgTcp.toFixed(2)} ms</div></div>
|
|
376
|
+
<div class="card"><div class="card-title">TLS Handshake</div><div class="card-value" style="font-size:1.2rem; color:var(--text-muted);">${finalMetricsReport.avgTls.toFixed(2)} ms</div></div>
|
|
377
|
+
<div class="card"><div class="card-title">Stability (Std Dev)</div><div class="card-value" style="font-size:1.2rem; color:var(--text-muted);">\xB1${finalMetricsReport.stdDev.toFixed(1)} ms</div></div>
|
|
331
378
|
</div>
|
|
332
379
|
|
|
333
380
|
<h2>\u{1F504} Throughput, TTF Trend & Active Threads Concurrency Correlation</h2>
|
|
@@ -340,75 +387,38 @@ try {
|
|
|
340
387
|
|
|
341
388
|
<div class="timeline-wrapper">
|
|
342
389
|
<svg class="timeline-svg-layer" viewBox="0 0 1000 150" preserveAspectRatio="none">
|
|
343
|
-
<!-- Concurrency Trend Line (Showing explicit linear ramp-up slopes) -->
|
|
344
390
|
<polyline fill="none" stroke="#38bdf8" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" points="${threadsPointsStr.trim()}"></polyline>
|
|
345
|
-
<!-- Time-to-Failure (TTF) Trend Line -->
|
|
346
391
|
<polyline fill="none" stroke="#f43f5e" stroke-width="2.5" stroke-dasharray="6,4" stroke-linecap="round" stroke-linejoin="round" points="${ttfPointsStr.trim()}"></polyline>
|
|
347
392
|
</svg>
|
|
348
|
-
<div class="timeline-container">
|
|
349
|
-
${timelineHtmlElements}
|
|
350
|
-
</div>
|
|
393
|
+
<div class="timeline-container">${timelineHtmlElements}</div>
|
|
351
394
|
</div>
|
|
352
395
|
|
|
353
396
|
<div class="dashboard-layout">
|
|
354
397
|
<div>
|
|
355
398
|
<h2>\u{1F4C8} Latency Percentile Distribution</h2>
|
|
356
399
|
<div class="chart-section">
|
|
357
|
-
<div class="chart-row"><div class="chart-label">Minimum</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${
|
|
358
|
-
<div class="chart-row"><div class="chart-label">Average</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${
|
|
359
|
-
<div class="chart-row"><div class="chart-label">90% Line</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${
|
|
360
|
-
<div class="chart-row"><div class="chart-label">95% Line</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${
|
|
361
|
-
<div class="chart-row"><div class="chart-label">99% Line</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${
|
|
362
|
-
<div class="chart-row"><div class="chart-label">Maximum</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: 100%;"></div></div><div class="chart-value-tag">${maxLatency.toFixed(1)} ms</div></div>
|
|
400
|
+
<div class="chart-row"><div class="chart-label">Minimum</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${(finalMetricsReport.minLatency / peakBoundary * 100).toFixed(1)}%;"></div></div><div class="chart-value-tag">${finalMetricsReport.minLatency.toFixed(1)} ms</div></div>
|
|
401
|
+
<div class="chart-row"><div class="chart-label">Average</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${(finalMetricsReport.avgLatency / peakBoundary * 100).toFixed(1)}%;"></div></div><div class="chart-value-tag">${finalMetricsReport.avgLatency.toFixed(1)} ms</div></div>
|
|
402
|
+
<div class="chart-row"><div class="chart-label">90% Line</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${(finalMetricsReport.p90 / peakBoundary * 100).toFixed(1)}%;"></div></div><div class="chart-value-tag">${finalMetricsReport.p90.toFixed(1)} ms</div></div>
|
|
403
|
+
<div class="chart-row"><div class="chart-label">95% Line</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${(finalMetricsReport.p95 / peakBoundary * 100).toFixed(1)}%;"></div></div><div class="chart-value-tag">${finalMetricsReport.p95.toFixed(1)} ms</div></div>
|
|
404
|
+
<div class="chart-row"><div class="chart-label">99% Line</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${(finalMetricsReport.p99 / peakBoundary * 100).toFixed(1)}%;"></div></div><div class="chart-value-tag">${finalMetricsReport.p99.toFixed(1)} ms</div></div>
|
|
363
405
|
</div>
|
|
364
406
|
</div>
|
|
365
407
|
|
|
366
408
|
<div>
|
|
367
409
|
<h2>\u{1F4CB} HTTP Response Matrix</h2>
|
|
368
410
|
<div class="status-matrix">
|
|
369
|
-
<div class="matrix-row"><span>Successful <span class="badge s2xx">2xx</span></span><strong>${status2xx}</strong></div>
|
|
370
|
-
<div class="matrix-row"><span>Redirects <span class="badge
|
|
371
|
-
<div class="matrix-row"><span>Client Error <span class="badge
|
|
372
|
-
<div class="matrix-row"><span>Server Error <span class="badge s5xx">5xx</span></span><strong>${status5xx}</strong></div>
|
|
411
|
+
<div class="matrix-row"><span>Successful <span class="badge s2xx">2xx</span></span><strong>${finalMetricsReport.status2xx}</strong></div>
|
|
412
|
+
<div class="matrix-row"><span>Redirects <span class="badge badge">3xx</span></span><strong>${finalMetricsReport.status3xx}</strong></div>
|
|
413
|
+
<div class="matrix-row"><span>Client Error <span class="badge badge">4xx</span></span><strong>${finalMetricsReport.status4xx}</strong></div>
|
|
414
|
+
<div class="matrix-row"><span>Server Error <span class="badge s5xx">5xx</span></span><strong>${finalMetricsReport.status5xx}</strong></div>
|
|
373
415
|
</div>
|
|
374
416
|
</div>
|
|
375
417
|
</div>
|
|
376
|
-
|
|
377
|
-
<h2>Performance Breakdown Table</h2>
|
|
378
|
-
<table>
|
|
379
|
-
<thead>
|
|
380
|
-
<tr>
|
|
381
|
-
<th>Label</th>
|
|
382
|
-
<th># Samples</th>
|
|
383
|
-
<th>Average (ms)</th>
|
|
384
|
-
<th>Min (ms)</th>
|
|
385
|
-
<th>Max (ms)</th>
|
|
386
|
-
<th>DNS Lookup</th>
|
|
387
|
-
<th>TLS Handshake</th>
|
|
388
|
-
<th>Apdex Score</th>
|
|
389
|
-
<th>Error %</th>
|
|
390
|
-
</tr>
|
|
391
|
-
</thead>
|
|
392
|
-
<tbody>
|
|
393
|
-
<tr>
|
|
394
|
-
<td>${targetScript}</td>
|
|
395
|
-
<td>${totalRequests}</td>
|
|
396
|
-
<td>${avgLatency.toFixed(1)}</td>
|
|
397
|
-
<td>${minLatency.toFixed(1)}</td>
|
|
398
|
-
<td>${maxLatency.toFixed(1)}</td>
|
|
399
|
-
<td>${avgDns.toFixed(2)} ms</td>
|
|
400
|
-
<td>${avgTls.toFixed(2)} ms</td>
|
|
401
|
-
<td>${apdexScore.toFixed(2)}</td>
|
|
402
|
-
<td>${errorRate}%</td>
|
|
403
|
-
</tr>
|
|
404
|
-
</tbody>
|
|
405
|
-
</table>
|
|
406
418
|
</div>
|
|
407
419
|
</body>
|
|
408
420
|
</html>`;
|
|
409
421
|
fs.writeFileSync(htmlOutputPath, htmlContent, "utf-8");
|
|
410
|
-
console.log(`\u2728 \x1B[32mHTML Dashboard
|
|
422
|
+
console.log(`\u2728 \x1B[32mHTML Performance Dashboard cleanly generated at:\x1B[0m ${htmlOutputPath}
|
|
411
423
|
`);
|
|
412
|
-
}
|
|
413
|
-
console.error("Execution error:", error);
|
|
414
|
-
}
|
|
424
|
+
})();
|
|
Binary file
|