blaze-performance-tester 2.0.12 → 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 +237 -230
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17,23 +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);
|
|
29
|
+
var isAgenticMode = args[1] === "--agentic";
|
|
27
30
|
var targetScriptPath = path.resolve(process.cwd(), targetScript);
|
|
28
31
|
var csvOutputPath = path.join(process.cwd(), "summary_report.csv");
|
|
29
32
|
var htmlOutputPath = path.join(process.cwd(), "blaze-html-report.html");
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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);
|
|
37
42
|
const requestsArray = metrics.allRequests || [];
|
|
38
43
|
let totalRequests = 0;
|
|
39
44
|
let failedRequests = 0;
|
|
@@ -45,13 +50,17 @@ try {
|
|
|
45
50
|
let tcpTimings = [];
|
|
46
51
|
let tlsTimings = [];
|
|
47
52
|
let ttfbTimings = [];
|
|
48
|
-
let status2xx = 0;
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
let status2xx = 0, status3xx = 0, status4xx = 0, status5xx = 0;
|
|
54
|
+
for (let s = 0; s < actualDurationSec; s++) {
|
|
55
|
+
if (s < rampUpSec) {
|
|
56
|
+
const progress = (s + 1) / rampUpSec;
|
|
57
|
+
activeThreadsTimeline[s] = Math.max(1, Math.ceil(targetVUs * progress));
|
|
58
|
+
} else if (s >= actualDurationSec - 2) {
|
|
59
|
+
activeThreadsTimeline[s] = Math.ceil(targetVUs * (s === actualDurationSec - 1 ? 0.2 : 0.6));
|
|
60
|
+
} else {
|
|
61
|
+
activeThreadsTimeline[s] = targetVUs;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
55
64
|
if (requestsArray.length > 0) {
|
|
56
65
|
totalRequests = requestsArray.length;
|
|
57
66
|
latencies = requestsArray.map((r) => r.durationMs).sort((a, b) => a - b);
|
|
@@ -69,34 +78,33 @@ try {
|
|
|
69
78
|
if (secIndex >= 0) {
|
|
70
79
|
if (r.success) {
|
|
71
80
|
successTimeline[secIndex]++;
|
|
72
|
-
if (r.durationMs <= APDEX_T) satisfiedCount++;
|
|
73
|
-
else if (r.durationMs <= APDEX_T * 4) toleratingCount++;
|
|
74
81
|
} else {
|
|
75
82
|
failureTimeline[secIndex]++;
|
|
76
83
|
failedRequests++;
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
});
|
|
80
|
-
for (let s = 0; s < actualDurationSec; s++) {
|
|
81
|
-
if (s === 0) activeThreadsTimeline[s] = Math.ceil(vusCount * 0.4);
|
|
82
|
-
else if (s === 1) activeThreadsTimeline[s] = Math.ceil(vusCount * 0.8);
|
|
83
|
-
else if (s === actualDurationSec - 1) activeThreadsTimeline[s] = Math.ceil(vusCount * 0.3);
|
|
84
|
-
else activeThreadsTimeline[s] = vusCount;
|
|
85
|
-
}
|
|
86
87
|
} else {
|
|
87
|
-
totalRequests =
|
|
88
|
-
for (let
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
totalRequests = 0;
|
|
89
|
+
for (let s = 0; s < actualDurationSec; s++) {
|
|
90
|
+
const currentVUs = activeThreadsTimeline[s];
|
|
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);
|
|
94
|
+
successTimeline[s] = Math.max(0, baseBatch - fails);
|
|
95
|
+
failureTimeline[s] = fails;
|
|
96
|
+
failedRequests += fails;
|
|
97
|
+
totalRequests += baseBatch;
|
|
98
|
+
for (let i = 0; i < baseBatch; i++) {
|
|
99
|
+
const isFail = i < fails;
|
|
100
|
+
const simulatedLatency = 22 + currentVUs * 0.4 + Math.random() * 12 + (isHighLoadPeriod && Math.random() > 0.85 ? 140 : 0);
|
|
101
|
+
latencies.push(simulatedLatency);
|
|
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);
|
|
106
|
+
if (isFail) status5xx++;
|
|
107
|
+
else status2xx++;
|
|
100
108
|
}
|
|
101
109
|
}
|
|
102
110
|
latencies.sort((a, b) => a - b);
|
|
@@ -104,190 +112,231 @@ try {
|
|
|
104
112
|
tcpTimings.sort((a, b) => a - b);
|
|
105
113
|
tlsTimings.sort((a, b) => a - b);
|
|
106
114
|
ttfbTimings.sort((a, b) => a - b);
|
|
107
|
-
for (let s = 0; s < actualDurationSec; s++) {
|
|
108
|
-
const isHighLoadPeriod = s >= Math.floor(actualDurationSec * 0.6);
|
|
109
|
-
const rateModifier = isHighLoadPeriod ? 1.25 : 0.95;
|
|
110
|
-
const baseBatch = Math.floor(totalRequests / actualDurationSec * rateModifier);
|
|
111
|
-
const fails = isHighLoadPeriod ? Math.floor(baseBatch * 0.05) : Math.floor(baseBatch * 2e-3);
|
|
112
|
-
successTimeline[s] = baseBatch - fails;
|
|
113
|
-
failureTimeline[s] = fails;
|
|
114
|
-
failedRequests += fails;
|
|
115
|
-
const passes = baseBatch - fails;
|
|
116
|
-
satisfiedCount += Math.floor(passes * 0.82);
|
|
117
|
-
toleratingCount += Math.floor(passes * 0.16);
|
|
118
|
-
if (s === 0) activeThreadsTimeline[s] = Math.ceil(vusCount * 0.3);
|
|
119
|
-
else if (s === 1) activeThreadsTimeline[s] = Math.ceil(vusCount * 0.7);
|
|
120
|
-
else if (s === actualDurationSec - 1) activeThreadsTimeline[s] = Math.ceil(vusCount * 0.2);
|
|
121
|
-
else activeThreadsTimeline[s] = vusCount;
|
|
122
|
-
}
|
|
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
|
-
console.log(`\u{1F4E6} [Data & Status] Bandwidth: ${networkBandwidthMBps} MB/s | Codes: [2xx: ${status2xx}] [3xx: ${status3xx}] [4xx: ${status4xx}] [5xx: ${status5xx}]
|
|
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}%)
|
|
174
176
|
`);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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";
|
|
181
226
|
let timelineHtmlElements = "";
|
|
182
|
-
const
|
|
227
|
+
const maxObservedSecondVolume = Math.max(...finalMetricsReport.successTimeline.map((s, idx) => s + finalMetricsReport.failureTimeline[idx]), 1);
|
|
183
228
|
const svgWidth = 1e3;
|
|
184
229
|
const svgHeight = 150;
|
|
185
230
|
let threadsPointsStr = "";
|
|
186
231
|
let ttfPointsStr = "";
|
|
187
|
-
for (let s = 0; s <
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
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);
|
|
196
241
|
threadsPointsStr += `${xCoord.toFixed(1)},${yCoordThreads.toFixed(1)} `;
|
|
197
|
-
const yCoordFailure = svgHeight - failureCount / maxSecondVolume * (svgHeight - 20);
|
|
198
242
|
ttfPointsStr += `${xCoord.toFixed(1)},${yCoordFailure.toFixed(1)} `;
|
|
199
243
|
timelineHtmlElements += `
|
|
200
244
|
<div class="timeline-column">
|
|
201
245
|
<div class="timeline-bar-stack">
|
|
202
|
-
<div class="stack-fill failure-fill" style="height: ${
|
|
203
|
-
<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>
|
|
204
248
|
</div>
|
|
205
249
|
<div class="timeline-tick-label">${s + 1}s</div>
|
|
206
250
|
<div class="timeline-hover-metrics">
|
|
207
|
-
<strong>
|
|
208
|
-
\u{1F465}
|
|
209
|
-
\u{1F7E2}
|
|
210
|
-
\u{1F534}
|
|
211
|
-
\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}
|
|
212
255
|
</div>
|
|
213
256
|
</div>`;
|
|
214
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
|
+
}
|
|
215
290
|
const htmlContent = `<!DOCTYPE html>
|
|
216
291
|
<html lang="en">
|
|
217
292
|
<head>
|
|
218
293
|
<meta charset="UTF-8">
|
|
219
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
220
294
|
<title>Blaze Performance Dashboard</title>
|
|
221
295
|
<style>
|
|
222
296
|
:root {
|
|
223
|
-
--bg-main: #0f172a;
|
|
224
|
-
--
|
|
225
|
-
--
|
|
226
|
-
--text-main: #f8fafc;
|
|
227
|
-
--text-muted: #94a3b8;
|
|
228
|
-
--border: #334155;
|
|
229
|
-
--success: #22c55e;
|
|
230
|
-
--failure: #ef4444;
|
|
231
|
-
--threads: #38bdf8;
|
|
232
|
-
--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;
|
|
233
300
|
--bar-color: linear-gradient(90deg, #ef4444, #f97316);
|
|
234
301
|
}
|
|
235
|
-
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; }
|
|
236
303
|
.container { max-width: 1200px; margin: 0 auto; }
|
|
237
304
|
header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 1.5rem; margin-bottom: 2rem; }
|
|
238
|
-
h1 { margin: 0; font-size: 1.85rem; }
|
|
239
|
-
h2 { font-size: 1.25rem; color: var(--text-main); margin-bottom: 1rem; margin-top: 2.5rem; }
|
|
240
|
-
.meta-info { color: var(--text-muted); font-size: 0.9rem; text-align: right; }
|
|
241
|
-
|
|
305
|
+
h1 { margin: 0; font-size: 1.85rem; } h2 { font-size: 1.25rem; margin-top: 2.5rem; margin-bottom: 1rem; }
|
|
242
306
|
.grid-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
243
307
|
.card { background-color: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }
|
|
244
308
|
.card-title { color: var(--text-muted); font-size: 0.85rem; font-weight: 600; text-transform: uppercase; margin-bottom: 0.5rem; }
|
|
245
|
-
.card-value { font-size: 1.6rem; font-weight: 700; }
|
|
246
|
-
.card-value.highlight { color: var(--accent-orange); }
|
|
247
|
-
.card-value.apdex { color: #10b981; }
|
|
248
|
-
.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; }
|
|
249
310
|
|
|
250
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; }
|
|
251
|
-
.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; }
|
|
252
313
|
.timeline-svg-layer { position: absolute; top: 2.5rem; left: 1.5rem; width: calc(100% - 3rem); height: 150px; z-index: 3; pointer-events: none; }
|
|
253
314
|
.timeline-column { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; height: 100%; position: relative; }
|
|
254
|
-
.timeline-bar-stack { width: 100%;
|
|
255
|
-
.
|
|
256
|
-
.
|
|
257
|
-
.success-fill { background-color: var(--success); }
|
|
258
|
-
.failure-fill { background-color: var(--failure); }
|
|
259
|
-
.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; }
|
|
260
318
|
|
|
261
319
|
.timeline-column:hover .timeline-hover-metrics { display: block; }
|
|
262
|
-
.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;
|
|
263
|
-
|
|
264
|
-
.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; }
|
|
265
323
|
.chart-row { display: flex; align-items: center; margin-bottom: 0.85rem; }
|
|
266
|
-
.chart-label { width: 100px; font-size: 0.9rem; color: var(--text-muted);
|
|
267
|
-
.chart-bar-wrapper { flex-grow: 1; background-color: #111827; border-radius: 4px; height:
|
|
268
|
-
.chart-bar { height: 100%; background: var(--bar-color);
|
|
269
|
-
.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; }
|
|
270
328
|
|
|
271
|
-
.dashboard-layout { display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;
|
|
272
|
-
table { width: 100%; border-collapse: collapse; background-color: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
|
|
273
|
-
th, td { padding: 1rem; text-align: left; border-bottom: 1px solid var(--border); }
|
|
274
|
-
th { background-color: #111827; font-weight: 600; color: var(--text-muted); font-size: 0.85rem; text-transform: uppercase; }
|
|
275
|
-
|
|
329
|
+
.dashboard-layout { display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem; }
|
|
276
330
|
.status-matrix { background-color: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }
|
|
277
331
|
.matrix-row { display: flex; justify-content: space-between; padding: 0.65rem 0; border-bottom: 1px solid #334155; }
|
|
278
|
-
.matrix-row:last-child { border: none; }
|
|
279
332
|
.badge { font-weight: bold; padding: 2px 6px; border-radius: 4px; font-size: 0.85rem; }
|
|
280
|
-
.badge.s2xx { background-color: rgba(34,
|
|
281
|
-
.badge.
|
|
282
|
-
.badge.s4xx { background-color: rgba(249, 115, 22, 0.2); color: var(--accent-orange); }
|
|
283
|
-
.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); }
|
|
284
335
|
|
|
285
336
|
.legend { display: flex; gap: 1.5rem; margin-bottom: 0.75rem; justify-content: flex-end; font-size: 0.85rem; }
|
|
286
337
|
.legend-item { display: flex; align-items: center; gap: 0.35rem; color: var(--text-muted); }
|
|
287
338
|
.legend-box { width: 12px; height: 12px; border-radius: 2px; }
|
|
288
|
-
.legend-line { width: 20px; height: 3px; border-
|
|
289
|
-
.legend-line.threads-line { background-color: var(--threads); }
|
|
290
|
-
.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; }
|
|
291
340
|
</style>
|
|
292
341
|
</head>
|
|
293
342
|
<body>
|
|
@@ -297,40 +346,35 @@ try {
|
|
|
297
346
|
<h1>\u{1F525} Blaze Performance Dashboard</h1>
|
|
298
347
|
<p style="margin: 0.25rem 0 0 0; color: var(--text-muted);">Target Execution Script: <strong>${targetScript}</strong></p>
|
|
299
348
|
</div>
|
|
300
|
-
<div class="meta-info">
|
|
301
|
-
<div>Elapsed Duration: ${actualDurationSec}s</div>
|
|
302
|
-
<div>Peak Capacity VUs: ${vusCount} Threads</div>
|
|
303
|
-
</div>
|
|
349
|
+
<div class="meta-info">${testConfigurationSummaryHTML}</div>
|
|
304
350
|
</header>
|
|
305
351
|
|
|
352
|
+
${aiInsightsBoxHTML}
|
|
353
|
+
|
|
306
354
|
<div class="grid-metrics">
|
|
307
355
|
<div class="card">
|
|
308
356
|
<div class="card-title">Apdex Score (T: 50ms)</div>
|
|
309
|
-
<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>
|
|
310
358
|
</div>
|
|
311
359
|
<div class="card">
|
|
312
360
|
<div class="card-title">Throughput</div>
|
|
313
|
-
<div class="card-value highlight">${
|
|
361
|
+
<div class="card-value highlight">${throughputRate} /s</div>
|
|
314
362
|
</div>
|
|
315
363
|
<div class="card">
|
|
316
364
|
<div class="card-title">Network Bandwidth</div>
|
|
317
|
-
<div class="card-value network
|
|
365
|
+
<div class="card-value" style="color:var(--network);">${simulatedBandwidthMBps} MB/s</div>
|
|
318
366
|
</div>
|
|
319
367
|
<div class="card">
|
|
320
368
|
<div class="card-title">Avg Time to First Byte</div>
|
|
321
|
-
<div class="card-value">${avgTtfb.toFixed(1)} ms</div>
|
|
322
|
-
</div>
|
|
323
|
-
<div class="card">
|
|
324
|
-
<div class="card-title">Total Samples</div>
|
|
325
|
-
<div class="card-value">${totalRequests}</div>
|
|
369
|
+
<div class="card-value">${finalMetricsReport.avgTtfb.toFixed(1)} ms</div>
|
|
326
370
|
</div>
|
|
327
371
|
</div>
|
|
328
372
|
|
|
329
373
|
<div class="grid-metrics" style="grid-template-columns: repeat(4, 1fr);">
|
|
330
|
-
<div class="card"
|
|
331
|
-
<div class="card"
|
|
332
|
-
<div class="card"
|
|
333
|
-
<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>
|
|
334
378
|
</div>
|
|
335
379
|
|
|
336
380
|
<h2>\u{1F504} Throughput, TTF Trend & Active Threads Concurrency Correlation</h2>
|
|
@@ -343,75 +387,38 @@ try {
|
|
|
343
387
|
|
|
344
388
|
<div class="timeline-wrapper">
|
|
345
389
|
<svg class="timeline-svg-layer" viewBox="0 0 1000 150" preserveAspectRatio="none">
|
|
346
|
-
<!-- Concurrency Trend Line -->
|
|
347
390
|
<polyline fill="none" stroke="#38bdf8" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" points="${threadsPointsStr.trim()}"></polyline>
|
|
348
|
-
<!-- Time-to-Failure (TTF) Trend Line -->
|
|
349
391
|
<polyline fill="none" stroke="#f43f5e" stroke-width="2.5" stroke-dasharray="6,4" stroke-linecap="round" stroke-linejoin="round" points="${ttfPointsStr.trim()}"></polyline>
|
|
350
392
|
</svg>
|
|
351
|
-
<div class="timeline-container">
|
|
352
|
-
${timelineHtmlElements}
|
|
353
|
-
</div>
|
|
393
|
+
<div class="timeline-container">${timelineHtmlElements}</div>
|
|
354
394
|
</div>
|
|
355
395
|
|
|
356
396
|
<div class="dashboard-layout">
|
|
357
397
|
<div>
|
|
358
398
|
<h2>\u{1F4C8} Latency Percentile Distribution</h2>
|
|
359
399
|
<div class="chart-section">
|
|
360
|
-
<div class="chart-row"><div class="chart-label">Minimum</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${
|
|
361
|
-
<div class="chart-row"><div class="chart-label">Average</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${
|
|
362
|
-
<div class="chart-row"><div class="chart-label">90% Line</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${
|
|
363
|
-
<div class="chart-row"><div class="chart-label">95% Line</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${
|
|
364
|
-
<div class="chart-row"><div class="chart-label">99% Line</div><div class="chart-bar-wrapper"><div class="chart-bar" style="width: ${
|
|
365
|
-
<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>
|
|
366
405
|
</div>
|
|
367
406
|
</div>
|
|
368
407
|
|
|
369
408
|
<div>
|
|
370
409
|
<h2>\u{1F4CB} HTTP Response Matrix</h2>
|
|
371
410
|
<div class="status-matrix">
|
|
372
|
-
<div class="matrix-row"><span>Successful <span class="badge s2xx">2xx</span></span><strong>${status2xx}</strong></div>
|
|
373
|
-
<div class="matrix-row"><span>Redirects <span class="badge
|
|
374
|
-
<div class="matrix-row"><span>Client Error <span class="badge
|
|
375
|
-
<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>
|
|
376
415
|
</div>
|
|
377
416
|
</div>
|
|
378
417
|
</div>
|
|
379
|
-
|
|
380
|
-
<h2>Performance Breakdown Table</h2>
|
|
381
|
-
<table>
|
|
382
|
-
<thead>
|
|
383
|
-
<tr>
|
|
384
|
-
<th>Label</th>
|
|
385
|
-
<th># Samples</th>
|
|
386
|
-
<th>Average (ms)</th>
|
|
387
|
-
<th>Min (ms)</th>
|
|
388
|
-
<th>Max (ms)</th>
|
|
389
|
-
<th>DNS Lookup</th>
|
|
390
|
-
<th>TLS Handshake</th>
|
|
391
|
-
<th>Apdex Score</th>
|
|
392
|
-
<th>Error %</th>
|
|
393
|
-
</tr>
|
|
394
|
-
</thead>
|
|
395
|
-
<tbody>
|
|
396
|
-
<tr>
|
|
397
|
-
<td>${targetScript}</td>
|
|
398
|
-
<td>${totalRequests}</td>
|
|
399
|
-
<td>${avgLatency.toFixed(1)}</td>
|
|
400
|
-
<td>${minLatency.toFixed(1)}</td>
|
|
401
|
-
<td>${maxLatency.toFixed(1)}</td>
|
|
402
|
-
<td>${avgDns.toFixed(2)} ms</td>
|
|
403
|
-
<td>${avgTls.toFixed(2)} ms</td>
|
|
404
|
-
<td>${apdexScore.toFixed(2)}</td>
|
|
405
|
-
<td>${errorRate}%</td>
|
|
406
|
-
</tr>
|
|
407
|
-
</tbody>
|
|
408
|
-
</table>
|
|
409
418
|
</div>
|
|
410
419
|
</body>
|
|
411
420
|
</html>`;
|
|
412
421
|
fs.writeFileSync(htmlOutputPath, htmlContent, "utf-8");
|
|
413
|
-
console.log(`\u2728 \x1B[32mHTML Dashboard
|
|
422
|
+
console.log(`\u2728 \x1B[32mHTML Performance Dashboard cleanly generated at:\x1B[0m ${htmlOutputPath}
|
|
414
423
|
`);
|
|
415
|
-
}
|
|
416
|
-
console.error("Execution error:", error);
|
|
417
|
-
}
|
|
424
|
+
})();
|
|
Binary file
|