master
html 1,659 lines 80.7 KB
Raw
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>