Add README, implementation de l'api Docker sur le quizroom, Fonctionnel

remove docker group

graph generator remake

Docker API implementation
This commit is contained in:
MathieuSevignyLavallee 2024-12-07 19:33:38 -05:00
parent dabdfafd35
commit fff5830afd
13 changed files with 740 additions and 415 deletions

View file

@ -55,6 +55,8 @@ services:
container_name: quizroom
ports:
- "4500:4500"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- quiz_network
restart: always

View file

@ -8,6 +8,8 @@ services:
- PORT=${PORT:-4500}
ports:
- "${PORT:-4500}:${PORT:-4500}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- PORT=${PORT:-4500}
- ROOM_ID=${ROOM_ID}

View file

@ -9,17 +9,24 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"dockerode": "^4.0.2",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"http": "^0.0.1-security",
"socket.io": "^4.8.1"
},
"devDependencies": {
"@types/dockerode": "^3.3.32",
"@types/express": "^5.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
},
"node_modules/@balena/dockerignore": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -118,6 +125,27 @@
"@types/node": "*"
}
},
"node_modules/@types/docker-modem": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz",
"integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/ssh2": "*"
}
},
"node_modules/@types/dockerode": {
"version": "3.3.32",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.32.tgz",
"integrity": "sha512-xxcG0g5AWKtNyh7I7wswLdFvym4Mlqks5ZlKzxEUrGHS0r0PUOfxm2T0mspwu10mHQqu3Ck3MI3V2HqvLWE1fg==",
"dev": true,
"dependencies": {
"@types/docker-modem": "*",
"@types/node": "*",
"@types/ssh2": "*"
}
},
"node_modules/@types/express": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
@ -195,6 +223,30 @@
"@types/send": "*"
}
},
"node_modules/@types/ssh2": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.1.tgz",
"integrity": "sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==",
"dev": true,
"dependencies": {
"@types/node": "^18.11.18"
}
},
"node_modules/@types/ssh2/node_modules/@types/node": {
"version": "18.19.67",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz",
"integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/ssh2/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -243,6 +295,33 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"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/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@ -251,6 +330,24 @@
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@ -290,6 +387,38 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buildcheck": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
"integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
"optional": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -318,6 +447,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"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/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -365,6 +499,20 @@
"node": ">= 0.10"
}
},
"node_modules/cpu-features": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"buildcheck": "~0.0.6",
"nan": "^2.19.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@ -432,6 +580,33 @@
"node": ">=0.3.1"
}
},
"node_modules/docker-modem": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz",
"integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==",
"dependencies": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
"split-ca": "^1.0.1",
"ssh2": "^1.15.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/dockerode": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz",
"integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==",
"dependencies": {
"@balena/dockerignore": "^1.0.2",
"docker-modem": "^5.0.3",
"tar-fs": "~2.0.1"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
@ -458,6 +633,14 @@
"node": ">= 0.8"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/engine.io": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
@ -639,6 +822,11 @@
"node": ">= 0.6"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -760,6 +948,25 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"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/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@ -839,11 +1046,22 @@
"node": ">= 0.6"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/nan": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
"optional": true
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -884,6 +1102,14 @@
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -912,6 +1138,15 @@
"node": ">= 0.10"
}
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@ -951,6 +1186,19 @@
"node": ">= 0.8"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -1119,6 +1367,28 @@
"node": ">=10.0.0"
}
},
"node_modules/split-ca": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="
},
"node_modules/ssh2": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.6",
"bcrypt-pbkdf": "^1.0.2"
},
"engines": {
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "~0.0.10",
"nan": "^2.20.0"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -1128,6 +1398,40 @@
"node": ">= 0.8"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/tar-fs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.0.0"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -1180,6 +1484,11 @@
}
}
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -1220,6 +1529,11 @@
"node": ">= 0.8"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -1243,6 +1557,11 @@
"node": ">= 0.8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",

View file

@ -12,11 +12,13 @@
"license": "ISC",
"description": "",
"devDependencies": {
"@types/dockerode": "^3.3.32",
"@types/express": "^5.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
},
"dependencies": {
"dockerode": "^4.0.2",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"http": "^0.0.1-security",

View file

@ -1,5 +1,5 @@
import { Server, Socket } from "socket.io";
import os from "os";
import Docker from 'dockerode';
import fs from 'fs';
const MAX_USERS_PER_ROOM = 60;
@ -113,171 +113,116 @@ export const setupWebsocket = (io: Server): void => {
socket.to(roomName).emit("message-sent-student", { message });
});
interface ContainerStats {
containerId: string;
containerName: string;
memoryUsedMB: number | null;
memoryUsedPercentage: number | null;
cpuUsedPercentage: number | null;
error?: string;
}
class ContainerMetrics {
private totalSystemMemory = os.totalmem();
private cgroupv2 = this.isCgroupV2();
private lastCPUUsage = 0;
private lastCPUTime = Date.now();
private docker: Docker;
private containerName: string;
private isCgroupV2(): boolean {
return fs.existsSync('/sys/fs/cgroup/cgroup.controllers');
private bytesToMB(bytes: number): number {
return Math.round(bytes / (1024 * 1024));
}
private readCgroupFile(filepath: string): string {
try {
return fs.readFileSync(filepath, 'utf-8').trim();
} catch (error) {
console.debug(`Could not read ${filepath}`);
return '';
}
constructor() {
this.docker = new Docker({
socketPath: process.platform === 'win32' ? '//./pipe/docker_engine' : '/var/run/docker.sock'
});
this.containerName = `room_${process.env.ROOM_ID}`;
}
private getCgroupCPUUsage(): number {
try {
if (this.cgroupv2) {
const usage = this.readCgroupFile('/sys/fs/cgroup/cpu.stat');
const usageMatch = usage.match(/usage_usec\s+(\d+)/);
if (usageMatch) {
const currentUsage = Number(usageMatch[1]) / 1000000;
const currentTime = Date.now();
const cpuDelta = currentUsage - this.lastCPUUsage;
const timeDelta = (currentTime - this.lastCPUTime) / 1000;
this.lastCPUUsage = currentUsage;
this.lastCPUTime = currentTime;
return (cpuDelta / timeDelta) * 100;
}
}
const cgroupV1Paths = [
'/sys/fs/cgroup/cpu/cpuacct.usage',
'/sys/fs/cgroup/cpuacct/cpuacct.usage',
'/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage'
];
for (const path of cgroupV1Paths) {
const usage = this.readCgroupFile(path);
if (usage) {
const currentUsage = Number(usage) / 1000000000;
const currentTime = Date.now();
const cpuDelta = currentUsage - this.lastCPUUsage;
const timeDelta = (currentTime - this.lastCPUTime) / 1000;
this.lastCPUUsage = currentUsage;
this.lastCPUTime = currentTime;
return (cpuDelta / timeDelta) * 100;
}
}
return this.getFallbackCPUUsage();
} catch (error) {
return this.getFallbackCPUUsage();
}
private async getContainerNetworks(containerId: string): Promise<string[]> {
const container = this.docker.getContainer(containerId);
const info = await container.inspect();
return Object.keys(info.NetworkSettings.Networks);
}
private getFallbackCPUUsage(): number {
public async getAllContainerStats(): Promise<ContainerStats[]> {
try {
const cpus = os.cpus();
return cpus.reduce((acc, cpu) => {
const total = Object.values(cpu.times).reduce((a, b) => a + b);
const idle = cpu.times.idle;
return acc + ((total - idle) / total) * 100;
}, 0) / cpus.length;
} catch (error) {
console.error('Error getting fallback CPU usage:', error);
return 0;
}
}
// First get our container to find its networks
const ourContainer = await this.docker.listContainers({
all: true,
filters: { name: [this.containerName] }
});
private getCgroupMemoryUsage(): { used: number; limit: number } | null {
try {
// First get process memory as baseline
const processMemory = process.memoryUsage();
const baselineMemory = processMemory.rss;
if (this.cgroupv2) {
const memUsage = Number(this.readCgroupFile('/sys/fs/cgroup/memory.current'));
if (!isNaN(memUsage) && memUsage > 0) {
return {
used: Math.max(baselineMemory, memUsage),
limit: this.totalSystemMemory
};
}
if (!ourContainer.length) {
throw new Error(`Container ${this.containerName} not found`);
}
// Try cgroup v1
const v1Paths = {
usage: '/sys/fs/cgroup/memory/memory.usage_in_bytes',
limit: '/sys/fs/cgroup/memory/memory.limit_in_bytes'
};
const ourNetworks = await this.getContainerNetworks(ourContainer[0].Id);
const memoryUsage = Number(this.readCgroupFile(v1Paths.usage));
if (!isNaN(memoryUsage) && memoryUsage > 0) {
return {
used: Math.max(baselineMemory, memoryUsage),
limit: this.totalSystemMemory
};
}
// Fallback to process memory
return {
used: baselineMemory,
limit: this.totalSystemMemory
};
// Get all containers
const allContainers = await this.docker.listContainers();
// Get stats for containers on the same networks
const containerStats = await Promise.all(
allContainers.map(async (container): Promise<ContainerStats | null> => {
try {
const containerNetworks = await this.getContainerNetworks(container.Id);
// Check if container shares any network with our container
if (!containerNetworks.some(network => ourNetworks.includes(network))) {
return null;
}
const stats = await this.docker.getContainer(container.Id).stats({ stream: false });
const memoryStats = {
usage: stats.memory_stats.usage,
limit: stats.memory_stats.limit || 0,
percent: stats.memory_stats.limit ? (stats.memory_stats.usage / stats.memory_stats.limit) * 100 : 0
};
const cpuDelta = stats.cpu_stats?.cpu_usage?.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage || 0);
const systemDelta = stats.cpu_stats?.system_cpu_usage - (stats.precpu_stats?.system_cpu_usage || 0);
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * (stats.cpu_stats?.online_cpus || 1) * 100 : 0;
return {
containerId: container.Id,
containerName: container.Names[0].replace(/^\//, ''),
memoryUsedMB: this.bytesToMB(memoryStats.usage),
memoryUsedPercentage: memoryStats.percent,
cpuUsedPercentage: cpuPercent
};
} catch (error) {
return {
containerId: container.Id,
containerName: container.Names[0].replace(/^\//, ''),
memoryUsedMB: null,
memoryUsedPercentage: null,
cpuUsedPercentage: null,
error: error instanceof Error ? error.message : String(error)
};
}
})
);
// Change the filter to use proper type predicate
return containerStats.filter((stats): stats is ContainerStats => stats !== null);
} catch (error) {
console.debug('Error reading cgroup memory:', error);
return null;
}
}
public getMetrics() {
try {
const mbFactor = 1024 * 1024;
let memoryData = this.getCgroupMemoryUsage();
if (!memoryData) {
const processMemory = process.memoryUsage();
memoryData = {
used: processMemory.rss,
limit: this.totalSystemMemory
};
}
const memoryUsedMB = memoryData.used / mbFactor;
const memoryTotalMB = memoryData.limit / mbFactor;
const memoryPercentage = (memoryData.used / memoryData.limit) * 100;
console.debug(`
Memory Usage: ${memoryUsedMB.toFixed(2)} MB
Memory Total: ${memoryTotalMB.toFixed(2)} MB
Memory %: ${memoryPercentage.toFixed(2)}%
`);
return {
memoryUsedMB: memoryUsedMB.toFixed(2),
memoryUsedPercentage: memoryPercentage.toFixed(2),
cpuUsedPercentage: this.getCgroupCPUUsage().toFixed(2)
};
} catch (error) {
console.error("Error getting metrics:", error);
return {
memoryUsedMB: "0",
memoryUsedPercentage: "0",
cpuUsedPercentage: "0"
};
console.error('Stats error:', error);
return [{
containerId: 'unknown',
containerName: 'unknown',
memoryUsedMB: null,
memoryUsedPercentage: null,
cpuUsedPercentage: null,
error: error instanceof Error ? error.message : String(error)
}];
}
}
}
// Usage in WebSocket setup
const containerMetrics = new ContainerMetrics();
socket.on("get-usage", () => {
socket.on("get-usage", async () => {
try {
const usageData = containerMetrics.getMetrics();
const usageData = await containerMetrics.getAllContainerStats();
socket.emit("usage-data", usageData);
} catch (error) {
socket.emit("error", { message: "Failed to retrieve usage data" });

View file

@ -99,7 +99,10 @@ class DockerRoomProvider extends BaseRoomProvider {
NetworkMode: this.docker_network,
RestartPolicy: {
Name: 'unless-stopped'
}
},
Binds: [
'/var/run/docker.sock:/var/run/docker.sock'
]
},
Env: [
`ROOM_ID=${roomId}`,

View file

@ -0,0 +1 @@
node_modules

View file

@ -1,8 +1,19 @@
BASE_URL=http://host.docker.internal #via Docker
BASE_URL=http://localhost # Via npm
# Target url
BASE_URL=http://msevignyl.duckdns.org
# Connection account
USER_EMAIL=admin@admin.com
USER_PASSWORD=admin
# Stress test parameters
NUMBER_ROOMS=5
USERS_PER_ROOM=60
MAX_MESSAGES=20
CONVERSATION_INTERVAL=1000
# Optionnal
MAX_MESSAGES_ROUND=20
CONVERSATION_INTERVAL=1000
MESSAGE_RESPONSE_TIMEOUT=5000
BATCH_DELAY=1000
BATCH_SIZE=10

51
test/stressTest/README.md Normal file
View file

@ -0,0 +1,51 @@
# Test de Charge - EvalueTonSavoir
Ce conteneur permet d'exécuter des tests de charge sur l'application EvalueTonSavoir.
## Prérequis
- Docker
- Docker Compose
## Configuration
1. Créez un fichier `.env` à partir du modèle `.env.example`:
```bash
copy .env.example .env
```
2. Modifiez les variables dans le fichier .env:
```bash
# URL de l'application cible
BASE_URL=http://votre-url.com
# Compte de connexion
USER_EMAIL=admin@admin.com
USER_PASSWORD=admin
# Paramètres du test de charge
NUMBER_ROOMS=5 # Nombre de salles à créer
USERS_PER_ROOM=60 # Nombre d'utilisateurs par salle
```
#### Paramètres optionnels
Dans le fichier .env, vous pouvez aussi configurer:
```bash
MAX_MESSAGES_ROUND=20 # Messages maximum par cycle
CONVERSATION_INTERVAL=1000 # Intervalle entre les messages (ms)
MESSAGE_RESPONSE_TIMEOUT=5000 # Timeout des réponses (ms)
BATCH_DELAY=1000 # Délai entre les lots (ms)
BATCH_SIZE=10 # Taille des lots d'utilisateurs
```
## Démarrage
Pour lancer le test de charge:
Les résultats seront disponibles dans le dossier output/.
```bash
docker compose up
```

View file

@ -22,18 +22,34 @@ export class Watcher extends RoomParticipant {
try {
this.socket.emit("get-usage");
this.socket.once("usage-data", (data) => {
//console.log(`Watcher ${this.username} received data:`, data);
this.roomRessourcesData.push({ timestamp: Date.now(), ...data });
const timestamp = Date.now();
// Store each container's metrics separately with timestamp
data.forEach(containerStat => {
const existingData = this.roomRessourcesData.find(d => d.containerId === containerStat.containerId);
if (existingData) {
existingData.metrics.push({
timestamp,
...containerStat
});
} else {
this.roomRessourcesData.push({
containerId: containerStat.containerId,
containerName: containerStat.containerName,
metrics: [{
timestamp,
...containerStat
}]
});
}
});
});
} catch (error) {
console.warn(`Error capturing metrics for room ${this.roomName}:`, error.message);
}
} else {
console.warn(`Socket not connected for room ${this.roomName}`);
}
}
startCheckingResources(intervalMs = 250) {
startCheckingResources(intervalMs = 500) {
if (this.checkRessourceInterval) {
console.warn(`Resource checking is already running for room ${this.roomName}.`);
return;

View file

@ -7,16 +7,10 @@ services:
context: .
dockerfile: Dockerfile
container_name: stress-test
networks:
- quiz_network
network_mode: host
env_file:
- .env
ports:
- "9229:9229"
volumes:
- ./output:/app/output
tty: true
stdin_open: true
#command: node --inspect=0.0.0.0:9229 main.js
networks:
quiz_network:
driver: bridge
stdin_open: true

View file

@ -15,21 +15,22 @@ const config = {
password: process.env.USER_PASSWORD || 'admin'
},
rooms: {
count: parseInt(process.env.NUMBER_ROOMS || '2'),
count: parseInt(process.env.NUMBER_ROOMS || '15'),
usersPerRoom: parseInt(process.env.USERS_PER_ROOM || '60'),
batchSize: 5,
batchDelay: 250
batchSize: parseInt(process.env.BATCH_SIZE || 5),
batchDelay: parseInt(process.env.BATCH_DELAY || 250)
},
simulation: {
maxMessages: parseInt(process.env.MAX_MESSAGES || '20'),
maxMessages: parseInt(process.env.MAX_MESSAGES_ROUND || '20'),
messageInterval: parseInt(process.env.CONVERSATION_INTERVAL || '1000'),
responseTimeout: 5000
responseTimeout: parseInt(process.env.MESSAGE_RESPONSE_TIMEOUT || 5000)
}
};
const rooms = new Map();
const metrics = new TestMetrics();
// Changes to setupRoom function
async function setupRoom(token, index) {
try {
const room = await createRoomContainer(config.baseUrl, token);
@ -37,8 +38,9 @@ async function setupRoom(token, index) {
metrics.roomsCreated++;
const teacher = new Teacher(`teacher_${index}`, room.id);
const watcher = new Watcher(`watcher_${index}`, room.id);
// Only create watcher for first room (index 0)
const watcher = index === 0 ? new Watcher(`watcher_${index}`, room.id) : null;
await Promise.all([
teacher.connectToRoom(config.baseUrl)
.then(() => metrics.usersConnected++)
@ -47,16 +49,24 @@ async function setupRoom(token, index) {
metrics.logError('teacherConnection', err);
console.warn(`Teacher ${index} connection failed:`, err.message);
}),
watcher.connectToRoom(config.baseUrl)
.then(() => metrics.usersConnected++)
.catch(err => {
metrics.userConnectionsFailed++;
metrics.logError('watcherConnection', err);
console.warn(`Watcher ${index} connection failed:`, err.message);
})
// Only connect watcher if it exists
...(watcher ? [
watcher.connectToRoom(config.baseUrl)
.then(() => metrics.usersConnected++)
.catch(err => {
metrics.userConnectionsFailed++;
metrics.logError('watcherConnection', err);
console.warn(`Watcher ${index} connection failed:`, err.message);
})
] : [])
]);
const students = Array.from({ length: config.rooms.usersPerRoom - 2 },
// Adjust number of students based on whether room has a watcher
const studentCount = watcher ?
config.rooms.usersPerRoom - 2 : // Room with watcher: subtract teacher and watcher
config.rooms.usersPerRoom - 1; // Rooms without watcher: subtract only teacher
const students = Array.from({ length: studentCount },
(_, i) => new Student(`student_${index}_${i}`, room.id));
rooms.set(room.id, { teacher, watcher, students });
@ -115,7 +125,7 @@ async function simulate() {
}
}, 100);
}),
new Promise((_, reject) =>
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Response timeout')), config.simulation.responseTimeout)
)
]);
@ -133,13 +143,14 @@ async function simulate() {
}
async function generateReport() {
const data = Object.fromEntries(
Array.from(rooms.entries()).map(([id, { watcher }]) => [
id,
watcher.roomRessourcesData
])
);
return generateMetricsReport(data,metrics);
const watcherRoom = Array.from(rooms.entries()).find(([_, room]) => room.watcher);
if (!watcherRoom) {
throw new Error('No watcher found in any room');
}
const data = {
[watcherRoom[0]]: watcherRoom[1].watcher.roomRessourcesData
};
return generateMetricsReport(data, metrics);
}
function cleanup() {
@ -178,6 +189,8 @@ async function main() {
} catch (error) {
metrics.logError('main', error);
console.error('Error:', error.message);
} finally {
cleanup();
}
}

View file

@ -2,126 +2,6 @@ import fs from 'fs';
import path from 'path';
import { ChartJSNodeCanvas } from 'chartjs-node-canvas';
function ensureDirectoryExists(directory) {
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
}
async function saveChart(chartJSNodeCanvas, data, xLabel, yLabel, outputFile, title) {
const chartConfig = {
type: 'line',
data,
options: {
scales: {
x: {
title: { display: true, text: xLabel }
},
y: {
title: { display: true, text: yLabel }
}
},
plugins: {
legend: {
display: true,
position: 'top'
}
}
}
};
const buffer = await chartJSNodeCanvas.renderToBuffer(chartConfig);
fs.writeFileSync(outputFile, buffer);
}
async function generateRoomGraphs(roomId, validRoomData, chartJSNodeCanvas, roomDir) {
const timeLabels = validRoomData.map(d => new Date(parseInt(d.timestamp)).toLocaleTimeString());
await Promise.all([
// Room Memory Usage (MB)
saveChart(chartJSNodeCanvas, {
labels: timeLabels,
datasets: [{
label: `Room ${roomId} Memory (MB)`,
data: validRoomData.map(d => parseFloat(d.memoryUsedMB || 0)),
borderColor: 'blue',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
fill: true,
tension: 0.4
}]
}, 'Time', 'Memory Usage (MB)', path.join(roomDir, 'memory-usage-mb.png'),
`Room ${roomId} Memory Usage in MB`),
// Room Memory Usage (Percentage)
saveChart(chartJSNodeCanvas, {
labels: timeLabels,
datasets: [{
label: `Room ${roomId} Memory %`,
data: validRoomData.map(d => parseFloat(d.memoryUsedPercentage || 0)),
borderColor: 'green',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
fill: true,
tension: 0.4
}]
}, 'Time', 'Memory Usage %', path.join(roomDir, 'memory-usage-percent.png'),
`Room ${roomId} Memory Usage Percentage`),
// Room CPU Usage
saveChart(chartJSNodeCanvas, {
labels: timeLabels,
datasets: [{
label: `Room ${roomId} CPU Usage %`,
data: validRoomData.map(d => parseFloat(d.cpuUsedPercentage || 0)),
borderColor: 'red',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: true,
tension: 0.4
}]
}, 'Time', 'CPU Usage %', path.join(roomDir, 'cpu-usage.png'),
`Room ${roomId} CPU Impact`)
]);
}
async function generateGlobalGraphs(data, chartJSNodeCanvas, globalMetricsDir) {
await Promise.all([
saveChart(chartJSNodeCanvas, {
labels: data.labels,
datasets: [{
label: 'Total System Memory Used (MB)',
data: data.memoryMB,
borderColor: 'blue',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
fill: true,
tension: 0.4
}]
}, 'Time', 'Total Memory Usage (MB)', path.join(globalMetricsDir, 'total-system-memory-mb.png')),
saveChart(chartJSNodeCanvas, {
labels: data.labels,
datasets: [{
label: 'Total System Memory Used %',
data: data.memoryPercentage,
borderColor: 'green',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
fill: true,
tension: 0.4
}]
}, 'Time', 'Total Memory Usage %', path.join(globalMetricsDir, 'total-system-memory-percent.png')),
saveChart(chartJSNodeCanvas, {
labels: data.labels,
datasets: [{
label: 'Total System CPU Usage %',
data: data.cpuPercentage,
borderColor: 'red',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: true,
tension: 0.4
}]
}, 'Time', 'Total CPU Usage %', path.join(globalMetricsDir, 'total-system-cpu.png'))
]);
}
async function saveMetricsSummary(metrics, baseOutputDir) {
const metricsData = metrics.getSummary();
@ -167,121 +47,207 @@ ${Object.entries(metricsData.errors)
);
}
// Common chart configurations
const CHART_CONFIG = {
width: 800,
height: 400,
chartStyles: {
memory: {
borderColor: 'blue',
backgroundColor: 'rgba(54, 162, 235, 0.2)'
},
memoryPercent: {
borderColor: 'green',
backgroundColor: 'rgba(75, 192, 192, 0.2)'
},
cpu: {
borderColor: 'red',
backgroundColor: 'rgba(255, 99, 132, 0.2)'
}
}
};
const createBaseChartConfig = (labels, dataset, xLabel, yLabel) => ({
type: 'line',
data: {
labels,
datasets: [dataset]
},
options: {
scales: {
x: { title: { display: true, text: xLabel }},
y: { title: { display: true, text: yLabel }}
},
plugins: {
legend: { display: true, position: 'top' }
}
}
});
function ensureDirectoryExists(directory) {
!fs.existsSync(directory) && fs.mkdirSync(directory, { recursive: true });
}
async function generateMetricsChart(chartJSNodeCanvas, data, style, label, timeLabels, metric, outputPath) {
const dataset = {
label,
data: data.map(m => m[metric] || 0),
...CHART_CONFIG.chartStyles[style],
fill: true,
tension: 0.4
};
const buffer = await chartJSNodeCanvas.renderToBuffer(
createBaseChartConfig(timeLabels, dataset, 'Time', label)
);
return fs.promises.writeFile(outputPath, buffer);
}
async function generateContainerCharts(chartJSNodeCanvas, containerData, outputDir) {
const timeLabels = containerData.metrics.map(m =>
new Date(m.timestamp).toLocaleTimeString()
);
const chartPromises = [
generateMetricsChart(
chartJSNodeCanvas,
containerData.metrics,
'memory',
`${containerData.containerName} Memory (MB)`,
timeLabels,
'memoryUsedMB',
path.join(outputDir, 'memory-usage-mb.png')
),
generateMetricsChart(
chartJSNodeCanvas,
containerData.metrics,
'memoryPercent',
`${containerData.containerName} Memory %`,
timeLabels,
'memoryUsedPercentage',
path.join(outputDir, 'memory-usage-percent.png')
),
generateMetricsChart(
chartJSNodeCanvas,
containerData.metrics,
'cpu',
`${containerData.containerName} CPU %`,
timeLabels,
'cpuUsedPercentage',
path.join(outputDir, 'cpu-usage.png')
)
];
await Promise.all(chartPromises);
}
async function processSummaryMetrics(containers, timeLabels) {
return timeLabels.map((_, timeIndex) => ({
memoryUsedMB: containers.reduce((sum, container) =>
sum + (container.metrics[timeIndex]?.memoryUsedMB || 0), 0),
memoryUsedPercentage: containers.reduce((sum, container) =>
sum + (container.metrics[timeIndex]?.memoryUsedPercentage || 0), 0),
cpuUsedPercentage: containers.reduce((sum, container) =>
sum + (container.metrics[timeIndex]?.cpuUsedPercentage || 0), 0)
}));
}
export default async function generateMetricsReport(allRoomsData, testMetrics) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const baseOutputDir = `./output/${timestamp}`;
ensureDirectoryExists(baseOutputDir);
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const baseOutputDir = `./output/${timestamp}`;
const dirs = [
baseOutputDir,
path.join(baseOutputDir, 'all-containers'),
path.join(baseOutputDir, 'all-rooms')
];
dirs.forEach(ensureDirectoryExists);
if (testMetrics) {
await saveMetricsSummary(testMetrics, baseOutputDir);
}
const globalMetricsDir = path.join(baseOutputDir, 'global');
ensureDirectoryExists(globalMetricsDir);
const chartJSNodeCanvas = new ChartJSNodeCanvas({ width: 800, height: 400 });
// Process individual room graphs first
for (const [roomId, roomData] of Object.entries(allRoomsData)) {
if (!Array.isArray(roomData)) {
console.warn(`Invalid data format for room ${roomId}`);
continue;
if (testMetrics) {
await saveMetricsSummary(testMetrics, baseOutputDir);
}
const roomDir = path.join(baseOutputDir, `room_${roomId}`);
ensureDirectoryExists(roomDir);
const chartJSNodeCanvas = new ChartJSNodeCanvas(CHART_CONFIG);
const allContainers = Object.values(allRoomsData).flat();
const roomContainers = allContainers.filter(c =>
c.containerName.startsWith('room_')
);
const validRoomData = roomData.filter(d => {
const isValid = d && d.timestamp &&
typeof d.memoryUsedMB !== 'undefined' &&
typeof d.memoryUsedPercentage !== 'undefined' &&
typeof d.cpuUsedPercentage !== 'undefined';
if (!isValid) {
console.warn(`Invalid metric data in room ${roomId}:`, d);
}
return isValid;
});
if (validRoomData.length === 0) {
console.warn(`No valid data for room ${roomId}`);
continue;
if (allContainers.length > 0) {
const timeLabels = allContainers[0].metrics.map(m =>
new Date(m.timestamp).toLocaleTimeString()
);
const summedMetrics = await processSummaryMetrics(allContainers, timeLabels);
await generateContainerSummaryCharts(
chartJSNodeCanvas,
summedMetrics,
timeLabels,
path.join(baseOutputDir, 'all-containers')
);
}
await generateRoomGraphs(roomId, validRoomData, chartJSNodeCanvas, roomDir);
if (roomContainers.length > 0) {
const timeLabels = roomContainers[0].metrics.map(m =>
new Date(m.timestamp).toLocaleTimeString()
);
const summedMetrics = await processSummaryMetrics(roomContainers, timeLabels);
await generateContainerSummaryCharts(
chartJSNodeCanvas,
summedMetrics,
timeLabels,
path.join(baseOutputDir, 'all-rooms')
);
}
// Process individual containers
const containerPromises = Object.values(allRoomsData)
.flat()
.filter(container => !container.containerName.startsWith('room_'))
.map(async containerData => {
const containerDir = path.join(baseOutputDir, containerData.containerName);
ensureDirectoryExists(containerDir);
await generateContainerCharts(chartJSNodeCanvas, containerData, containerDir);
});
await Promise.all(containerPromises);
return { outputDir: baseOutputDir };
} catch (error) {
console.error('Error generating metrics report:', error);
throw error;
}
}
// Process global metrics with time-based averaging
const timeWindows = {};
const timeInterval = 1000; // 250ms windows
const totalRooms = Object.keys(allRoomsData).length;
// Group data into time windows
Object.entries(allRoomsData).forEach(([roomId, roomData]) => {
if (!Array.isArray(roomData)) return;
roomData.forEach(metric => {
if (!metric?.timestamp) return;
const timeWindow = Math.floor(parseInt(metric.timestamp) / timeInterval) * timeInterval;
if (!timeWindows[timeWindow]) {
timeWindows[timeWindow] = {
rooms: new Map(),
roomCount: 0
};
}
if (!timeWindows[timeWindow].rooms.has(roomId)) {
timeWindows[timeWindow].rooms.set(roomId, {
memoryMB: [],
memoryPercentage: [],
cpuPercentage: []
});
timeWindows[timeWindow].roomCount++;
}
const roomMetrics = timeWindows[timeWindow].rooms.get(roomId);
roomMetrics.memoryMB.push(parseFloat(metric.memoryUsedMB || 0));
roomMetrics.memoryPercentage.push(parseFloat(metric.memoryUsedPercentage || 0));
roomMetrics.cpuPercentage.push(parseFloat(metric.cpuUsedPercentage || 0));
});
});
// Process only windows with data from all rooms
const globalMetrics = Object.entries(timeWindows)
.filter(([_, data]) => data.roomCount === totalRooms) // Only windows with all rooms
.map(([timestamp, data]) => {
const totals = Array.from(data.rooms.values()).reduce((acc, room) => {
// Calculate room averages
const memoryMBAvg = room.memoryMB.reduce((a, b) => a + b, 0) / room.memoryMB.length;
const memoryPercentageAvg = room.memoryPercentage.reduce((a, b) => a + b, 0) / room.memoryPercentage.length;
const cpuPercentageAvg = room.cpuPercentage.reduce((a, b) => a + b, 0) / room.cpuPercentage.length;
// Sum room averages
return {
memoryMB: acc.memoryMB + memoryMBAvg,
memoryPercentage: acc.memoryPercentage + memoryPercentageAvg,
cpuPercentage: acc.cpuPercentage + cpuPercentageAvg
};
}, { memoryMB: 0, memoryPercentage: 0, cpuPercentage: 0 });
return {
timestamp: parseInt(timestamp),
...totals
};
})
.sort((a, b) => a.timestamp - b.timestamp);
// Generate global graphs with complete window data
const timeLabels = globalMetrics.map(d => new Date(d.timestamp).toLocaleTimeString());
await generateGlobalGraphs({
labels: timeLabels,
memoryMB: globalMetrics.map(d => d.memoryMB),
memoryPercentage: globalMetrics.map(d => d.memoryPercentage),
cpuPercentage: globalMetrics.map(d => d.cpuPercentage)
}, chartJSNodeCanvas, globalMetricsDir);
return { outputDir: baseOutputDir };
async function generateContainerSummaryCharts(chartJSNodeCanvas, metrics, timeLabels, outputDir) {
await Promise.all([
generateMetricsChart(
chartJSNodeCanvas,
metrics,
'memory',
'Total Memory (MB)',
timeLabels,
'memoryUsedMB',
path.join(outputDir, 'total-memory-usage-mb.png')
),
generateMetricsChart(
chartJSNodeCanvas,
metrics,
'memoryPercent',
'Total Memory %',
timeLabels,
'memoryUsedPercentage',
path.join(outputDir, 'total-memory-usage-percent.png')
),
generateMetricsChart(
chartJSNodeCanvas,
metrics,
'cpu',
'Total CPU %',
timeLabels,
'cpuUsedPercentage',
path.join(outputDir, 'total-cpu-usage.png')
)
]);
}