not finished

This commit is contained in:
MathieuSevignyLavallee 2024-11-26 17:04:22 -05:00
parent 5c24ae56a9
commit 11222c70bd
12 changed files with 1080 additions and 200 deletions

View file

@ -2,7 +2,8 @@ import http from "http";
import { Server, ServerOptions } from "socket.io"; import { Server, ServerOptions } from "socket.io";
import { setupWebsocket } from "./socket/setupWebSocket"; import { setupWebsocket } from "./socket/setupWebSocket";
import dotenv from "dotenv"; import dotenv from "dotenv";
import express from 'express'; import express from "express";
import os from "os"; // Import the os module
// Load environment variables // Load environment variables
dotenv.config(); dotenv.config();
@ -36,6 +37,7 @@ app.get('/health', (_, res) => {
} }
}); });
const ioOptions: Partial<ServerOptions> = { const ioOptions: Partial<ServerOptions> = {
path: `/api/room/${roomId}/socket`, path: `/api/room/${roomId}/socket`,
cors: { cors: {
@ -52,4 +54,4 @@ setupWebsocket(io);
server.listen(port, () => { server.listen(port, () => {
console.log(`WebSocket server is running on port ${port}`); console.log(`WebSocket server is running on port ${port}`);
}); });

View file

@ -1,4 +1,5 @@
import { Server, Socket } from "socket.io"; import { Server, Socket } from "socket.io";
import os from "os";
const MAX_USERS_PER_ROOM = 60; const MAX_USERS_PER_ROOM = 60;
const MAX_TOTAL_CONNECTIONS = 2000; const MAX_TOTAL_CONNECTIONS = 2000;
@ -19,10 +20,10 @@ export const setupWebsocket = (io: Server): void => {
socket.on("create-room", (sentRoomName) => { socket.on("create-room", (sentRoomName) => {
// Ensure sentRoomName is a string before applying toUpperCase() // Ensure sentRoomName is a string before applying toUpperCase()
const roomName = (typeof sentRoomName === "string" && sentRoomName.trim() !== "") const roomName = (typeof sentRoomName === "string" && sentRoomName.trim() !== "")
? sentRoomName.toUpperCase() ? sentRoomName.toUpperCase()
: generateRoomName(); : generateRoomName();
if (!io.sockets.adapter.rooms.get(roomName)) { if (!io.sockets.adapter.rooms.get(roomName)) {
socket.join(roomName); socket.join(roomName);
socket.emit("create-success", roomName); socket.emit("create-success", roomName);
@ -96,10 +97,62 @@ export const setupWebsocket = (io: Server): void => {
socket.on("error", (error) => { socket.on("error", (error) => {
console.error("WebSocket server 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 generateRoomName = (length = 6): string => {
const characters = "0123456789"; const characters = "0123456789";
let result = ""; let result = "";

View file

@ -19,19 +19,20 @@ export class Student {
reconnectionDelay: 10000, reconnectionDelay: 10000,
timeout: 20000, timeout: 20000,
}); });
this.socket.on('connect', () => { this.socket.on('connect', () => {
this.joinRoom(this.roomName, this.username); this.joinRoom(this.roomName, this.username);
resolve(this.socket); this.listenForMessages(); // Start listening for messages
resolve(this.socket);
}); });
this.socket.on('error', (error) => { this.socket.on('error', (error) => {
reject(new Error(`Connection error: ${error.message}`)); reject(new Error(`Connection error: ${error.message}`));
}); });
} catch (error) { } catch (error) {
console.error(`Error connecting ${this.name} to room ${this.roomId}:`, error.message); console.error(`Error connecting ${this.name} to room ${this.roomName}:`, error.message);
reject(error); reject(error);
} }
}); });
} }
@ -41,4 +42,18 @@ export class Student {
this.socket.emit('join-room', { roomName, username }); 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);
});
}
}
} }

View file

@ -19,23 +19,19 @@ export class Teacher {
reconnectionDelay: 10000, reconnectionDelay: 10000,
timeout: 20000, timeout: 20000,
}); });
this.socket.on('connect', () => { this.socket.on('connect', () => {
this.createRoom(this.roomName); this.createRoom(this.roomName);
resolve(this.socket); this.listenForMessages(); // Start listening for messages
resolve(this.socket);
}); });
this.socket.on('error', (error) => { 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) { } catch (error) {
console.error(`Error connecting ${this.name} to room ${this.roomId}:`, error.message); console.error(`Error connecting ${this.name} to room ${this.roomName}:`, error.message);
reject(error); reject(error);
} }
}); });
} }
@ -45,4 +41,18 @@ export class Teacher {
this.socket.emit('create-room', this.roomName || undefined); 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);
});
}
}
} }

View file

@ -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 { Student } from './class/student.js';
import { Teacher } from './class/teacher.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 BASE_URL = 'http://localhost';
const user = { username: 'admin@example.com', password: 'adminPassword' }; const user = { username: 'admin@example.com', password: 'adminPassword' };
const numberRooms = 5; const numberRooms = 20;
const studentPerRoom = 59; // Max : 60, 1 place réservée pour le professeur const studentPerRoom = 58; // Max: 60, 1 reserved for teacher
const roomAssociations = {}; const roomAssociations = {};
const allSockets = []; // Suivi de toutes les connexions WebSocket actives const allSockets = []; // Tracks all active WebSocket connections
// Métriques
const metrics = { const metrics = {
roomsCreated: 0, roomsCreated: 0,
roomsFailed: 0, roomsFailed: 0,
@ -18,133 +17,211 @@ const metrics = {
teachersFailed: 0, teachersFailed: 0,
studentsConnected: 0, studentsConnected: 0,
studentsFailed: 0, studentsFailed: 0,
messagesSent: 0,
messagesReceived: 0,
totalLatency: 0,
maxLatency: 0,
minLatency: Number.MAX_SAFE_INTEGER,
throughput: 0,
startTime: null, startTime: null,
endTime: null, endTime: null,
}; };
// Creates rooms and tracks their creation
async function createRoomContainers(token) { 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 roomCreationPromises = Array.from({ length: numberRooms }, async () => {
const room = await createRoomContainer(BASE_URL, token); try {
if (room?.id) { const room = await createRoomContainer(BASE_URL, token);
roomAssociations[room.id] = { teacher: null, students: [] }; if (room?.id) {
metrics.roomsCreated++; roomAssociations[room.id] = { teacher: null, students: [] };
console.log(`Salle créée avec l'ID : ${room.id}`); metrics.roomsCreated++;
} else { console.log(`Room created with ID: ${room.id}`);
}
} catch {
metrics.roomsFailed++; metrics.roomsFailed++;
console.warn('Échec de la création dune salle.'); console.warn('Failed to create a room.');
} }
}); });
await Promise.allSettled(roomCreationPromises); await Promise.allSettled(roomCreationPromises);
console.timeEnd('Temps de création des salles'); console.timeEnd('Room creation time');
console.log(`Nombre total de salles créées : ${Object.keys(roomAssociations).length}`); console.log(`Total rooms created: ${Object.keys(roomAssociations).length}`);
} }
// Connects teachers to their respective rooms
async function addAndConnectTeachers() { async function addAndConnectTeachers() {
console.time('Temps de connexion des enseignants'); console.time('Teacher connection time');
const teacherCreationPromises = Object.keys(roomAssociations).map(async (roomId, index) => { const teacherPromises = Object.keys(roomAssociations).map(async (roomId, index) => {
const teacher = new Teacher(`teacher_${index}`, roomId); const teacher = new Teacher(`teacher_${index}`, roomId);
const start = Date.now(); const start = Date.now();
const socket = await teacher.connectToRoom(BASE_URL); const socket = await teacher.connectToRoom(BASE_URL);
const latency = Date.now() - start; 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) { if (socket.connected) {
allSockets.push(socket); allSockets.push(socket);
roomAssociations[roomId].teacher = teacher; roomAssociations[roomId].teacher = teacher;
metrics.teachersConnected++; 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 { } else {
metrics.teachersFailed++; 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); await Promise.allSettled(teacherPromises);
console.timeEnd('Temps de connexion des enseignants'); console.timeEnd('Teacher connection time');
console.log('Tous les enseignants ont été ajoutés et connectés à leurs salles respectives.'); console.log('All teachers connected to their respective rooms.');
} }
// Connects students to their respective rooms
async function addAndConnectStudents() { async function addAndConnectStudents() {
console.time('Temps de connexion des étudiants'); console.time('Student connection time');
const studentCreationPromises = Object.entries(roomAssociations).flatMap(([roomId, association], roomIndex) => const studentPromises = Object.entries(roomAssociations).flatMap(([roomId, association], roomIndex) =>
Array.from({ length: studentPerRoom }, async (_, i) => { Array.from({ length: studentPerRoom }, async (_, i) => {
const student = new Student(`student_${roomIndex}_${i}`, roomId); const student = new Student(`student_${roomIndex}_${i}`, roomId);
const start = Date.now(); const start = Date.now();
const socket = await student.connectToRoom(BASE_URL); const socket = await student.connectToRoom(BASE_URL);
const latency = Date.now() - start; 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) { if (socket.connected) {
allSockets.push(socket); allSockets.push(socket);
association.students.push(student); association.students.push(student);
metrics.studentsConnected++; metrics.studentsConnected++;
console.log(`Étudiant ${student.username} connecté à la salle ${roomId}. Latence : ${latency}ms`);
} else { } else {
metrics.studentsFailed++; 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); await Promise.allSettled(studentPromises);
console.timeEnd('Temps de connexion des étudiants'); console.timeEnd('Student connection time');
console.log('Tous les étudiants ont été ajoutés et connectés à leurs salles respectives.'); 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() { function closeAllSockets() {
console.log('Fermeture de toutes les connexions Socket.IO...'); console.log('Closing all Socket.IO connections...');
allSockets.forEach((socket) => { allSockets.forEach((socket) => {
if (socket && socket.connected) { if (socket && socket.connected) {
try { try {
socket.disconnect(); socket.disconnect();
console.log('Connexion Socket.IO déconnectée.'); console.log('Socket.IO connection disconnected.');
} catch (error) { } 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.'); console.log('All Socket.IO connections have been disconnected.');
}
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);
} }
// Main function to orchestrate the workflow
async function main() { async function main() {
try { try {
metrics.startTime = new Date(); 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 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(); metrics.endTime = new Date();
writeMetricsToFile(metrics);
await generateGraphs(metrics.resourceUsage, metrics);
generateReport(); console.log("All tasks completed successfully!");
} catch (error) { } 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', () => { process.on('SIGINT', () => {
console.log('Script interrompu (Ctrl+C).'); console.log('Process interrupted (Ctrl+C).');
closeAllSockets(); closeAllSockets();
process.exit(0); 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(); main();

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -10,6 +10,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.7.7", "axios": "^1.7.7",
"chartjs-node-canvas": "^4.1.6",
"dockerode": "^4.0.2", "dockerode": "^4.0.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-client": "^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", "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==" "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": { "node_modules/@socket.io/component-emitter": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@ -46,6 +66,11 @@
"undici-types": "~6.19.8" "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": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -58,6 +83,43 @@
"node": ">= 0.6" "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": { "node_modules/asn1": {
"version": "0.2.6", "version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
@ -81,6 +143,11 @@
"proxy-from-env": "^1.1.0" "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": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -126,6 +193,15 @@
"readable-stream": "^3.4.0" "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": { "node_modules/buffer": {
"version": "5.7.1", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@ -158,11 +234,51 @@
"node": ">=10.0.0" "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": { "node_modules/chownr": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" "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": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -174,6 +290,16 @@
"node": ">= 0.8" "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": { "node_modules/cookie": {
"version": "0.7.2", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "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": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -232,6 +369,19 @@
"node": ">=0.4.0" "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": { "node_modules/docker-modem": {
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz",
@ -259,6 +409,11 @@
"node": ">= 8.0" "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": { "node_modules/end-of-stream": {
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "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", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" "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": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "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": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "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": { "node_modules/mime-db": {
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -387,6 +666,70 @@
"node": ">= 0.6" "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": { "node_modules/mkdirp-classic": {
"version": "0.5.3", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@ -400,8 +743,7 @@
"node_modules/nan": { "node_modules/nan": {
"version": "2.22.0", "version": "2.22.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw=="
"optional": true
}, },
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
@ -411,6 +753,51 @@
"node": ">= 0.6" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -427,6 +814,14 @@
"wrappy": "1" "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": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@ -454,6 +849,21 @@
"node": ">= 6" "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": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "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", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "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": { "node_modules/socket.io": {
"version": "4.8.1", "version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
@ -560,6 +1020,46 @@
"safe-buffer": "~5.2.0" "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": { "node_modules/tar-fs": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
@ -586,6 +1086,24 @@
"node": ">=6" "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": { "node_modules/tweetnacl": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@ -609,6 +1127,28 @@
"node": ">= 0.8" "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": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@ -641,6 +1181,11 @@
"engines": { "engines": {
"node": ">=0.4.0" "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=="
} }
} }
} }

View file

@ -11,6 +11,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.7.7", "axios": "^1.7.7",
"chartjs-node-canvas": "^4.1.6",
"dockerode": "^4.0.2", "dockerode": "^4.0.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-client": "^4.8.1" "socket.io-client": "^4.8.1"

View file

@ -1,4 +1,5 @@
import axios from "axios"; import axios from "axios";
import { io } from "socket.io-client";
/** /**
* Logs in a user. * Logs in a user.
@ -8,24 +9,18 @@ import axios from "axios";
* @returns {Promise<string>} - The authentication token if successful. * @returns {Promise<string>} - The authentication token if successful.
*/ */
async function login(baseUrl, email, password) { async function login(baseUrl, email, password) {
if (!email || !password) { if (!email || !password) throw new Error("Email and password are required.");
throw new Error("Email and password are required.");
}
const url = `${baseUrl}/api/user/login`;
const payload = { email, password };
try { try {
const res = await axios.post(url, payload, { const res = await axios.post(`${baseUrl}/api/user/login`, { email, password }, {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });
if (res.status !== 200 || !res.data.token) { if (res.status === 200 && res.data.token) {
throw new Error(`Login failed. Status: ${res.status}`); console.log(`Login successful for ${email}`);
return res.data.token;
} }
throw new Error(`Login failed. Status: ${res.status}`);
console.log(`Login successful for ${email}`);
return res.data.token;
} catch (error) { } catch (error) {
console.error(`Login error for ${email}:`, error.message); console.error(`Login error for ${email}:`, error.message);
throw error; throw error;
@ -40,24 +35,18 @@ async function login(baseUrl, email, password) {
* @returns {Promise<string>} - A success message if registration is successful. * @returns {Promise<string>} - A success message if registration is successful.
*/ */
async function register(baseUrl, email, password) { async function register(baseUrl, email, password) {
if (!email || !password) { if (!email || !password) throw new Error("Email and password are required.");
throw new Error("Email and password are required.");
}
const url = `${baseUrl}/api/user/register`;
const payload = { email, password };
try { try {
const res = await axios.post(url, payload, { const res = await axios.post(`${baseUrl}/api/user/register`, { email, password }, {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });
if (res.status !== 200) { if (res.status === 200) {
throw new Error(`Registration failed. Status: ${res.status}`); console.log(`Registration successful for ${email}`);
return res.data.message || "Registration completed successfully.";
} }
throw new Error(`Registration failed. Status: ${res.status}`);
console.log(`Registration successful for ${email}`);
return res.data.message || "Registration completed successfully.";
} catch (error) { } catch (error) {
console.error(`Registration error for ${email}:`, error.message); console.error(`Registration error for ${email}:`, error.message);
throw error; throw error;
@ -73,21 +62,14 @@ async function register(baseUrl, email, password) {
*/ */
export async function attemptLoginOrRegister(baseUrl, username, password) { export async function attemptLoginOrRegister(baseUrl, username, password) {
try { try {
const token = await login(baseUrl, username, password); return await login(baseUrl, username, password);
console.log(`User successfully logged in: ${username}`);
return token;
} catch (loginError) { } catch (loginError) {
console.log(`Login failed for ${username}. Attempting registration...`); console.log(`Login failed for ${username}. Attempting registration...`);
try { try {
const registerResponse = await register(baseUrl, username, password); await register(baseUrl, username, password);
console.log(`User successfully registered: ${username}`); return await login(baseUrl, username, password);
const token = await login(baseUrl, username, password);
console.log(`User successfully logged in after registration: ${username}`);
return token;
} catch (registerError) { } catch (registerError) {
console.error(`Registration failed for ${username}:`, registerError.message); console.error(`Registration and login failed for ${username}:`, registerError.message);
return null; return null;
} }
} }
@ -100,28 +82,76 @@ export async function attemptLoginOrRegister(baseUrl, username, password) {
* @returns {Promise<object>} - The created room object if successful. * @returns {Promise<object>} - The created room object if successful.
*/ */
export async function createRoomContainer(baseUrl, token) { export async function createRoomContainer(baseUrl, token) {
if (!token) { if (!token) throw new Error("Authorization token is required.");
throw new Error("Authorization token is required.");
}
const url = `${baseUrl}/api/room`;
try { try {
const res = await axios.post(url, {}, { const res = await axios.post(`${baseUrl}/api/room`, {}, {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
}); });
if (res.status !== 200) { if (res.status === 200) return res.data;
throw new Error(`Room creation failed. Status: ${res.status}`); throw new Error(`Room creation failed. Status: ${res.status}`);
}
//console.log("Room successfully created:", res.data);
return res.data;
} catch (error) { } catch (error) {
console.error("Room creation error:", error.message); console.error("Room creation error:", error.message);
throw error; 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;
}

View file

@ -1,26 +1,227 @@
import fs from 'fs'; import fs from "fs";
import path from "path";
import { ChartJSNodeCanvas } from "chartjs-node-canvas";
/** // Ensure a directory exists, creating it if necessary
* Écrit les métriques dans un fichier JSON. function ensureDirectoryExists(directory) {
* @param {string} filename - Nom du fichier écrire les métriques. try {
* @param {Object} metrics - Objet contenant les métriques à enregistrer. 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) { 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 = { const metricsData = {
...metrics, summary: {
startTime: metrics.startTime?.toISOString(), roomsCreated: metrics.roomsCreated,
endTime: metrics.endTime?.toISOString(), roomsFailed: metrics.roomsFailed,
executionTime: metrics.endTime && metrics.startTime teachersConnected: metrics.teachersConnected,
? (metrics.endTime - metrics.startTime) / 1000 teachersFailed: metrics.teachersFailed,
: null, studentsConnected: metrics.studentsConnected,
memoryUsage: process.memoryUsage(), 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) { 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 { } 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);
}