From 11222c70bd29d98d00cde8a44e05f1fc64645ab1 Mon Sep 17 00:00:00 2001 From: MathieuSevignyLavallee <89943988+MathieuSevignyLavallee@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:04:22 -0500 Subject: [PATCH] not finished --- quizRoom/app.ts | 6 +- quizRoom/socket/setupWebSocket.ts | 63 ++- test/stressTest/class/student.js | 27 +- test/stressTest/class/teacher.js | 32 +- test/stressTest/main.js | 191 +++++-- test/stressTest/metrics/metrics_report.json | 18 - test/stressTest/metrics_report_1731719315629 | 18 - test/stressTest/metrics_report_1731719370035 | 18 - test/stressTest/package-lock.json | 549 ++++++++++++++++++- test/stressTest/package.json | 1 + test/stressTest/utility/apiServices.js | 124 +++-- test/stressTest/utility/writeMetrics.js | 233 +++++++- 12 files changed, 1080 insertions(+), 200 deletions(-) delete mode 100644 test/stressTest/metrics/metrics_report.json delete mode 100644 test/stressTest/metrics_report_1731719315629 delete mode 100644 test/stressTest/metrics_report_1731719370035 diff --git a/quizRoom/app.ts b/quizRoom/app.ts index 0e40adc..81347b4 100644 --- a/quizRoom/app.ts +++ b/quizRoom/app.ts @@ -2,7 +2,8 @@ import http from "http"; import { Server, ServerOptions } from "socket.io"; import { setupWebsocket } from "./socket/setupWebSocket"; import dotenv from "dotenv"; -import express from 'express'; +import express from "express"; +import os from "os"; // Import the os module // Load environment variables dotenv.config(); @@ -36,6 +37,7 @@ app.get('/health', (_, res) => { } }); + const ioOptions: Partial = { path: `/api/room/${roomId}/socket`, cors: { @@ -52,4 +54,4 @@ setupWebsocket(io); server.listen(port, () => { console.log(`WebSocket server is running on port ${port}`); -}); \ No newline at end of file +}); diff --git a/quizRoom/socket/setupWebSocket.ts b/quizRoom/socket/setupWebSocket.ts index d5e2ab0..a5ea04c 100644 --- a/quizRoom/socket/setupWebSocket.ts +++ b/quizRoom/socket/setupWebSocket.ts @@ -1,4 +1,5 @@ import { Server, Socket } from "socket.io"; +import os from "os"; const MAX_USERS_PER_ROOM = 60; const MAX_TOTAL_CONNECTIONS = 2000; @@ -19,10 +20,10 @@ export const setupWebsocket = (io: Server): void => { socket.on("create-room", (sentRoomName) => { // Ensure sentRoomName is a string before applying toUpperCase() - const roomName = (typeof sentRoomName === "string" && sentRoomName.trim() !== "") - ? sentRoomName.toUpperCase() + const roomName = (typeof sentRoomName === "string" && sentRoomName.trim() !== "") + ? sentRoomName.toUpperCase() : generateRoomName(); - + if (!io.sockets.adapter.rooms.get(roomName)) { socket.join(roomName); socket.emit("create-success", roomName); @@ -96,10 +97,62 @@ export const setupWebsocket = (io: Server): void => { socket.on("error", (error) => { console.error("WebSocket server error:", error); }); + + + // Stress Testing + + socket.on("message-test", ({ roomName, message }: { roomName: string; message: string }) => { + console.log(`Message reçu dans la salle ${roomName} : ${message}`); + socket.to(roomName).emit("message", { id: socket.id, message }); + }); + + socket.on("get-usage", () => { + try { + const memoryUsage = process.memoryUsage(); + const cpuUsage = process.cpuUsage(); + const totalMemory = os.totalmem(); + const freeMemory = os.freemem(); + const loadAverage = os.loadavg(); // Load average over 1, 5, and 15 minutes + + // Calculate CPU usage percentage + const userCpuPercentage = ((cpuUsage.user / 1e6) / os.cpus().length).toFixed(2); // in % + const systemCpuPercentage = ((cpuUsage.system / 1e6) / os.cpus().length).toFixed(2); // in % + + const usageData = { + memory: { + total: totalMemory, + free: freeMemory, + rss: memoryUsage.rss, + heapTotal: memoryUsage.heapTotal, + heapUsed: memoryUsage.heapUsed, + external: memoryUsage.external, + usagePercentage: (((totalMemory - freeMemory) / totalMemory) * 100).toFixed(2), // % used + }, + cpu: { + user: cpuUsage.user, + system: cpuUsage.system, + userPercentage: userCpuPercentage, + systemPercentage: systemCpuPercentage, + }, + system: { + uptime: os.uptime(), // System uptime in seconds + loadAverage: { + "1min": loadAverage[0], + "5min": loadAverage[1], + "15min": loadAverage[2], + }, + }, + }; + + socket.emit("usage-data", usageData); // Send usage data back to the client + } catch (error) { + console.error("Error getting usage data:", error); + socket.emit("error", { message: "Failed to retrieve usage data" }); + } + }); + }); - - const generateRoomName = (length = 6): string => { const characters = "0123456789"; let result = ""; diff --git a/test/stressTest/class/student.js b/test/stressTest/class/student.js index 890b425..639d17c 100644 --- a/test/stressTest/class/student.js +++ b/test/stressTest/class/student.js @@ -19,19 +19,20 @@ export class Student { reconnectionDelay: 10000, timeout: 20000, }); - + this.socket.on('connect', () => { this.joinRoom(this.roomName, this.username); - resolve(this.socket); + this.listenForMessages(); // Start listening for messages + resolve(this.socket); }); - + this.socket.on('error', (error) => { reject(new Error(`Connection error: ${error.message}`)); }); - + } catch (error) { - console.error(`Error connecting ${this.name} to room ${this.roomId}:`, error.message); - reject(error); + console.error(`Error connecting ${this.name} to room ${this.roomName}:`, error.message); + reject(error); } }); } @@ -41,4 +42,18 @@ export class Student { this.socket.emit('join-room', { roomName, username }); } } + + sendMessage(message) { + if (this.socket && this.socket.connected) { + this.socket.emit('message-test', { room: this.roomName, message }); + } + } + + listenForMessages() { + if (this.socket) { + this.socket.on('message-test', (data) => { + console.log(`Message received in room ${this.roomName} by ${this.username}:`, data.message); + }); + } + } } diff --git a/test/stressTest/class/teacher.js b/test/stressTest/class/teacher.js index 0f895c1..6660e50 100644 --- a/test/stressTest/class/teacher.js +++ b/test/stressTest/class/teacher.js @@ -19,23 +19,19 @@ export class Teacher { reconnectionDelay: 10000, timeout: 20000, }); - + this.socket.on('connect', () => { this.createRoom(this.roomName); - resolve(this.socket); + this.listenForMessages(); // Start listening for messages + resolve(this.socket); }); - + this.socket.on('error', (error) => { - reject(new Error(`Connection error: ${error.message}`)); + reject(new Error(`Connection error: ${error.message}`)); }); - - this.socket.on('create-success', () => { - - }); - } catch (error) { - console.error(`Error connecting ${this.name} to room ${this.roomId}:`, error.message); - reject(error); + console.error(`Error connecting ${this.name} to room ${this.roomName}:`, error.message); + reject(error); } }); } @@ -45,4 +41,18 @@ export class Teacher { this.socket.emit('create-room', this.roomName || undefined); } } + + sendMessage(message) { + if (this.socket && this.socket.connected) { + this.socket.emit('message-test', { room: this.roomName, message }); + } + } + + listenForMessages() { + if (this.socket) { + this.socket.on('message-test', (data) => { + console.log(`Message received in room ${this.roomName} by ${this.username}:`, data.message); + }); + } + } } diff --git a/test/stressTest/main.js b/test/stressTest/main.js index f5c925a..f9a1d78 100644 --- a/test/stressTest/main.js +++ b/test/stressTest/main.js @@ -1,16 +1,15 @@ -import { attemptLoginOrRegister, createRoomContainer } from './utility/apiServices.js'; +import { attemptLoginOrRegister, createRoomContainer, captureResourceUsageForContainers } from './utility/apiServices.js'; import { Student } from './class/student.js'; import { Teacher } from './class/teacher.js'; -import { writeMetricsToFile } from './utility/writeMetrics.js'; +import { writeMetricsToFile, generateGraphs } from './utility/writeMetrics.js'; const BASE_URL = 'http://localhost'; const user = { username: 'admin@example.com', password: 'adminPassword' }; -const numberRooms = 5; -const studentPerRoom = 59; // Max : 60, 1 place réservée pour le professeur +const numberRooms = 20; +const studentPerRoom = 58; // Max: 60, 1 reserved for teacher const roomAssociations = {}; -const allSockets = []; // Suivi de toutes les connexions WebSocket actives +const allSockets = []; // Tracks all active WebSocket connections -// Métriques const metrics = { roomsCreated: 0, roomsFailed: 0, @@ -18,133 +17,211 @@ const metrics = { teachersFailed: 0, studentsConnected: 0, studentsFailed: 0, + messagesSent: 0, + messagesReceived: 0, + totalLatency: 0, + maxLatency: 0, + minLatency: Number.MAX_SAFE_INTEGER, + throughput: 0, startTime: null, endTime: null, }; +// Creates rooms and tracks their creation async function createRoomContainers(token) { - console.time('Temps de création des salles'); + console.time('Room creation time'); const roomCreationPromises = Array.from({ length: numberRooms }, async () => { - const room = await createRoomContainer(BASE_URL, token); - if (room?.id) { - roomAssociations[room.id] = { teacher: null, students: [] }; - metrics.roomsCreated++; - console.log(`Salle créée avec l'ID : ${room.id}`); - } else { + try { + const room = await createRoomContainer(BASE_URL, token); + if (room?.id) { + roomAssociations[room.id] = { teacher: null, students: [] }; + metrics.roomsCreated++; + console.log(`Room created with ID: ${room.id}`); + } + } catch { metrics.roomsFailed++; - console.warn('Échec de la création d’une salle.'); + console.warn('Failed to create a room.'); } }); await Promise.allSettled(roomCreationPromises); - console.timeEnd('Temps de création des salles'); - console.log(`Nombre total de salles créées : ${Object.keys(roomAssociations).length}`); + console.timeEnd('Room creation time'); + console.log(`Total rooms created: ${Object.keys(roomAssociations).length}`); } +// Connects teachers to their respective rooms async function addAndConnectTeachers() { - console.time('Temps de connexion des enseignants'); - const teacherCreationPromises = Object.keys(roomAssociations).map(async (roomId, index) => { + console.time('Teacher connection time'); + const teacherPromises = Object.keys(roomAssociations).map(async (roomId, index) => { const teacher = new Teacher(`teacher_${index}`, roomId); const start = Date.now(); const socket = await teacher.connectToRoom(BASE_URL); const latency = Date.now() - start; + metrics.totalLatency += latency; + metrics.maxLatency = Math.max(metrics.maxLatency, latency); + metrics.minLatency = Math.min(metrics.minLatency, latency); + if (socket.connected) { allSockets.push(socket); roomAssociations[roomId].teacher = teacher; metrics.teachersConnected++; - console.log(`Enseignant ${teacher.username} connecté à la salle ${roomId}. Latence : ${latency}ms`); + console.log(`Teacher ${teacher.username} connected to room ${roomId}. Latency: ${latency}ms`); } else { metrics.teachersFailed++; - console.warn(`Échec de la connexion de l'enseignant ${index} à la salle ${roomId}`); + console.warn(`Failed to connect teacher ${index} to room ${roomId}`); } }); - await Promise.allSettled(teacherCreationPromises); - console.timeEnd('Temps de connexion des enseignants'); - console.log('Tous les enseignants ont été ajoutés et connectés à leurs salles respectives.'); + await Promise.allSettled(teacherPromises); + console.timeEnd('Teacher connection time'); + console.log('All teachers connected to their respective rooms.'); } +// Connects students to their respective rooms async function addAndConnectStudents() { - console.time('Temps de connexion des étudiants'); - const studentCreationPromises = Object.entries(roomAssociations).flatMap(([roomId, association], roomIndex) => + console.time('Student connection time'); + const studentPromises = Object.entries(roomAssociations).flatMap(([roomId, association], roomIndex) => Array.from({ length: studentPerRoom }, async (_, i) => { const student = new Student(`student_${roomIndex}_${i}`, roomId); const start = Date.now(); const socket = await student.connectToRoom(BASE_URL); const latency = Date.now() - start; + metrics.totalLatency += latency; + metrics.maxLatency = Math.max(metrics.maxLatency, latency); + metrics.minLatency = Math.min(metrics.minLatency, latency); + if (socket.connected) { allSockets.push(socket); association.students.push(student); metrics.studentsConnected++; - console.log(`Étudiant ${student.username} connecté à la salle ${roomId}. Latence : ${latency}ms`); } else { metrics.studentsFailed++; - console.warn(`Échec de la connexion de l'étudiant ${roomIndex}_${i} à la salle ${roomId}`); + console.warn(`Failed to connect student ${roomIndex}_${i} to room ${roomId}`); } }) ); - await Promise.allSettled(studentCreationPromises); - console.timeEnd('Temps de connexion des étudiants'); - console.log('Tous les étudiants ont été ajoutés et connectés à leurs salles respectives.'); + await Promise.allSettled(studentPromises); + console.timeEnd('Student connection time'); + console.log('All students connected to their respective rooms.'); } +// Simulates conversations in all rooms +async function simulateConversation() { + console.log("Conversation simulation started..."); + const messages = [ + "Bonjour, tout le monde !", + "Pouvez-vous répondre à la question 1 ?", + "J'ai une question sur l'exercice.", + "Voici la réponse à la question 1.", + "Merci pour vos réponses, continuons avec la question 2.", + "Je ne comprends pas bien, pouvez-vous expliquer à nouveau ?", + ]; + + const interval = 1000; + const duration = 10000; + const startTime = Date.now(); + + while (Date.now() - startTime < duration) { + for (const [roomId, association] of Object.entries(roomAssociations)) { + if (association.teacher) { + const teacherMessage = `Teacher says: ${messages[Math.floor(Math.random() * messages.length)]}`; + association.teacher.sendMessage(teacherMessage); + metrics.messagesSent++; + } + + for (const student of association.students) { + const studentMessage = `${student.username} says: ${messages[Math.floor(Math.random() * messages.length)]}`; + student.sendMessage(studentMessage); + metrics.messagesSent++; + } + } + await new Promise((resolve) => setTimeout(resolve, interval)); + } + + console.log("Conversation simulation ended."); +} + +// Closes all active WebSocket connections function closeAllSockets() { - console.log('Fermeture de toutes les connexions Socket.IO...'); + console.log('Closing all Socket.IO connections...'); allSockets.forEach((socket) => { if (socket && socket.connected) { try { socket.disconnect(); - console.log('Connexion Socket.IO déconnectée.'); + console.log('Socket.IO connection disconnected.'); } catch (error) { - console.error('Erreur lors de la déconnexion du socket Socket.IO :', error.message); + console.error('Error disconnecting Socket.IO connection:', error.message); } } }); - console.log('Toutes les connexions Socket.IO ont été déconnectées.'); -} - -function generateReport() { - console.log('Toutes les tâches ont été terminées.'); - console.log('--- Résultats du test de charge ---'); - console.log(`Salles créées : ${metrics.roomsCreated}`); - console.log(`Échecs de création de salles : ${metrics.roomsFailed}`); - console.log(`Enseignants connectés : ${metrics.teachersConnected}`); - console.log(`Échecs de connexion des enseignants : ${metrics.teachersFailed}`); - console.log(`Étudiants connectés : ${metrics.studentsConnected}`); - console.log(`Échecs de connexion des étudiants : ${metrics.studentsFailed}`); - console.log(`Durée totale d'exécution : ${(metrics.endTime - metrics.startTime) / 1000}s`); - console.log('Utilisation de la mémoire :', process.memoryUsage()); - writeMetricsToFile( metrics); + console.log('All Socket.IO connections have been disconnected.'); } +// Main function to orchestrate the workflow async function main() { try { metrics.startTime = new Date(); - const token = await attemptLoginOrRegister(BASE_URL, user.username, user.password); - if (!token) throw new Error('Échec de la connexion.'); + // Login or register + const token = await attemptLoginOrRegister(BASE_URL, user.username, user.password); + if (!token) throw new Error('Failed to login or register.'); + + // Room creation await createRoomContainers(token); - await addAndConnectTeachers(); - await addAndConnectStudents(); + + // Resource monitoring and test activities + const roomIds = Object.keys(roomAssociations); + const usageCaptureInterval = 100; + let testCompleted = false; + + const resourceCapturePromise = captureResourceUsageForContainers( + BASE_URL, + roomIds, + usageCaptureInterval, + () => testCompleted, + metrics + ); + + const testPromise = (async () => { + await addAndConnectTeachers(); + await addAndConnectStudents(); + await simulateConversation(); + testCompleted = true; + await new Promise((resolve) => setTimeout(resolve, 5000)); + })(); + + await Promise.all([resourceCapturePromise, testPromise]); metrics.endTime = new Date(); + writeMetricsToFile(metrics); + await generateGraphs(metrics.resourceUsage, metrics); - generateReport(); + console.log("All tasks completed successfully!"); } catch (error) { - console.error('Une erreur est survenue :', error.message); + console.error('Error:', error.message); } } -// Gestion de l'interruption et de la fermeture +// Handle process interruptions process.on('SIGINT', () => { - console.log('Script interrompu (Ctrl+C).'); + console.log('Process interrupted (Ctrl+C).'); closeAllSockets(); process.exit(0); }); -process.on('exit', closeAllSockets); +process.on('exit', () => closeAllSockets()); +process.on('uncaughtException', (err) => { + console.error('Uncaught Exception:', err); + closeAllSockets(); + process.exit(1); +}); +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection:', promise, 'reason:', reason); + closeAllSockets(); + process.exit(1); +}); main(); diff --git a/test/stressTest/metrics/metrics_report.json b/test/stressTest/metrics/metrics_report.json deleted file mode 100644 index fde1ad8..0000000 --- a/test/stressTest/metrics/metrics_report.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "roomsCreated": 5, - "roomsFailed": 0, - "teachersConnected": 5, - "teachersFailed": 0, - "studentsConnected": 295, - "studentsFailed": 0, - "startTime": "2024-11-16T01:06:28.606Z", - "endTime": "2024-11-16T01:06:31.135Z", - "executionTime": 2.529, - "memoryUsage": { - "rss": 72040448, - "heapTotal": 51122176, - "heapUsed": 23656264, - "external": 4503630, - "arrayBuffers": 170538 - } -} \ No newline at end of file diff --git a/test/stressTest/metrics_report_1731719315629 b/test/stressTest/metrics_report_1731719315629 deleted file mode 100644 index 828dd90..0000000 --- a/test/stressTest/metrics_report_1731719315629 +++ /dev/null @@ -1,18 +0,0 @@ -{ - "roomsCreated": 5, - "roomsFailed": 0, - "teachersConnected": 5, - "teachersFailed": 0, - "studentsConnected": 295, - "studentsFailed": 0, - "startTime": "2024-11-16T01:08:33.133Z", - "endTime": "2024-11-16T01:08:35.626Z", - "executionTime": 2.493, - "memoryUsage": { - "rss": 71737344, - "heapTotal": 51122176, - "heapUsed": 23472768, - "external": 4499368, - "arrayBuffers": 166276 - } -} \ No newline at end of file diff --git a/test/stressTest/metrics_report_1731719370035 b/test/stressTest/metrics_report_1731719370035 deleted file mode 100644 index 0a9eb5d..0000000 --- a/test/stressTest/metrics_report_1731719370035 +++ /dev/null @@ -1,18 +0,0 @@ -{ - "roomsCreated": 5, - "roomsFailed": 0, - "teachersConnected": 5, - "teachersFailed": 0, - "studentsConnected": 295, - "studentsFailed": 0, - "startTime": "2024-11-16T01:09:27.129Z", - "endTime": "2024-11-16T01:09:30.032Z", - "executionTime": 2.903, - "memoryUsage": { - "rss": 72146944, - "heapTotal": 50860032, - "heapUsed": 22783944, - "external": 4496062, - "arrayBuffers": 162970 - } -} \ No newline at end of file diff --git a/test/stressTest/package-lock.json b/test/stressTest/package-lock.json index a5bf80f..b5eb2f0 100644 --- a/test/stressTest/package-lock.json +++ b/test/stressTest/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "axios": "^1.7.7", + "chartjs-node-canvas": "^4.1.6", "dockerode": "^4.0.2", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1" @@ -20,6 +21,25 @@ "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==" }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -46,6 +66,11 @@ "undici-types": "~6.19.8" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -58,6 +83,43 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -81,6 +143,11 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -126,6 +193,15 @@ "readable-stream": "^3.4.0" } }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -158,11 +234,51 @@ "node": ">=10.0.0" } }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/chart.js": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==", + "peer": true + }, + "node_modules/chartjs-node-canvas": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/chartjs-node-canvas/-/chartjs-node-canvas-4.1.6.tgz", + "integrity": "sha512-UQJbPWrvqB/FoLclGA9BaLQmZbzSYlujF4w8NZd6Xzb+sqgACBb2owDX6m7ifCXLjUW5Nz0Qx0qqrTtQkkSoYw==", + "dependencies": { + "canvas": "^2.8.0", + "tslib": "^2.3.1" + }, + "peerDependencies": { + "chart.js": "^3.5.1" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -174,6 +290,16 @@ "node": ">= 0.8" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -224,6 +350,17 @@ } } }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -232,6 +369,19 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/docker-modem": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", @@ -259,6 +409,11 @@ "node": ">= 8.0" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -344,6 +499,90 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -363,11 +602,51 @@ } ] }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -387,6 +666,70 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -400,8 +743,7 @@ "node_modules/nan": { "version": "2.22.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", - "optional": true + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==" }, "node_modules/negotiator": { "version": "0.6.3", @@ -411,6 +753,51 @@ "node": ">= 0.6" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -427,6 +814,14 @@ "wrappy": "1" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -454,6 +849,21 @@ "node": ">= 6" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -478,6 +888,56 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -560,6 +1020,46 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", @@ -586,6 +1086,24 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -609,6 +1127,28 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -641,6 +1181,11 @@ "engines": { "node": ">=0.4.0" } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/test/stressTest/package.json b/test/stressTest/package.json index ff3a474..0ed6014 100644 --- a/test/stressTest/package.json +++ b/test/stressTest/package.json @@ -11,6 +11,7 @@ "license": "ISC", "dependencies": { "axios": "^1.7.7", + "chartjs-node-canvas": "^4.1.6", "dockerode": "^4.0.2", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1" diff --git a/test/stressTest/utility/apiServices.js b/test/stressTest/utility/apiServices.js index bbb8d8d..34cce33 100644 --- a/test/stressTest/utility/apiServices.js +++ b/test/stressTest/utility/apiServices.js @@ -1,4 +1,5 @@ import axios from "axios"; +import { io } from "socket.io-client"; /** * Logs in a user. @@ -8,24 +9,18 @@ import axios from "axios"; * @returns {Promise} - The authentication token if successful. */ async function login(baseUrl, email, password) { - if (!email || !password) { - throw new Error("Email and password are required."); - } - - const url = `${baseUrl}/api/user/login`; - const payload = { email, password }; + if (!email || !password) throw new Error("Email and password are required."); try { - const res = await axios.post(url, payload, { + const res = await axios.post(`${baseUrl}/api/user/login`, { email, password }, { headers: { "Content-Type": "application/json" }, }); - if (res.status !== 200 || !res.data.token) { - throw new Error(`Login failed. Status: ${res.status}`); + if (res.status === 200 && res.data.token) { + console.log(`Login successful for ${email}`); + return res.data.token; } - - console.log(`Login successful for ${email}`); - return res.data.token; + throw new Error(`Login failed. Status: ${res.status}`); } catch (error) { console.error(`Login error for ${email}:`, error.message); throw error; @@ -40,24 +35,18 @@ async function login(baseUrl, email, password) { * @returns {Promise} - A success message if registration is successful. */ async function register(baseUrl, email, password) { - if (!email || !password) { - throw new Error("Email and password are required."); - } - - const url = `${baseUrl}/api/user/register`; - const payload = { email, password }; + if (!email || !password) throw new Error("Email and password are required."); try { - const res = await axios.post(url, payload, { + const res = await axios.post(`${baseUrl}/api/user/register`, { email, password }, { headers: { "Content-Type": "application/json" }, }); - if (res.status !== 200) { - throw new Error(`Registration failed. Status: ${res.status}`); + if (res.status === 200) { + console.log(`Registration successful for ${email}`); + return res.data.message || "Registration completed successfully."; } - - console.log(`Registration successful for ${email}`); - return res.data.message || "Registration completed successfully."; + throw new Error(`Registration failed. Status: ${res.status}`); } catch (error) { console.error(`Registration error for ${email}:`, error.message); throw error; @@ -73,21 +62,14 @@ async function register(baseUrl, email, password) { */ export async function attemptLoginOrRegister(baseUrl, username, password) { try { - const token = await login(baseUrl, username, password); - console.log(`User successfully logged in: ${username}`); - return token; + return await login(baseUrl, username, password); } catch (loginError) { console.log(`Login failed for ${username}. Attempting registration...`); - try { - const registerResponse = await register(baseUrl, username, password); - console.log(`User successfully registered: ${username}`); - - const token = await login(baseUrl, username, password); - console.log(`User successfully logged in after registration: ${username}`); - return token; + await register(baseUrl, username, password); + return await login(baseUrl, username, password); } catch (registerError) { - console.error(`Registration failed for ${username}:`, registerError.message); + console.error(`Registration and login failed for ${username}:`, registerError.message); return null; } } @@ -100,28 +82,76 @@ export async function attemptLoginOrRegister(baseUrl, username, password) { * @returns {Promise} - The created room object if successful. */ export async function createRoomContainer(baseUrl, token) { - if (!token) { - throw new Error("Authorization token is required."); - } - - const url = `${baseUrl}/api/room`; + if (!token) throw new Error("Authorization token is required."); try { - const res = await axios.post(url, {}, { + const res = await axios.post(`${baseUrl}/api/room`, {}, { headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); - if (res.status !== 200) { - throw new Error(`Room creation failed. Status: ${res.status}`); - } - - //console.log("Room successfully created:", res.data); - return res.data; + if (res.status === 200) return res.data; + throw new Error(`Room creation failed. Status: ${res.status}`); } catch (error) { console.error("Room creation error:", error.message); throw error; } } + +/** + * Captures resource usage from multiple containers via WebSocket. + * @param {string} baseUrl - The base URL of the API. + * @param {string[]} roomIds - List of room IDs. + * @param {number} interval - Time interval between captures (ms). + * @param {function} shouldStop - Callback to determine if capturing should stop. + * @param {object} metrics - Metrics object to store resource usage. + */ +export async function captureResourceUsageForContainers(baseUrl, roomIds, interval, shouldStop, metrics) { + console.log("Starting resource usage capture..."); + + const sockets = {}; + const resourceData = {}; + + // Initialize WebSocket connections for each room + roomIds.forEach((id) => { + resourceData[id] = []; + const socket = io(baseUrl, { + path: `/api/room/${id}/socket`, + transports: ["websocket"], + autoConnect: true, + reconnection: true, + }); + + socket.on("connect", () => console.log(`Connected to room ${id}`)); + socket.on("connect_error", (err) => console.error(`Connection error for room ${id}:`, err.message)); + sockets[id] = socket; + }); + + // Capture resource usage periodically + while (!shouldStop()) { + for (const id of roomIds) { + const socket = sockets[id]; + if (socket?.connected) { + try { + socket.emit("get-usage"); + socket.once("usage-data", (data) => { + resourceData[id].push({ timestamp: Date.now(), ...data }); + }); + } catch (error) { + console.warn(`Error capturing metrics for room ${id}:`, error.message); + } + } else { + console.warn(`Socket not connected for room ${id}`); + } + } + await new Promise((resolve) => setTimeout(resolve, interval)); + } + + // Close all WebSocket connections + Object.values(sockets).forEach((socket) => socket.close()); + console.log("Resource usage capture completed."); + + metrics.resourceUsage = resourceData; +} diff --git a/test/stressTest/utility/writeMetrics.js b/test/stressTest/utility/writeMetrics.js index ee23c29..b5f35fa 100644 --- a/test/stressTest/utility/writeMetrics.js +++ b/test/stressTest/utility/writeMetrics.js @@ -1,26 +1,227 @@ -import fs from 'fs'; +import fs from "fs"; +import path from "path"; +import { ChartJSNodeCanvas } from "chartjs-node-canvas"; -/** - * Écrit les métriques dans un fichier JSON. - * @param {string} filename - Nom du fichier où écrire les métriques. - * @param {Object} metrics - Objet contenant les métriques à enregistrer. - */ +// Ensure a directory exists, creating it if necessary +function ensureDirectoryExists(directory) { + try { + fs.mkdirSync(directory, { recursive: true }); + } catch (err) { + console.error(`Error creating directory ${directory}:`, err.message); + throw err; + } +} + +// Write metrics to a JSON file export function writeMetricsToFile(metrics) { + if (!metrics.endTime) { + console.error("Error: metrics.endTime is not defined. Ensure it is set before calling this function."); + return; + } + + const directory = `./metrics/${metrics.startTime.toISOString().replace(/[:.]/g, "-")}`; + const filename = path.join(directory, "metrics_report.json"); + + // Ensure the directory exists + ensureDirectoryExists(directory); + const metricsData = { - ...metrics, - startTime: metrics.startTime?.toISOString(), - endTime: metrics.endTime?.toISOString(), - executionTime: metrics.endTime && metrics.startTime - ? (metrics.endTime - metrics.startTime) / 1000 - : null, - memoryUsage: process.memoryUsage(), + summary: { + roomsCreated: metrics.roomsCreated, + roomsFailed: metrics.roomsFailed, + teachersConnected: metrics.teachersConnected, + teachersFailed: metrics.teachersFailed, + studentsConnected: metrics.studentsConnected, + studentsFailed: metrics.studentsFailed, + }, + messages: { + messagesSent: metrics.messagesSent, + messagesReceived: metrics.messagesReceived, + throughput: metrics.throughput.toFixed(2), // Messages per second + }, + latencies: { + totalLatency: metrics.totalLatency, + averageLatency: metrics.teachersConnected + metrics.studentsConnected + ? (metrics.totalLatency / (metrics.teachersConnected + metrics.studentsConnected)).toFixed(2) + : null, + maxLatency: metrics.maxLatency, + minLatency: metrics.minLatency, + }, + timing: { + startTime: metrics.startTime?.toISOString(), + endTime: metrics.endTime?.toISOString(), + executionTimeInSeconds: metrics.endTime && metrics.startTime + ? (metrics.endTime - metrics.startTime) / 1000 + : null, + }, }; - fs.writeFile(`metrics_report_${Date.now()}`, JSON.stringify(metricsData, null, 4), (err) => { + // Write metrics to a file + fs.writeFile(filename, JSON.stringify(metricsData, null, 4), (err) => { if (err) { - console.error('Erreur lors de l\'écriture des métriques dans le fichier :', err.message); + console.error(`Error writing metrics to file:`, err.message); } else { - console.log(`Métriques enregistrées dans le fichier metrics_report_${Date.now()}.`); + console.log(`Metrics saved to file: ${filename}`); } }); } + +// Generate charts for resource data +export async function generateGraphs(resourceData, metrics) { + if (!metrics.endTime) { + console.error("Error: metrics.endTime is not defined. Ensure it is set before calling this function."); + return; + } + + const directory = `./metrics/${metrics.startTime.toISOString().replace(/[:.]/g, "-")}`; + ensureDirectoryExists(directory); + + const chartJSNodeCanvas = new ChartJSNodeCanvas({ width: 800, height: 600 }); + + // Aggregated data for all containers + const aggregatedTimestamps = []; + const aggregatedMemoryUsage = []; + const aggregatedCpuUserPercentage = []; + const aggregatedCpuSystemPercentage = []; + + // Generate charts for individual containers + for (const [roomId, data] of Object.entries(resourceData)) { + if (!data || !data.length) { + console.warn(`No data available for room ${roomId}. Skipping individual charts.`); + continue; + } + + // Extract data + const timestamps = data.map((point) => new Date(point.timestamp).toLocaleTimeString()); + const memoryUsage = data.map((point) => (point.memory.rss || 0) / (1024 * 1024)); // MB + const cpuUserPercentage = data.map((point) => point.cpu.userPercentage || 0); + const cpuSystemPercentage = data.map((point) => point.cpu.systemPercentage || 0); + + // Update aggregated data + data.forEach((point, index) => { + if (!aggregatedTimestamps[index]) { + aggregatedTimestamps[index] = timestamps[index]; + aggregatedMemoryUsage[index] = 0; + aggregatedCpuUserPercentage[index] = 0; + aggregatedCpuSystemPercentage[index] = 0; + } + aggregatedMemoryUsage[index] += memoryUsage[index]; + aggregatedCpuUserPercentage[index] += cpuUserPercentage[index]; + aggregatedCpuSystemPercentage[index] += cpuSystemPercentage[index]; + }); + + // Memory usage chart + await saveChart( + chartJSNodeCanvas, + { + labels: timestamps, + datasets: [ + { + label: "Memory Usage (MB)", + data: memoryUsage, + borderColor: "blue", + fill: false, + }, + ], + }, + "Time", + "Memory Usage (MB)", + path.join(directory, `memory-usage-room-${roomId}.png`) + ); + + // CPU usage chart + await saveChart( + chartJSNodeCanvas, + { + labels: timestamps, + datasets: [ + { + label: "CPU User Usage (%)", + data: cpuUserPercentage, + borderColor: "red", + fill: false, + }, + { + label: "CPU System Usage (%)", + data: cpuSystemPercentage, + borderColor: "orange", + fill: false, + }, + ], + }, + "Time", + "CPU Usage (%)", + path.join(directory, `cpu-usage-room-${roomId}.png`) + ); + + console.log(`Charts generated for room ${roomId}`); + } + + // Ensure aggregated data is not empty + if (!aggregatedTimestamps.length) { + console.error("Error: Aggregated data is empty. Verify container data."); + return; + } + + // Aggregated memory usage chart + await saveChart( + chartJSNodeCanvas, + { + labels: aggregatedTimestamps, + datasets: [ + { + label: "Total Memory Usage (MB)", + data: aggregatedMemoryUsage, + borderColor: "blue", + fill: false, + }, + ], + }, + "Time", + "Memory Usage (MB)", + path.join(directory, "aggregated-memory-usage.png") + ); + + // Aggregated CPU usage chart + await saveChart( + chartJSNodeCanvas, + { + labels: aggregatedTimestamps, + datasets: [ + { + label: "Total CPU User Usage (%)", + data: aggregatedCpuUserPercentage, + borderColor: "red", + fill: false, + }, + { + label: "Total CPU System Usage (%)", + data: aggregatedCpuSystemPercentage, + borderColor: "orange", + fill: false, + }, + ], + }, + "Time", + "CPU Usage (%)", + path.join(directory, "aggregated-cpu-usage.png") + ); + + console.log("Aggregated charts generated."); +} + +// Helper function to save a chart +async function saveChart(chartJSNodeCanvas, data, xLabel, yLabel, outputFile) { + const chartConfig = { + type: "line", + data, + options: { + scales: { + x: { title: { display: true, text: xLabel } }, + y: { title: { display: true, text: yLabel } }, + }, + }, + }; + const chartBuffer = await chartJSNodeCanvas.renderToBuffer(chartConfig); + fs.writeFileSync(outputFile, chartBuffer); +}