1
+<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
2
+
3
+<!DOCTYPE html>
4
+<html lang="en">
5
+<head>
6
+ <meta charset="UTF-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <title>Netdata WebSocket Test</title>
9
+ <style>
10
+ body {
11
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
12
+ max-width: 900px;
13
+ margin: 0 auto;
14
+ padding: 20px;
15
+ line-height: 1.6;
16
+ }
17
+ .container {
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: 10px;
21
+ }
22
+ .message-box {
23
+ display: flex;
24
+ gap: 10px;
25
+ align-items: center;
26
+ }
27
+ #messageInput {
28
+ flex-grow: 1;
29
+ padding: 8px;
30
+ border: 1px solid #ccc;
31
+ border-radius: 4px;
32
+ }
33
+ button {
34
+ padding: 8px 16px;
35
+ background-color: #0078d7;
36
+ color: white;
37
+ border: none;
38
+ border-radius: 4px;
39
+ cursor: pointer;
40
+ }
41
+ button:hover {
42
+ background-color: #0063b1;
43
+ }
44
+ button:disabled {
45
+ background-color: #ccc;
46
+ cursor: not-allowed;
47
+ }
48
+ #connectionStatus {
49
+ padding: 8px;
50
+ border-radius: 4px;
51
+ margin-bottom: 10px;
52
+ }
53
+ .connected {
54
+ background-color: #d4edda;
55
+ color: #155724;
56
+ }
57
+ .disconnected {
58
+ background-color: #f8d7da;
59
+ color: #721c24;
60
+ }
61
+ .connecting {
62
+ background-color: #fff3cd;
63
+ color: #856404;
64
+ }
65
+ #messagesLog {
66
+ border: 1px solid #ddd;
67
+ padding: 10px;
68
+ border-radius: 4px;
69
+ max-height: 300px;
70
+ overflow-y: auto;
71
+ background-color: #f8f9fa;
72
+ }
73
+ .message {
74
+ margin-bottom: 5px;
75
+ padding: 5px;
76
+ border-radius: 4px;
77
+ }
78
+ .sent {
79
+ background-color: #e2f0fd;
80
+ text-align: right;
81
+ margin-left: 20%;
82
+ }
83
+ .received {
84
+ background-color: #f1f1f1;
85
+ margin-right: 20%;
86
+ }
87
+
88
+ /* Tab Styles */
89
+ .tabs {
90
+ display: flex;
91
+ border-bottom: 1px solid #ddd;
92
+ margin-bottom: 15px;
93
+ }
94
+ .tab {
95
+ padding: 10px 20px;
96
+ cursor: pointer;
97
+ border: 1px solid transparent;
98
+ border-bottom: none;
99
+ margin-right: 5px;
100
+ border-radius: 4px 4px 0 0;
101
+ }
102
+ .tab.active {
103
+ border-color: #ddd;
104
+ background-color: #fff;
105
+ border-bottom: 1px solid #fff;
106
+ margin-bottom: -1px;
107
+ font-weight: bold;
108
+ }
109
+ .tab-content {
110
+ display: none;
111
+ }
112
+ .tab-content.active {
113
+ display: block;
114
+ }
115
+
116
+ /* Progress Bar */
117
+ .progress-container {
118
+ width: 100%;
119
+ height: 20px;
120
+ background-color: #f1f1f1;
121
+ border-radius: 4px;
122
+ margin: 10px 0;
123
+ }
124
+ .progress-bar {
125
+ height: 100%;
126
+ background-color: #4caf50;
127
+ border-radius: 4px;
128
+ width: 0%;
129
+ transition: width 0.5s ease;
130
+ }
131
+
132
+ /* Stats Panel */
133
+ .stats-panel {
134
+ display: grid;
135
+ grid-template-columns: repeat(3, 1fr);
136
+ gap: 10px;
137
+ margin: 15px 0;
138
+ }
139
+ .stat-card {
140
+ background-color: #f8f9fa;
141
+ border: 1px solid #ddd;
142
+ border-radius: 4px;
143
+ padding: 10px;
144
+ text-align: center;
145
+ }
146
+ .stat-card.error {
147
+ background-color: #f8d7da;
148
+ border-color: #f5c6cb;
149
+ }
150
+ .stat-value {
151
+ font-size: 1.5em;
152
+ font-weight: bold;
153
+ margin: 5px 0;
154
+ }
155
+ .stat-label {
156
+ font-size: 0.85em;
157
+ color: #666;
158
+ }
159
+
160
+ /* Form Controls for Stress Test */
161
+ .form-group {
162
+ margin-bottom: 15px;
163
+ }
164
+ label {
165
+ display: inline-block;
166
+ margin-bottom: 5px;
167
+ font-weight: bold;
168
+ }
169
+ input[type="number"], input[type="range"] {
170
+ width: 100%;
171
+ padding: 8px;
172
+ box-sizing: border-box;
173
+ border: 1px solid #ccc;
174
+ border-radius: 4px;
175
+ }
176
+
177
+ /* Log Area */
178
+ #stressTestLog {
179
+ border: 1px solid #ddd;
180
+ padding: 10px;
181
+ border-radius: 4px;
182
+ max-height: 200px;
183
+ overflow-y: auto;
184
+ background-color: #f8f9fa;
185
+ font-family: monospace;
186
+ margin-top: 10px;
187
+ }
188
+ </style>
189
+</head>
190
+<body>
191
+ <h1>Netdata WebSocket Test Client</h1>
192
+
193
+ <!-- Common Connection Controls -->
194
+ <div class="container">
195
+ <div id="connectionStatus" class="disconnected">Disconnected</div>
196
+
197
+ <div class="message-box">
198
+ <label for="endpointInput">WebSocket URL:</label>
199
+ <input type="text" id="endpointInput" value="ws://localhost:19999/echo" style="flex-grow: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
200
+ <button id="toggleProtocolButton" title="Toggle between ws:// and wss://">Switch to wss://</button>
201
+ </div>
202
+
203
+ <div class="message-box">
204
+ <input type="checkbox" id="enableDebugMode">
205
+ <label for="enableDebugMode">Debug Mode (more verbose logging)</label>
206
+ </div>
207
+
208
+ <div class="message-box">
209
+ <button id="connectButton">Connect</button>
210
+ <button id="disconnectButton" disabled>Disconnect</button>
211
+ </div>
212
+
213
+ <div id="connectionDetails" style="margin-top: 10px; display: none; background-color: #e7f3fe; padding: 10px; border-radius: 4px;">
214
+ <div><strong>Protocol:</strong> <span id="negotiatedProtocol">-</span></div>
215
+ <div><strong>Extensions:</strong> <span id="negotiatedExtensions">-</span></div>
216
+ <div><strong>Compression:</strong> <span id="compressionStatus">Disabled</span></div>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- Tab Navigation -->
221
+ <div class="tabs">
222
+ <div class="tab active" data-tab="basicTest">Basic Testing</div>
223
+ <div class="tab" data-tab="stressTest">Stress Test</div>
224
+ </div>
225
+
226
+ <!-- Basic Test Tab -->
227
+ <div id="basicTest" class="tab-content active">
228
+ <div class="container">
229
+ <div class="message-box">
230
+ <input type="text" id="messageInput" placeholder="Type a message to send..." disabled>
231
+ <button id="sendButton" disabled>Send</button>
232
+ <button id="testLargeMessageButton" disabled>Test Compression</button>
233
+ </div>
234
+
235
+ <div id="compressionOptions" style="margin-top: 10px; display: none; background-color: #f8f9fa; padding: 10px; border-radius: 4px;">
236
+ <div style="margin-bottom: 10px;"><strong>Message Compression Characteristics:</strong></div>
237
+
238
+ <div style="display: flex; gap: 15px; margin-bottom: 10px;">
239
+ <label>
240
+ <input type="radio" name="compressionType" value="high" checked>
241
+ Highly Compressible
242
+ </label>
243
+
244
+ <label>
245
+ <input type="radio" name="compressionType" value="medium">
246
+ Medium Compression
247
+ </label>
248
+
249
+ <label>
250
+ <input type="radio" name="compressionType" value="low">
251
+ Low Compression
252
+ </label>
253
+
254
+ <label>
255
+ <input type="radio" name="compressionType" value="mixed">
256
+ Mixed Content
257
+ </label>
258
+ </div>
259
+
260
+ <div style="margin-top: 10px;">
261
+ <label style="display: flex; align-items: center; cursor: pointer;">
262
+ <input type="checkbox" id="useBinaryMode" style="margin-right: 8px;">
263
+ <span>Send as Binary Frame (use for non-UTF8 content)</span>
264
+ </label>
265
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px; margin-left: 24px;">
266
+ Use binary mode only when sending data that is not valid UTF-8 text.
267
+ For most test cases, text mode works fine.
268
+ </div>
269
+ </div>
270
+ </div>
271
+
272
+ <h3>Messages</h3>
273
+ <div id="messagesLog"></div>
274
+ </div>
275
+ </div>
276
+
277
+ <!-- Stress Test Tab -->
278
+ <div id="stressTest" class="tab-content">
279
+ <div class="container">
280
+ <h3>WebSocket Stress Tester</h3>
281
+ <p>Configure the stress test parameters below. This will send random messages for the specified duration and verify responses.</p>
282
+
283
+ <div class="form-group">
284
+ <label for="testDurationMinutes">Test Duration (minutes):</label>
285
+ <input type="number" id="testDurationMinutes" value="1" min="0.1" max="60" step="0.1">
286
+ </div>
287
+
288
+ <div class="form-group">
289
+ <label for="minMessageSize">Minimum Message Size (bytes):</label>
290
+ <input type="number" id="minMessageSize" value="100" min="10" max="50000">
291
+ </div>
292
+
293
+ <div class="form-group">
294
+ <label for="maxMessageSize">Maximum Message Size (bytes):</label>
295
+ <input type="number" id="maxMessageSize" value="10000" min="100" max="100000">
296
+ </div>
297
+
298
+ <div class="form-group">
299
+ <label for="singleSize">Use Fixed Message Size:</label>
300
+ <input type="checkbox" id="useSingleSize" style="margin-left: 10px;">
301
+ <input type="number" id="singleMessageSize" value="5000" min="100" max="100000" style="width: 120px; margin-left: 10px;">
302
+ <span style="font-size: 0.85em; color: #666; margin-left: 10px;">Check to use a consistent message size for better testing</span>
303
+ </div>
304
+
305
+ <div class="form-group">
306
+ <label for="messageFrequency">Messages per Second:</label>
307
+ <input type="range" id="messageFrequency" value="5" min="1" max="100" step="1">
308
+ <div style="display: flex; justify-content: space-between;">
309
+ <span>1</span>
310
+ <span id="messageFrequencyValue">5</span>
311
+ <span>100</span>
312
+ </div>
313
+ <div style="font-size: 0.85em; color: #0c5460; font-weight: normal; background-color: #d1ecf1; padding: 5px; border-radius: 4px; margin-top: 5px;">
314
+ NOTE: Maximum rate is limited to 100 messages/second for reliable testing,
315
+ as browsers cannot reliably process WebSocket messages at higher rates.
316
+ </div>
317
+ </div>
318
+
319
+ <div class="message-box">
320
+ <button id="startStressTestButton" disabled>Start Stress Test</button>
321
+ <button id="stopStressTestButton" disabled>Stop Test</button>
322
+ </div>
323
+
324
+ <div class="progress-container">
325
+ <div id="stressTestProgress" class="progress-bar" style="width: 0%"></div>
326
+ </div>
327
+
328
+ <div id="timeRemaining" style="text-align: center;">Ready to start</div>
329
+
330
+ <!-- Real-time Stats -->
331
+ <div class="stats-panel">
332
+ <div class="stat-card">
333
+ <div class="stat-value" id="messagesSent">0</div>
334
+ <div class="stat-label">Messages Sent</div>
335
+ </div>
336
+ <div class="stat-card">
337
+ <div class="stat-value" id="messagesReceived">0</div>
338
+ <div class="stat-label">Messages Received</div>
339
+ </div>
340
+ <div class="stat-card" id="errorCard">
341
+ <div class="stat-value" id="errorCount">0</div>
342
+ <div class="stat-label">Errors</div>
343
+ </div>
344
+ <div class="stat-card">
345
+ <div class="stat-value" id="avgLatency">0 ms</div>
346
+ <div class="stat-label">Average Latency</div>
347
+ </div>
348
+ <div class="stat-card">
349
+ <div class="stat-value" id="bytesSent">0 KB</div>
350
+ <div class="stat-label">Data Sent</div>
351
+ </div>
352
+ <div class="stat-card">
353
+ <div class="stat-value" id="bytesReceived">0 KB</div>
354
+ <div class="stat-label">Data Received</div>
355
+ </div>
356
+ </div>
357
+
358
+ <h4>Test Log</h4>
359
+ <div id="stressTestLog"></div>
360
+ </div>
361
+ </div>
362
+
363
+ <script>
364
+ // Tab switching functionality
365
+ document.querySelectorAll('.tab').forEach(tab => {
366
+ tab.addEventListener('click', () => {
367
+ // Remove active class from all tabs and content
368
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
369
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
370
+
371
+ // Add active class to clicked tab and its content
372
+ tab.classList.add('active');
373
+ const tabContentId = tab.getAttribute('data-tab');
374
+ document.getElementById(tabContentId).classList.add('active');
375
+ });
376
+ });
377
+
378
+ // Elements - Common
379
+ const connectButton = document.getElementById('connectButton');
380
+ const disconnectButton = document.getElementById('disconnectButton');
381
+ const connectionStatus = document.getElementById('connectionStatus');
382
+ const endpointInput = document.getElementById('endpointInput');
383
+ const toggleProtocolButton = document.getElementById('toggleProtocolButton');
384
+ const enableCompression = { checked: true }; // Default to enabled
385
+ const enableDebugMode = document.getElementById('enableDebugMode');
386
+ const connectionDetails = document.getElementById('connectionDetails');
387
+ const negotiatedProtocol = document.getElementById('negotiatedProtocol');
388
+ const negotiatedExtensions = document.getElementById('negotiatedExtensions');
389
+ const compressionStatus = document.getElementById('compressionStatus');
390
+
391
+ // Elements - Basic Test Tab
392
+ const messageInput = document.getElementById('messageInput');
393
+ const sendButton = document.getElementById('sendButton');
394
+ const testLargeMessageButton = document.getElementById('testLargeMessageButton');
395
+ const messagesLog = document.getElementById('messagesLog');
396
+
397
+ // Elements - Stress Test Tab
398
+ const startStressTestButton = document.getElementById('startStressTestButton');
399
+ const stopStressTestButton = document.getElementById('stopStressTestButton');
400
+ const testDurationMinutes = document.getElementById('testDurationMinutes');
401
+ const minMessageSize = document.getElementById('minMessageSize');
402
+ const maxMessageSize = document.getElementById('maxMessageSize');
403
+ const useSingleSize = document.getElementById('useSingleSize');
404
+ const singleMessageSize = document.getElementById('singleMessageSize');
405
+ const messageFrequency = document.getElementById('messageFrequency');
406
+ const messageFrequencyValue = document.getElementById('messageFrequencyValue');
407
+ const stressTestProgress = document.getElementById('stressTestProgress');
408
+ const timeRemaining = document.getElementById('timeRemaining');
409
+ const stressTestLog = document.getElementById('stressTestLog');
410
+ const errorCard = document.getElementById('errorCard');
411
+
412
+ // Stats elements
413
+ const messagesSentElement = document.getElementById('messagesSent');
414
+ const messagesReceivedElement = document.getElementById('messagesReceived');
415
+ const errorCountElement = document.getElementById('errorCount');
416
+ const avgLatencyElement = document.getElementById('avgLatency');
417
+ const bytesSentElement = document.getElementById('bytesSent');
418
+ const bytesReceivedElement = document.getElementById('bytesReceived');
419
+
420
+ // Update frequency slider value display
421
+ messageFrequency.addEventListener('input', () => {
422
+ messageFrequencyValue.textContent = messageFrequency.value;
423
+ });
424
+
425
+ // WebSocket connection
426
+ let socket = null;
427
+
428
+ // Connect to the WebSocket server
429
+ connectButton.addEventListener('click', () => {
430
+ // Get the URL from the input field
431
+ const url = endpointInput.value.trim();
432
+ if (!url) {
433
+ alert("Please enter a valid WebSocket URL");
434
+ return;
435
+ }
436
+
437
+ // Validate the URL has a WebSocket protocol
438
+ if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
439
+ alert("WebSocket URL must start with ws:// or wss://");
440
+ return;
441
+ }
442
+
443
+ connectionStatus.textContent = 'Connecting to ' + url;
444
+ connectionStatus.className = 'connecting';
445
+ connectionDetails.style.display = 'none';
446
+ addMessageToLog('Connecting to: ' + url, 'system');
447
+
448
+ try {
449
+ // Create WebSocket connection with netdata-json protocol
450
+ const protocols = [];
451
+ socket = new WebSocket(url, protocols);
452
+ addMessageToLog('WebSocket connection created', 'system');
453
+
454
+ // Connection opened
455
+ socket.addEventListener('open', (event) => {
456
+ connectionStatus.textContent = 'Connected';
457
+ connectionStatus.className = 'connected';
458
+
459
+ // Enable all connection-dependent controls
460
+ connectButton.disabled = true;
461
+ disconnectButton.disabled = false;
462
+ messageInput.disabled = false;
463
+ sendButton.disabled = false;
464
+ testLargeMessageButton.disabled = false;
465
+ startStressTestButton.disabled = false;
466
+
467
+ // Display connection details
468
+ connectionDetails.style.display = 'block';
469
+ negotiatedProtocol.textContent = socket.protocol || 'none';
470
+
471
+ const extensions = socket.extensions || 'none';
472
+ negotiatedExtensions.textContent = extensions;
473
+
474
+ // Check if compression was negotiated
475
+ if (extensions.includes('permessage-deflate')) {
476
+ compressionStatus.textContent = 'Enabled (permessage-deflate)';
477
+ compressionStatus.style.color = '#155724';
478
+ } else {
479
+ compressionStatus.textContent = 'Disabled';
480
+ compressionStatus.style.color = '#721c24';
481
+ }
482
+
483
+ addMessageToLog(`Connection established (Protocol: ${socket.protocol}, Extensions: ${extensions})`, 'system');
484
+ });
485
+
486
+ // Track message size for network analysis
487
+ let lastMessageSentInfo = null;
488
+
489
+ // Listen for messages
490
+ socket.addEventListener('message', (event) => {
491
+ const message = event.data;
492
+ const receiveTime = performance.now();
493
+
494
+ // First check if this is a stress test message
495
+ if (stressTestRunning && typeof message === 'string' && message.startsWith('MSG-')) {
496
+ const handled = processStressTestResponse(message);
497
+ if (handled) return; // Skip normal message processing if it was a stress test message
498
+ }
499
+
500
+ // Get transfer size from event if available
501
+ let transferSize = 0;
502
+
503
+ // Check if this is a response to a benchmark test
504
+ if (window.currentBenchmark && !window.currentBenchmark.responseComplete) {
505
+ // Update benchmark data
506
+ const benchmark = window.currentBenchmark;
507
+
508
+ if (!benchmark.lastResponseTime) {
509
+ // First response packet
510
+ benchmark.lastResponseTime = receiveTime;
511
+ const firstPacketLatency = (receiveTime - benchmark.sendTime).toFixed(2);
512
+ addMessageToLog(`First response received after ${firstPacketLatency}ms`, 'system');
513
+ }
514
+
515
+ // Handle different data types (string or blob)
516
+ const processResponse = (responseData) => {
517
+ // Track bytes received
518
+ benchmark.receivedBytes = responseData.length;
519
+
520
+ // Check if response is equal to what we sent
521
+ // For compressed responses, the protocol should preserve the exact content
522
+ const dataMatches = (responseData === benchmark.originalMessage);
523
+
524
+ // Check if response seems complete - always do the verification
525
+ // regardless of the content to ensure we check data integrity
526
+ if (responseData && responseData.length > 0) {
527
+ benchmark.responseComplete = true;
528
+ const totalTime = (receiveTime - benchmark.sendTime).toFixed(2);
529
+
530
+ // Data verification results
531
+ let verificationResults = '';
532
+ if (dataMatches) {
533
+ verificationResults = `<div style="color: #155724; margin-top: 5px;"><strong>✓ Data Integrity:</strong> Response content matches sent data exactly!</div>`;
534
+ } else {
535
+ // If sizes match but content differs
536
+ if (responseData.length === benchmark.originalMessage.length) {
537
+ verificationResults = `<div style="color: #721c24; margin-top: 5px;"><strong>❌ Data Integrity:</strong> Response size matches (${responseData.length} bytes) but content differs!</div>`;
538
+
539
+ // Add some diagnostic info to help understand the mismatch
540
+ // Find the first point of difference
541
+ let diffPos = -1;
542
+ for (let i = 0; i < responseData.length; i++) {
543
+ if (responseData[i] !== benchmark.originalMessage[i]) {
544
+ diffPos = i;
545
+ break;
546
+ }
547
+ }
548
+
549
+ if (diffPos >= 0) {
550
+ const sentContext = benchmark.originalMessage.substring(
551
+ Math.max(0, diffPos - 10),
552
+ Math.min(benchmark.originalMessage.length, diffPos + 10)
553
+ );
554
+ const recvContext = responseData.substring(
555
+ Math.max(0, diffPos - 10),
556
+ Math.min(responseData.length, diffPos + 10)
557
+ );
558
+
559
+ verificationResults += `<div style="color: #721c24; margin-top: 5px;">
560
+ First difference at position ${diffPos}:<br>
561
+ Sent: "${sentContext}" (char code: ${benchmark.originalMessage.charCodeAt(diffPos)})<br>
562
+ Recv: "${recvContext}" (char code: ${responseData.charCodeAt(diffPos)})
563
+ </div>`;
564
+ }
565
+ } else {
566
+ verificationResults = `<div style="color: #721c24; margin-top: 5px;"><strong>❌ Data Integrity:</strong> Response content differs! Sent: ${benchmark.originalMessage.length} bytes, Received: ${responseData.length} bytes</div>`;
567
+ }
568
+ }
569
+
570
+ // Calculate compression ratio if we know the original size
571
+ let compressionInfo = "";
572
+ if (benchmark.expectedBytes > 0 && responseData.length > 0) {
573
+ // Check if compression extension was actually negotiated
574
+ const usingCompression = negotiatedExtensions.textContent.includes("permessage-deflate");
575
+
576
+ const compressionRatio = (benchmark.expectedBytes / responseData.length).toFixed(2);
577
+ const percentReduction = ((1 - (responseData.length / benchmark.expectedBytes)) * 100).toFixed(2);
578
+
579
+ // Use color formatting based on compression effectiveness
580
+ const compressionColor = percentReduction > 50 ? '#155724' : (percentReduction > 20 ? '#856404' : '#721c24');
581
+
582
+ compressionInfo = `
583
+ <div style="margin-top: 5px;">
584
+ <strong>Compression Stats:</strong>
585
+ <ul style="margin-top: 5px; margin-bottom: 5px;">
586
+ <li>Original size: ${(benchmark.expectedBytes / 1024).toFixed(2)} KB</li>
587
+ <li>Size in JavaScript: ${(responseData.length / 1024).toFixed(2)} KB <span style="color: #856404">(Browser auto-decompressed the data)</span></li>
588
+ <li>WebSocket extensions: ${usingCompression ?
589
+ '<span style="color: #155724">permessage-deflate enabled (compression happens at protocol level)</span>' :
590
+ '<span style="color: #721c24">compression not enabled! Check server configuration.</span>'}</li>
591
+ <li style="color: #155724">Note: The server is correctly compressing the data (99.5% reduction), but the browser automatically decompresses it before JavaScript receives it</li>
592
+ </ul>
593
+ </div>
594
+ `;
595
+ }
596
+
597
+ // Calculate throughput
598
+ const kbPerSecond = ((benchmark.receivedBytes / 1024) / (totalTime / 1000)).toFixed(2);
599
+
600
+ addMessageToLog(
601
+ `Benchmark complete: ${benchmark.messageSizeKB} KB message echoed back in ${totalTime}ms (${kbPerSecond} KB/s)${verificationResults}${compressionInfo}`,
602
+ 'system',
603
+ true
604
+ );
605
+
606
+ // Store this message info for display
607
+ lastMessageSentInfo = {
608
+ sent: benchmark.expectedBytes,
609
+ received: responseData.length,
610
+ dataMatches: dataMatches
611
+ };
612
+
613
+ // Reset benchmark
614
+ window.currentBenchmark = null;
615
+ }
616
+ };
617
+
618
+ // Check if the response is a Blob (binary data)
619
+ if (message instanceof Blob) {
620
+ addMessageToLog(`Received response as BINARY data (${message.size} bytes)`, 'system');
621
+
622
+ // Handle binary data by reading it as text for comparison
623
+ const reader = new FileReader();
624
+ reader.onload = function() {
625
+ // Convert binary to text for comparison
626
+ const responseText = reader.result;
627
+ addMessageToLog(`Successfully converted binary response to text for comparison (${responseText.length} chars)`, 'system');
628
+ processResponse(responseText);
629
+ };
630
+ reader.onerror = function() {
631
+ addMessageToLog(`Error: Failed to read binary response data for verification`, 'system');
632
+ // Still try to process what we can
633
+ benchmark.responseComplete = true;
634
+ const totalTime = (receiveTime - benchmark.sendTime).toFixed(2);
635
+ addMessageToLog(
636
+ `Benchmark complete: ${benchmark.messageSizeKB} KB message echoed back in ${totalTime}ms as binary data, but verification failed`,
637
+ 'system',
638
+ true
639
+ );
640
+ };
641
+ reader.readAsText(message);
642
+ } else {
643
+ // It's already text
644
+ addMessageToLog(`Received response as TEXT data (${message.length} chars)`, 'system');
645
+ processResponse(message);
646
+ }
647
+ }
648
+
649
+ // Add compression info to received message if available
650
+ let receivedInfo = '';
651
+ if (lastMessageSentInfo && lastMessageSentInfo.sent > 0) {
652
+ // For binary blobs we use the size property, for string we use length
653
+ const receivedLength = message instanceof Blob ? message.size : message.length;
654
+
655
+ if (receivedLength > 0 && receivedLength !== lastMessageSentInfo.sent) {
656
+ const compressionRatio = (lastMessageSentInfo.sent / receivedLength).toFixed(2);
657
+ const percentReduction = ((1 - (receivedLength / lastMessageSentInfo.sent)) * 100).toFixed(2);
658
+
659
+ receivedInfo = ` | Compressed: ${(receivedLength / 1024).toFixed(2)} KB (${percentReduction}% smaller than sent)`;
660
+ }
661
+
662
+ // Add data integrity info if available
663
+ if (lastMessageSentInfo.hasOwnProperty('dataMatches')) {
664
+ receivedInfo += ` | Data Integrity: ${lastMessageSentInfo.dataMatches ? '✓ Match' : '❌ Mismatch'}`;
665
+ }
666
+
667
+ // Reset after use
668
+ lastMessageSentInfo = null;
669
+ }
670
+
671
+ // Log the message - if it's a blob, show that in a friendly way
672
+ if (message instanceof Blob) {
673
+ addMessageToLog(`[Binary data: ${message.size} bytes]`, 'received', false, receivedInfo);
674
+ } else {
675
+ addMessageToLog(message, 'received', false, receivedInfo);
676
+ }
677
+ });
678
+
679
+ // Connection closed
680
+ socket.addEventListener('close', (event) => {
681
+ connectionStatus.textContent = 'Disconnected';
682
+ connectionStatus.className = 'disconnected';
683
+ connectionDetails.style.display = 'none';
684
+
685
+ // Disable all connection-dependent controls
686
+ connectButton.disabled = false;
687
+ disconnectButton.disabled = true;
688
+ messageInput.disabled = true;
689
+ sendButton.disabled = true;
690
+ testLargeMessageButton.disabled = true;
691
+ startStressTestButton.disabled = true;
692
+
693
+ // Stop any running stress test
694
+ if (stressTestRunning) {
695
+ stopStressTest();
696
+ addStressTestLog('Test stopped due to WebSocket disconnection', 'error');
697
+ }
698
+
699
+ let closeReason = '';
700
+ let recommendedAction = '';
701
+
702
+ // Try to give a friendly description of common close codes
703
+ switch (event.code) {
704
+ case 1000:
705
+ closeReason = "Normal closure";
706
+ recommendedAction = "This is a clean shutdown, no action needed.";
707
+ break;
708
+ case 1001:
709
+ closeReason = "Going away";
710
+ recommendedAction = "The server or browser is navigating away from the page.";
711
+ break;
712
+ case 1002:
713
+ closeReason = "Protocol error";
714
+ recommendedAction = "Check the messages sent - may indicate a problem with message format or headers.";
715
+ break;
716
+ case 1003:
717
+ closeReason = "Unsupported data";
718
+ recommendedAction = "The server couldn't process the data type sent.";
719
+ break;
720
+ case 1005:
721
+ closeReason = "No status code";
722
+ recommendedAction = "Connection closed without a proper code (abnormal).";
723
+ break;
724
+ case 1006:
725
+ closeReason = "Abnormal closure";
726
+ recommendedAction = "Connection was closed unexpectedly. Check server logs or network connectivity.";
727
+ break;
728
+ case 1007:
729
+ closeReason = "Invalid frame payload data";
730
+ recommendedAction = "Message contained invalid data format, possibly not valid UTF-8 text.";
731
+ break;
732
+ case 1008:
733
+ closeReason = "Policy violation";
734
+ recommendedAction = "Server policy was violated. Check message rate or authentication.";
735
+ break;
736
+ case 1009:
737
+ closeReason = "Message too big";
738
+ recommendedAction = "Try reducing your message size or using fragmentation.";
739
+ break;
740
+ case 1010:
741
+ closeReason = "Missing extension";
742
+ recommendedAction = "Client requested an extension the server doesn't support.";
743
+ break;
744
+ case 1011:
745
+ closeReason = "Internal error";
746
+ recommendedAction = "Server encountered an unexpected error. Check server logs.";
747
+ break;
748
+ case 1012:
749
+ closeReason = "Service restart";
750
+ recommendedAction = "The server is restarting, try reconnecting in a moment.";
751
+ break;
752
+ case 1013:
753
+ closeReason = "Try again later";
754
+ recommendedAction = "Server is temporarily unavailable, try reconnecting later.";
755
+ break;
756
+ case 1014:
757
+ closeReason = "Bad gateway";
758
+ recommendedAction = "A gateway or proxy received an invalid response from the upstream server.";
759
+ break;
760
+ case 1015:
761
+ closeReason = "TLS handshake failure";
762
+ recommendedAction = "Check your SSL/TLS configuration and certificates.";
763
+ break;
764
+ // Netdata specific codes (4000+)
765
+ case 4000:
766
+ closeReason = "Netdata: Client timeout";
767
+ recommendedAction = "The connection was inactive for too long.";
768
+ break;
769
+ case 4001:
770
+ closeReason = "Netdata: Server shutdown";
771
+ recommendedAction = "The Netdata server is shutting down.";
772
+ break;
773
+ case 4002:
774
+ closeReason = "Netdata: Connection rejected";
775
+ recommendedAction = "The server rejected the connection (check authorization).";
776
+ break;
777
+ case 4003:
778
+ closeReason = "Netdata: Rate limit exceeded";
779
+ recommendedAction = "You've exceeded the message rate limit. Reduce message frequency.";
780
+ break;
781
+ default:
782
+ closeReason = "Unknown";
783
+ recommendedAction = "Unrecognized close code - check server logs for details.";
784
+ break;
785
+ }
786
+
787
+ // Add detailed information to the log
788
+ let closeMessage = `Connection closed (code: ${event.code} - ${closeReason})`;
789
+ if (event.reason) {
790
+ closeMessage += `\nReason: ${event.reason}`;
791
+ }
792
+
793
+ // Add recommendation if available
794
+ if (recommendedAction) {
795
+ closeMessage += `\nRecommended action: ${recommendedAction}`;
796
+ }
797
+
798
+ // Check for common error conditions
799
+ if (event.code === 1006) {
800
+ // Add more detailed debugging advice for abnormal closures
801
+ closeMessage += "\n\nThis error often occurs when:";
802
+ closeMessage += "\n- The server crashed or was stopped";
803
+ closeMessage += "\n- Network connectivity issues occurred";
804
+ closeMessage += "\n- A browser timeout occurred due to inactivity";
805
+ closeMessage += "\n- Invalid WebSocket headers were sent";
806
+ } else if (event.code === 1009) {
807
+ // For message too big errors
808
+ closeMessage += "\n\nTo fix message size issues:";
809
+ closeMessage += "\n- Break large messages into smaller chunks";
810
+ closeMessage += "\n- Consider using binary mode for large messages";
811
+ closeMessage += "\n- Check the server's max message size configuration";
812
+ }
813
+
814
+ addMessageToLog(closeMessage, 'system');
815
+
816
+ // If we have pending messages during disconnect, log the details
817
+ if (pendingMessages.size > 0) {
818
+ addMessageToLog(`Warning: ${pendingMessages.size} messages were still pending when the connection closed`, 'system');
819
+ }
820
+ });
821
+
822
+ // Connection error
823
+ socket.addEventListener('error', (error) => {
824
+ connectionStatus.textContent = 'Connection Error';
825
+ connectionStatus.className = 'disconnected';
826
+ connectionDetails.style.display = 'none';
827
+
828
+ // Stop any running stress test
829
+ if (stressTestRunning) {
830
+ stopStressTest();
831
+ addStressTestLog('Test stopped due to WebSocket error', 'error');
832
+ }
833
+
834
+ // Log details about the error
835
+ const errorDetails = `WebSocket error occurred during ${stressTestRunning ? 'stress test' : 'normal operation'}`;
836
+ addMessageToLog(errorDetails, 'system');
837
+
838
+ // Check the type of error and provide more helpful information
839
+ if (error instanceof Event && error.target) {
840
+ // WebSocket error events don't contain much useful information
841
+ // Add troubleshooting advice
842
+ let troubleshooting = "\nPossible reasons for WebSocket errors:";
843
+ troubleshooting += "\n- Network connectivity issues";
844
+ troubleshooting += "\n- Server unavailable or incorrect URL";
845
+ troubleshooting += "\n- Cross-origin issues (CORS)";
846
+ troubleshooting += "\n- Too many simultaneous connections";
847
+
848
+ // Add specific advice for stress testing
849
+ if (stressTestRunning) {
850
+ troubleshooting += "\n\nFor stress test issues:";
851
+ troubleshooting += "\n- Reduce message frequency";
852
+ troubleshooting += "\n- Reduce message size";
853
+ troubleshooting += "\n- Check for 'invalid frame header' errors in the browser console";
854
+ troubleshooting += "\n- Try using smaller message bursts";
855
+ }
856
+
857
+ addMessageToLog(troubleshooting, 'system');
858
+ }
859
+
860
+ addMessageToLog('Check the browser console (F12 > Console tab) for more error details', 'system');
861
+ console.error('WebSocket error:', error);
862
+ });
863
+ } catch (error) {
864
+ connectionStatus.textContent = 'Connection Failed';
865
+ connectionStatus.className = 'disconnected';
866
+ console.error('Failed to create WebSocket:', error);
867
+ }
868
+ });
869
+
870
+ // Disconnect from the WebSocket server
871
+ disconnectButton.addEventListener('click', () => {
872
+ if (socket) {
873
+ socket.close();
874
+ socket = null;
875
+ }
876
+ });
877
+
878
+ // Send message
879
+ sendButton.addEventListener('click', sendMessage);
880
+ messageInput.addEventListener('keypress', (event) => {
881
+ if (event.key === 'Enter') {
882
+ sendMessage();
883
+ }
884
+ });
885
+
886
+ // Show compression options when hovering over test button
887
+ testLargeMessageButton.addEventListener('mouseenter', () => {
888
+ document.getElementById('compressionOptions').style.display = 'block';
889
+ });
890
+
891
+ // Test compression with large message
892
+ testLargeMessageButton.addEventListener('click', () => {
893
+ if (socket && socket.readyState === WebSocket.OPEN) {
894
+ // Prompt for message size
895
+ const sizePrompt = prompt("Enter approximate message size in KB (10-1000):", "100");
896
+ if (!sizePrompt) return;
897
+
898
+ const sizeKB = parseInt(sizePrompt);
899
+ if (isNaN(sizeKB) || sizeKB < 10 || sizeKB > 1000) {
900
+ alert("Please enter a valid size between 10 and 1000 KB");
901
+ return;
902
+ }
903
+
904
+ // Create a large message with realistic data that has varying compression characteristics
905
+ // This will be a better test of real-world compression performance
906
+
907
+ // Random compression efficiency for this test
908
+ const compressionType = document.querySelector('input[name="compressionType"]:checked')?.value || 'mixed';
909
+
910
+ // Calculate target size in bytes
911
+ const targetBytes = sizeKB * 1024;
912
+
913
+ // Whether to send as binary (true) or text (false)
914
+ // For the test page specifically, we're generating text data
915
+ // We'll only use binary mode for demonstration if explicitly selected by user
916
+ let sendAsBinary = false;
917
+
918
+ // Add UI to let user choose text/binary mode
919
+ const binaryModeCheckbox = document.getElementById('useBinaryMode');
920
+ if (binaryModeCheckbox && binaryModeCheckbox.checked) {
921
+ sendAsBinary = true;
922
+ }
923
+
924
+ // Create the message based on compression type selected
925
+ let largeMessage = "This is a test message to verify WebSocket compression. ";
926
+
927
+ switch (compressionType) {
928
+ case 'high':
929
+ // Highly compressible - repeating text blocks
930
+ const baseText = "This is a highly compressible repeating pattern. ";
931
+ while (largeMessage.length < targetBytes) {
932
+ largeMessage += baseText;
933
+ }
934
+ break;
935
+
936
+ case 'medium':
937
+ // Medium compressibility - structured data with some repetition
938
+ // This is valid UTF-8 JSON-like data that compresses moderately well
939
+ while (largeMessage.length < targetBytes) {
940
+ largeMessage += `{"id":${Math.floor(Math.random() * 1000)},"name":"user${Math.floor(Math.random() * 100)}","timestamp":${Date.now()},"value":${Math.random()},"status":"active"},`;
941
+ }
942
+ break;
943
+
944
+ case 'low':
945
+ // Low compressibility - random data
946
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
947
+ while (largeMessage.length < targetBytes) {
948
+ largeMessage += chars.charAt(Math.floor(Math.random() * chars.length));
949
+ }
950
+ break;
951
+
952
+ case 'mixed':
953
+ default:
954
+ // Define character set for random data (also defined in 'low' case)
955
+ const mixedChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
956
+
957
+ // Mixed - combination of different patterns
958
+ const sections = [
959
+ // Highly compressible section (20%)
960
+ Array(Math.floor(targetBytes * 0.2 / 50)).fill("AAAABBBBBCCCCCDDDDDEEEEEFFFFF").join(""),
961
+
962
+ // Moderately compressible section (40%)
963
+ Array(Math.floor(targetBytes * 0.4 / 100)).fill().map(() =>
964
+ `{"id":${Math.floor(Math.random() * 100)},"data":"value"}`
965
+ ).join(","),
966
+
967
+ // Barely compressible section (40%)
968
+ Array(Math.floor(targetBytes * 0.4)).fill().map(() =>
969
+ mixedChars.charAt(Math.floor(Math.random() * mixedChars.length))
970
+ ).join("")
971
+ ];
972
+
973
+ largeMessage += sections.join("");
974
+ break;
975
+ }
976
+
977
+ // Trim to exact size if needed
978
+ if (largeMessage.length > targetBytes) {
979
+ largeMessage = largeMessage.substring(0, targetBytes);
980
+ }
981
+
982
+ // Show message size
983
+ const actualSizeKB = (largeMessage.length / 1024).toFixed(2);
984
+ addMessageToLog(`Sending large message (${actualSizeKB} KB) to test compression...`, 'system');
985
+
986
+ // Measure time before sending
987
+ const startTime = performance.now();
988
+
989
+ // Send the message explicitly as a BINARY frame for large compressed data
990
+ // This prevents UTF-8 validation issues with random/compressed data
991
+ if (sendAsBinary) {
992
+ // Convert string to binary blob for proper binary WebSocket frame
993
+ // This ensures it's sent as opcode=2 (BINARY) not opcode=1 (TEXT)
994
+ const binaryData = new Blob([largeMessage]);
995
+ socket.send(binaryData);
996
+ addMessageToLog(`Sent data as Blob in BINARY mode to avoid UTF-8 validation issues`, 'system');
997
+ } else {
998
+ socket.send(largeMessage); // Default TEXT mode
999
+ addMessageToLog(`Sent data in TEXT mode (must be valid UTF-8)`, 'system');
1000
+ }
1001
+
1002
+ // Record send completion time
1003
+ const endTime = performance.now();
1004
+ const sendDuration = (endTime - startTime).toFixed(2);
1005
+
1006
+ // Show first part of message with timing info
1007
+ addMessageToLog(
1008
+ `${largeMessage.substring(0, 50)}... [message truncated, full length: ${largeMessage.length} bytes, send time: ${sendDuration}ms]`,
1009
+ 'sent'
1010
+ );
1011
+
1012
+ // Set up tracking for response time
1013
+ const responseBenchmark = {
1014
+ messageSizeKB: actualSizeKB,
1015
+ sendTime: startTime,
1016
+ lastResponseTime: null,
1017
+ receivedBytes: 0,
1018
+ expectedBytes: largeMessage.length,
1019
+ originalMessage: largeMessage, // Store original message for verification
1020
+ responseComplete: false
1021
+ };
1022
+
1023
+ // Store the benchmark data for the message listener to use
1024
+ window.currentBenchmark = responseBenchmark;
1025
+
1026
+ // Log whether this is sent as text or binary for verification purposes
1027
+ if (sendAsBinary) {
1028
+ addMessageToLog(`Verification info: Message sent as BINARY (Blob) format, will convert back for comparison`, 'system');
1029
+ } else {
1030
+ addMessageToLog(`Verification info: Message sent as TEXT format, expecting TEXT response`, 'system');
1031
+ }
1032
+ }
1033
+ });
1034
+
1035
+ // Track if we're currently in a benchmark test
1036
+ window.currentBenchmark = null;
1037
+
1038
+ // Stress test state variables
1039
+ let stressTestRunning = false;
1040
+ let stressTestStartTime = null;
1041
+ let stressTestEndTime = null;
1042
+ let stressTestInterval = null;
1043
+ let stressTestTimer = null;
1044
+ let pendingMessages = new Map(); // Map of messageId -> message data
1045
+ const MAX_PENDING_MESSAGES = 500; // Limit concurrent pending messages to prevent browser overload
1046
+
1047
+ // Stats tracking
1048
+ let stressTestStats = {
1049
+ messagesSent: 0,
1050
+ messagesReceived: 0,
1051
+ errorCount: 0,
1052
+ totalLatency: 0,
1053
+ bytesSent: 0,
1054
+ bytesReceived: 0,
1055
+ lastUpdateTime: 0,
1056
+ // Per-second stats
1057
+ messagesPerSecond: 0,
1058
+ bytesPerSecond: 0,
1059
+ latencyPerSecond: 0
1060
+ };
1061
+
1062
+ // Function to generate random string of specified length with variable compression efficiency
1063
+ function generateRandomString(length) {
1064
+ // Message identifier prefix
1065
+ const prefix = `MSG-${Date.now()}-${Math.random().toString(36).substring(2, 8)}-`;
1066
+ let result = prefix;
1067
+
1068
+ // Calculate remaining length
1069
+ const remainingLength = length - prefix.length;
1070
+
1071
+ // Determine the compression pattern type for this message
1072
+ // This will create a mix of highly compressible, moderately compressible, and nearly incompressible messages
1073
+ const patternType = Math.floor(Math.random() * 5);
1074
+
1075
+ switch(patternType) {
1076
+ case 0:
1077
+ // Highly compressible - repeating pattern (simulates repetitive data)
1078
+ const pattern1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
1079
+ const repeatedPattern = pattern1.repeat(Math.ceil(remainingLength / pattern1.length));
1080
+ result += repeatedPattern.substring(0, remainingLength);
1081
+ break;
1082
+
1083
+ case 1:
1084
+ // Moderately compressible - English-like text with common words
1085
+ // This simulates natural language which has moderate compression
1086
+ const words = ["the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog",
1087
+ "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing",
1088
+ "elit", "sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore",
1089
+ "et", "dolore", "magna", "aliqua"];
1090
+ let text = "";
1091
+ while (text.length < remainingLength) {
1092
+ text += words[Math.floor(Math.random() * words.length)] + " ";
1093
+ }
1094
+ result += text.substring(0, remainingLength);
1095
+ break;
1096
+
1097
+ case 2:
1098
+ // JSON-like data with repeated keys but varying values
1099
+ // Simulates structured data which compresses moderately well
1100
+ let jsonData = "";
1101
+ const keys = ["id", "name", "value", "timestamp", "status"];
1102
+ while (jsonData.length < remainingLength) {
1103
+ jsonData += `{"${keys[Math.floor(Math.random() * keys.length)]}":"${Math.random().toString(36).substring(2, 8)}",`;
1104
+ jsonData += `"${keys[Math.floor(Math.random() * keys.length)]}":${Math.floor(Math.random() * 1000)},`;
1105
+ jsonData += `"${keys[Math.floor(Math.random() * keys.length)]}":"${Math.random() > 0.5 ? "true" : "false"}"},`;
1106
+ }
1107
+ result += jsonData.substring(0, remainingLength);
1108
+ break;
1109
+
1110
+ case 3:
1111
+ // Nearly incompressible - random data
1112
+ // This simulates already compressed or encrypted data
1113
+ let randomData = "";
1114
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1115
+ for (let i = 0; i < remainingLength; i++) {
1116
+ randomData += chars.charAt(Math.floor(Math.random() * chars.length));
1117
+ }
1118
+ result += randomData;
1119
+ break;
1120
+
1121
+ case 4:
1122
+ // Binary-like data with some patterns
1123
+ // This simulates mixed binary data which has varying compression efficiency
1124
+ let binaryData = "";
1125
+ for (let i = 0; i < remainingLength; i++) {
1126
+ // Mix of patterns and random data
1127
+ if (i % 16 < 8) {
1128
+ // Pattern part
1129
+ binaryData += String.fromCharCode((i % 256));
1130
+ } else {
1131
+ // Random part
1132
+ binaryData += String.fromCharCode(Math.floor(Math.random() * 256));
1133
+ }
1134
+ }
1135
+ result += binaryData;
1136
+ break;
1137
+ }
1138
+
1139
+ return result;
1140
+ }
1141
+
1142
+ // Start stress test
1143
+ function startStressTest() {
1144
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
1145
+ addStressTestLog('ERROR: WebSocket not connected', 'error');
1146
+ return;
1147
+ }
1148
+
1149
+ // Get test parameters
1150
+ const duration = parseFloat(testDurationMinutes.value);
1151
+ const minSize = parseInt(minMessageSize.value);
1152
+ const maxSize = parseInt(maxMessageSize.value);
1153
+ const frequency = parseInt(messageFrequency.value);
1154
+
1155
+ // Validate parameters
1156
+ if (isNaN(duration) || duration <= 0) {
1157
+ alert('Invalid test duration. Please enter a positive number.');
1158
+ return;
1159
+ }
1160
+ if (isNaN(minSize) || minSize <= 0) {
1161
+ alert('Invalid minimum message size. Please enter a positive number.');
1162
+ return;
1163
+ }
1164
+ if (isNaN(maxSize) || maxSize <= 0 || maxSize < minSize) {
1165
+ alert('Invalid maximum message size. Please enter a number greater than the minimum size.');
1166
+ return;
1167
+ }
1168
+ if (isNaN(frequency) || frequency <= 0) {
1169
+ alert('Invalid message frequency. Please enter a positive number.');
1170
+ return;
1171
+ }
1172
+
1173
+ // Calculate test end time
1174
+ stressTestStartTime = Date.now();
1175
+ stressTestEndTime = stressTestStartTime + (duration * 60 * 1000);
1176
+
1177
+ // Reset statistics
1178
+ stressTestStats = {
1179
+ messagesSent: 0,
1180
+ messagesReceived: 0,
1181
+ errorCount: 0,
1182
+ totalLatency: 0,
1183
+ bytesSent: 0,
1184
+ bytesReceived: 0,
1185
+ lastUpdateTime: Date.now(),
1186
+ messagesPerSecond: 0,
1187
+ bytesPerSecond: 0,
1188
+ latencyPerSecond: 0
1189
+ };
1190
+
1191
+ // Clear pending messages map
1192
+ pendingMessages.clear();
1193
+
1194
+ // Reset UI
1195
+ messagesSentElement.textContent = '0';
1196
+ messagesReceivedElement.textContent = '0';
1197
+ errorCountElement.textContent = '0';
1198
+ avgLatencyElement.textContent = '0 ms';
1199
+ bytesSentElement.textContent = '0 KB';
1200
+ bytesReceivedElement.textContent = '0 KB';
1201
+ errorCard.classList.remove('error');
1202
+ stressTestProgress.style.width = '0%';
1203
+
1204
+ // Clear log
1205
+ stressTestLog.innerHTML = '';
1206
+
1207
+ // Log test start
1208
+ addStressTestLog(`Starting stress test with params: duration=${duration}min, size=${minSize}-${maxSize} bytes, freq=${frequency} msg/s`);
1209
+
1210
+ // Update UI
1211
+ startStressTestButton.disabled = true;
1212
+ stopStressTestButton.disabled = false;
1213
+ testDurationMinutes.disabled = true;
1214
+ minMessageSize.disabled = true;
1215
+ maxMessageSize.disabled = true;
1216
+ useSingleSize.disabled = true;
1217
+ singleMessageSize.disabled = true;
1218
+ messageFrequency.disabled = true;
1219
+ stressTestRunning = true;
1220
+
1221
+ // Set up intervals for sending messages and updating UI
1222
+ // Simple approach with no batching - one message per interval at all frequencies
1223
+ const sendInterval = Math.floor(1000 / frequency);
1224
+
1225
+ addStressTestLog(`Using standard timing mode: 1 message every ${sendInterval}ms`, 'info');
1226
+
1227
+ // Single message per interval for all frequencies
1228
+ stressTestInterval = setInterval(() => {
1229
+ // Only send if we don't have too many pending messages
1230
+ if (pendingMessages.size < MAX_PENDING_MESSAGES) {
1231
+ sendStressTestMessage(minSize, maxSize);
1232
+ } else {
1233
+ addStressTestLog(`Auto-throttling: ${pendingMessages.size} pending messages`, 'warning');
1234
+ }
1235
+ }, sendInterval);
1236
+
1237
+ // Update timer every second
1238
+ stressTestTimer = setInterval(updateStressTestTimer, 1000);
1239
+
1240
+ // Update initial time display
1241
+ updateStressTestTimer();
1242
+ }
1243
+
1244
+ // Stop stress test
1245
+ function stopStressTest(showSummary = true) {
1246
+ if (!stressTestRunning) return;
1247
+
1248
+ // Clear intervals
1249
+ clearInterval(stressTestInterval);
1250
+ clearInterval(stressTestTimer);
1251
+
1252
+ // Update UI
1253
+ startStressTestButton.disabled = false;
1254
+ stopStressTestButton.disabled = true;
1255
+ testDurationMinutes.disabled = false;
1256
+ minMessageSize.disabled = false;
1257
+ maxMessageSize.disabled = false;
1258
+ useSingleSize.disabled = false;
1259
+ singleMessageSize.disabled = false;
1260
+ messageFrequency.disabled = false;
1261
+ timeRemaining.textContent = 'Test stopped';
1262
+ stressTestRunning = false;
1263
+
1264
+ // Show summary if requested
1265
+ if (showSummary) {
1266
+ const testDuration = Math.floor((Date.now() - stressTestStartTime) / 1000);
1267
+ const avgLatency = stressTestStats.messagesReceived > 0 ?
1268
+ Math.round(stressTestStats.totalLatency / stressTestStats.messagesReceived) : 0;
1269
+ const totalBytesSent = (stressTestStats.bytesSent / 1024).toFixed(2);
1270
+ const totalBytesReceived = (stressTestStats.bytesReceived / 1024).toFixed(2);
1271
+ const throughputSent = testDuration > 0 ?
1272
+ (stressTestStats.bytesSent / testDuration / 1024).toFixed(2) : 0;
1273
+ const throughputReceived = testDuration > 0 ?
1274
+ (stressTestStats.bytesReceived / testDuration / 1024).toFixed(2) : 0;
1275
+
1276
+ // Check for errors - messages that weren't properly echoed back
1277
+ const unacknowledgedMessages = pendingMessages.size;
1278
+ if (unacknowledgedMessages > 0) {
1279
+ stressTestStats.errorCount += unacknowledgedMessages;
1280
+ errorCountElement.textContent = stressTestStats.errorCount;
1281
+ errorCard.classList.add('error');
1282
+ addStressTestLog(`WARNING: ${unacknowledgedMessages} messages were not acknowledged`, 'error');
1283
+ }
1284
+
1285
+ // Add summary to log
1286
+ addStressTestLog('------------------------');
1287
+ addStressTestLog('TEST SUMMARY:');
1288
+ addStressTestLog(`Test duration: ${testDuration} seconds`);
1289
+ addStressTestLog(`Messages sent: ${stressTestStats.messagesSent}`);
1290
+ addStressTestLog(`Messages received: ${stressTestStats.messagesReceived}`);
1291
+ addStressTestLog(`Errors: ${stressTestStats.errorCount}`);
1292
+ addStressTestLog(`Average latency: ${avgLatency} ms`);
1293
+ addStressTestLog(`Data sent: ${totalBytesSent} KB (${throughputSent} KB/s)`);
1294
+ addStressTestLog(`Data received: ${totalBytesReceived} KB (${throughputReceived} KB/s)`);
1295
+
1296
+ // Check success rate
1297
+ const successRate = stressTestStats.messagesSent > 0 ?
1298
+ Math.round((stressTestStats.messagesReceived / stressTestStats.messagesSent) * 100) : 0;
1299
+ addStressTestLog(`Success rate: ${successRate}%`);
1300
+
1301
+ if (successRate < 100) {
1302
+ addStressTestLog('Some messages were not correctly echoed back!', 'error');
1303
+ } else if (stressTestStats.errorCount === 0) {
1304
+ addStressTestLog('Test completed successfully with no errors!', 'success');
1305
+ }
1306
+ }
1307
+
1308
+ // Clear any pending messages
1309
+ pendingMessages.clear();
1310
+ }
1311
+
1312
+ // Send a stress test message
1313
+ function sendStressTestMessage(minSize, maxSize) {
1314
+ if (!stressTestRunning || !socket || socket.readyState !== WebSocket.OPEN) {
1315
+ return;
1316
+ }
1317
+
1318
+ // Check if test should end
1319
+ if (Date.now() >= stressTestEndTime) {
1320
+ stopStressTest();
1321
+ return;
1322
+ }
1323
+
1324
+ // Check if we need to throttle due to too many pending messages
1325
+ if (pendingMessages.size >= MAX_PENDING_MESSAGES) {
1326
+ addStressTestLog(`Throttling: ${pendingMessages.size} pending messages`, 'warning');
1327
+ return;
1328
+ }
1329
+
1330
+ // Generate message size based on user settings
1331
+ let messageSize;
1332
+
1333
+ if (useSingleSize.checked) {
1334
+ // Use fixed size if that option is selected
1335
+ messageSize = parseInt(singleMessageSize.value);
1336
+ if (isNaN(messageSize) || messageSize < 100 || messageSize > 100000) {
1337
+ messageSize = 5000; // Default if invalid
1338
+ }
1339
+ } else {
1340
+ // Generate random size within range, but cap at 32KB to reduce fragmentation issues
1341
+ const safeMaxSize = Math.min(maxSize, 32768);
1342
+ messageSize = Math.floor(Math.random() * (safeMaxSize - minSize + 1)) + minSize;
1343
+ }
1344
+
1345
+ // Generate random message with unique ID embedded at the start
1346
+ const message = generateRandomString(messageSize);
1347
+ const messageId = message.substring(0, message.indexOf('-', 4) + 1); // Extract the unique prefix
1348
+
1349
+ try {
1350
+ // Record send time and track message
1351
+ const sendTime = Date.now();
1352
+ pendingMessages.set(messageId, {
1353
+ id: messageId,
1354
+ sentTime: sendTime,
1355
+ size: message.length,
1356
+ received: false
1357
+ });
1358
+
1359
+ // Log large messages that may trigger fragmentation
1360
+ if (message.length > 10000) {
1361
+ addStressTestLog(`Sending large message (${message.length} bytes) - may be fragmented by browser`, 'warning');
1362
+ }
1363
+
1364
+ // Debug logs for detailed tracking
1365
+ addStressTestLog(`Sending message ${messageId} (${message.length} bytes)`, 'debug');
1366
+
1367
+ // Send the message
1368
+ socket.send(message);
1369
+
1370
+ // Update stats
1371
+ stressTestStats.messagesSent++;
1372
+ stressTestStats.bytesSent += message.length;
1373
+ messagesSentElement.textContent = stressTestStats.messagesSent;
1374
+ bytesSentElement.textContent = (stressTestStats.bytesSent / 1024).toFixed(2) + ' KB';
1375
+ } catch (error) {
1376
+ console.error('Error sending message:', error);
1377
+ stressTestStats.errorCount++;
1378
+ errorCountElement.textContent = stressTestStats.errorCount;
1379
+ errorCard.classList.add('error');
1380
+
1381
+ addStressTestLog(`ERROR sending message: ${error.message}`, 'error');
1382
+ }
1383
+ }
1384
+
1385
+ // Update stress test timer and progress
1386
+ function updateStressTestTimer() {
1387
+ if (!stressTestRunning) return;
1388
+
1389
+ const now = Date.now();
1390
+ const elapsed = now - stressTestStartTime;
1391
+ const total = stressTestEndTime - stressTestStartTime;
1392
+ const remaining = Math.max(0, stressTestEndTime - now);
1393
+
1394
+ // Update progress bar
1395
+ const progressPercent = Math.min(100, (elapsed / total) * 100);
1396
+ stressTestProgress.style.width = `${progressPercent}%`;
1397
+
1398
+ // Update time remaining
1399
+ const minutes = Math.floor(remaining / 60000);
1400
+ const seconds = Math.floor((remaining % 60000) / 1000);
1401
+ timeRemaining.textContent = `Time remaining: ${minutes}:${seconds.toString().padStart(2, '0')}`;
1402
+
1403
+ // Update real-time stats (every second)
1404
+ const secondsSinceLastUpdate = (now - stressTestStats.lastUpdateTime) / 1000;
1405
+ if (secondsSinceLastUpdate >= 1) {
1406
+ // Calculate per-second rates
1407
+ stressTestStats.messagesPerSecond = Math.round(
1408
+ (stressTestStats.messagesSent - stressTestStats.messagesPerSecond) / secondsSinceLastUpdate
1409
+ );
1410
+ stressTestStats.bytesPerSecond = Math.round(
1411
+ (stressTestStats.bytesSent - stressTestStats.bytesPerSecond) / secondsSinceLastUpdate
1412
+ );
1413
+
1414
+ // Update last update time
1415
+ stressTestStats.lastUpdateTime = now;
1416
+ }
1417
+
1418
+ // Check for test completion
1419
+ if (remaining <= 0) {
1420
+ stopStressTest();
1421
+ }
1422
+ }
1423
+
1424
+ // Add a log entry to the stress test log
1425
+ function addStressTestLog(message, type = 'info', forceLog = false) {
1426
+ // Skip debug messages unless debug mode is enabled or forceLog is true
1427
+ if (type === 'debug' && !enableDebugMode.checked && !forceLog) {
1428
+ return;
1429
+ }
1430
+
1431
+ const logEntry = document.createElement('div');
1432
+ logEntry.className = `log-entry ${type}`;
1433
+
1434
+ // Add timestamp
1435
+ const timestamp = new Date().toLocaleTimeString();
1436
+
1437
+ // Format log entry
1438
+ logEntry.textContent = `[${timestamp}] ${message}`;
1439
+
1440
+ // Add styles based on type
1441
+ if (type === 'error') {
1442
+ logEntry.style.color = '#dc3545';
1443
+ } else if (type === 'success') {
1444
+ logEntry.style.color = '#28a745';
1445
+ } else if (type === 'warning') {
1446
+ logEntry.style.color = '#ffc107';
1447
+ } else if (type === 'debug') {
1448
+ logEntry.style.color = '#6c757d';
1449
+ logEntry.style.fontSize = '0.9em';
1450
+ }
1451
+
1452
+ // Add to log and scroll to bottom
1453
+ stressTestLog.appendChild(logEntry);
1454
+ stressTestLog.scrollTop = stressTestLog.scrollHeight;
1455
+ }
1456
+
1457
+ // Verify and process stress test response
1458
+ function processStressTestResponse(message) {
1459
+ // Try to extract the message ID from the response
1460
+ const messageId = message.substring(0, message.indexOf('-', 4) + 1);
1461
+
1462
+ // Debug log for message receipt
1463
+ addStressTestLog(`Received message with ID ${messageId} (${message.length} bytes)`, 'debug');
1464
+
1465
+ // Check if this is a response to a tracked message
1466
+ if (pendingMessages.has(messageId)) {
1467
+ const pendingMessage = pendingMessages.get(messageId);
1468
+ const receiveTime = Date.now();
1469
+ const latency = receiveTime - pendingMessage.sentTime;
1470
+
1471
+ // Verify the message content
1472
+ if (message.length < 20) {
1473
+ // Message is severely truncated/corrupted
1474
+ stressTestStats.errorCount++;
1475
+ errorCountElement.textContent = stressTestStats.errorCount;
1476
+ errorCard.classList.add('error');
1477
+ addStressTestLog(`ERROR: Received severely truncated message: ${messageId}, length=${message.length}`, 'error');
1478
+ }
1479
+ else if (message !== pendingMessage.id && message.indexOf('-', 4) !== pendingMessage.id.indexOf('-', 4)) {
1480
+ // The message ID structure appears to be corrupted - this could indicate decompression issues
1481
+ addStressTestLog(`WARNING: Message ID structure changed: original=${pendingMessage.id}, received=${messageId}`, 'warning');
1482
+ }
1483
+
1484
+ // Update stats
1485
+ stressTestStats.messagesReceived++;
1486
+ stressTestStats.bytesReceived += message.length;
1487
+ stressTestStats.totalLatency += latency;
1488
+
1489
+ // Record original message size for statistics (no discrepancy logging)
1490
+ const originalSize = pendingMessage.size;
1491
+
1492
+ // Update UI
1493
+ messagesReceivedElement.textContent = stressTestStats.messagesReceived;
1494
+ bytesReceivedElement.textContent = (stressTestStats.bytesReceived / 1024).toFixed(2) + ' KB';
1495
+ const avgLatency = Math.round(stressTestStats.totalLatency / stressTestStats.messagesReceived);
1496
+ avgLatencyElement.textContent = avgLatency + ' ms';
1497
+
1498
+ // Every 50 messages, log current stats to help with debugging
1499
+ if (stressTestStats.messagesReceived % 50 === 0) {
1500
+ addStressTestLog(`Progress: ${stressTestStats.messagesReceived}/${stressTestStats.messagesSent} messages, avg latency: ${avgLatency}ms`);
1501
+ }
1502
+
1503
+ // Remove from pending messages
1504
+ pendingMessages.delete(messageId);
1505
+
1506
+ return true;
1507
+ }
1508
+
1509
+ return false;
1510
+ }
1511
+
1512
+ function sendMessage() {
1513
+ const message = messageInput.value.trim();
1514
+ if (message && socket && socket.readyState === WebSocket.OPEN) {
1515
+ socket.send(message);
1516
+ addMessageToLog(message, 'sent');
1517
+ messageInput.value = '';
1518
+ }
1519
+ }
1520
+
1521
+ // Add message to the log
1522
+ function addMessageToLog(message, type, isHtml = false, additionalInfo = '') {
1523
+ const messageElement = document.createElement('div');
1524
+ messageElement.className = `message ${type}`;
1525
+
1526
+ // Handle different message types
1527
+ if (type === 'system') {
1528
+ // System messages can contain HTML if specified
1529
+ if (isHtml) {
1530
+ messageElement.innerHTML = message;
1531
+ } else {
1532
+ messageElement.textContent = message;
1533
+ }
1534
+ messageElement.style.backgroundColor = '#fff3cd';
1535
+ messageElement.style.color = '#856404';
1536
+ messageElement.style.fontStyle = 'italic';
1537
+ }
1538
+ else if (type === 'received' || type === 'sent') {
1539
+ // For regular messages, check size
1540
+ if (message.length > 1000) {
1541
+ // For large messages, create a collapsible view
1542
+ const messageSizeKB = (message.length / 1024).toFixed(2);
1543
+
1544
+ // Create summary with expand button
1545
+ const summary = document.createElement('div');
1546
+ summary.innerHTML = `
1547
+ <span class="message-preview">${message.substring(0, 500)}...</span>
1548
+ <div class="message-info">
1549
+ Message length: ${message.length} bytes (${messageSizeKB} KB)${additionalInfo}
1550
+ <button class="toggle-button">Show Full Message</button>
1551
+ </div>
1552
+ `;
1553
+
1554
+ // Create content div (initially hidden)
1555
+ const content = document.createElement('div');
1556
+ content.className = 'full-message';
1557
+ content.style.display = 'none';
1558
+ content.style.maxHeight = '300px';
1559
+ content.style.overflow = 'auto';
1560
+ content.style.border = '1px solid #ddd';
1561
+ content.style.marginTop = '5px';
1562
+ content.style.padding = '5px';
1563
+ content.textContent = message;
1564
+
1565
+ // Add toggle functionality
1566
+ const toggleButton = summary.querySelector('.toggle-button');
1567
+ toggleButton.style.marginLeft = '10px';
1568
+ toggleButton.style.padding = '2px 5px';
1569
+ toggleButton.style.fontSize = '0.8em';
1570
+ toggleButton.addEventListener('click', function() {
1571
+ if (content.style.display === 'none') {
1572
+ content.style.display = 'block';
1573
+ this.textContent = 'Hide Full Message';
1574
+ } else {
1575
+ content.style.display = 'none';
1576
+ this.textContent = 'Show Full Message';
1577
+ }
1578
+ });
1579
+
1580
+ // Add elements to message container
1581
+ messageElement.appendChild(summary);
1582
+ messageElement.appendChild(content);
1583
+ } else {
1584
+ // Normal sized message, show in full with any additional info
1585
+ if (additionalInfo) {
1586
+ const wrapper = document.createElement('div');
1587
+
1588
+ // Add the message text
1589
+ const messageText = document.createElement('div');
1590
+ messageText.textContent = message;
1591
+ wrapper.appendChild(messageText);
1592
+
1593
+ // Add additional info
1594
+ const infoText = document.createElement('div');
1595
+ infoText.style.fontSize = '0.85em';
1596
+ infoText.style.color = '#666';
1597
+ infoText.style.marginTop = '3px';
1598
+ infoText.textContent = additionalInfo.trim();
1599
+ wrapper.appendChild(infoText);
1600
+
1601
+ messageElement.appendChild(wrapper);
1602
+ } else {
1603
+ messageElement.textContent = message;
1604
+ }
1605
+ }
1606
+ }
1607
+
1608
+ // Add timestamp
1609
+ const timestamp = new Date().toLocaleTimeString();
1610
+ const timeElement = document.createElement('span');
1611
+ timeElement.className = 'timestamp';
1612
+ timeElement.textContent = timestamp;
1613
+ timeElement.style.fontSize = '0.8em';
1614
+ timeElement.style.color = '#666';
1615
+ timeElement.style.marginRight = '5px';
1616
+ timeElement.style.fontWeight = 'bold';
1617
+
1618
+ // For HTML content, we need to insert differently
1619
+ if (type === 'system' && isHtml) {
1620
+ // Create a container for the timestamp
1621
+ const timestampContainer = document.createElement('div');
1622
+ timestampContainer.appendChild(timeElement);
1623
+
1624
+ // Prepend the timestamp container
1625
+ messageElement.prepend(timestampContainer);
1626
+ } else {
1627
+ messageElement.prepend(timeElement);
1628
+ }
1629
+
1630
+ // Add to log and scroll to bottom
1631
+ messagesLog.appendChild(messageElement);
1632
+ messagesLog.scrollTop = messagesLog.scrollHeight;
1633
+ }
1634
+
1635
+ // Toggle between ws:// and wss:// protocols
1636
+ toggleProtocolButton.addEventListener('click', () => {
1637
+ const currentUrl = endpointInput.value.trim();
1638
+
1639
+ if (currentUrl.startsWith('ws://')) {
1640
+ // Switch from ws:// to wss://
1641
+ endpointInput.value = currentUrl.replace('ws://', 'wss://');
1642
+ toggleProtocolButton.textContent = 'Switch to ws://';
1643
+ } else if (currentUrl.startsWith('wss://')) {
1644
+ // Switch from wss:// to ws://
1645
+ endpointInput.value = currentUrl.replace('wss://', 'ws://');
1646
+ toggleProtocolButton.textContent = 'Switch to wss://';
1647
+ } else {
1648
+ // Invalid URL, add ws:// prefix
1649
+ endpointInput.value = 'ws://' + currentUrl;
1650
+ toggleProtocolButton.textContent = 'Switch to wss://';
1651
+ }
1652
+ });
1653
+
1654
+ // Stress test button handlers
1655
+ startStressTestButton.addEventListener('click', startStressTest);
1656
+ stopStressTestButton.addEventListener('click', () => stopStressTest(true));
1657
+ </script>
1658
+</body>
1659
+</html>
\ No newline at end of file