Introduction
Node.js est un environnement d'exécution JavaScript côté serveur qui repose sur le moteur JavaScript V8 de Google. Il
est utilisé pour développer des applications serveur en back-end d'une application web, des outils en ligne de commande
et des applications desktop. Cependant, la consommation de mémoire peut être un problème pour certaines applications
Node.js, en particulier celles qui manipulent de grandes quantités de données ou des données volumineuses.
Dans cet article, nous allons voir comment optimiser la consommation de mémoire d'une application Node.js en le couplant
avec Rust. Rust est un langage de programmation système qui offre des performances similaires à celles du C++, tout en
offrant une sécurité de mémoire à la compilation. Rust peut être utilisé pour écrire des bibliothèques C/C++ natives
pour Node.js.
Problématique
J'ai développé un logiciel de sauvegarde appelé Woodstock Backup, écrit en TypeScript. Lors du lancement des
sauvegardes, il crée une représentation du système de fichier en mémoire et nécessite une grande quantité de mémoire.
Pour illustrer cela, nous avons reproduit notre cas avec le code suivant :
const filesize = require("filesize.js");
const fs = require("fs");
const { serialize, deserialize } = require("v8");
function randomString(size) {
const buffer = Buffer.alloc(size);
for (let i = 0; i < size; i++) {
buffer[i] = Math.floor(Math.random() * 256);
}
return buffer;
}
function randomNumber() {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
}
const testObject = () => ({
path: randomString(100),
stats: {
ownerId: { low: randomNumber(), high: randomNumber(), unsigned: true },
groupId: { low: randomNumber(), high: randomNumber(), unsigned: true },
size: { low: randomNumber(), high: randomNumber(), unsigned: true },
compressedSize: {
low: randomNumber(),
high: randomNumber(),
unsigned: true,
},
lastRead: { low: randomNumber(), high: randomNumber(), unsigned: true },
lastModified: { low: randomNumber(), high: randomNumber(), unsigned: true },
created: { low: randomNumber(), high: randomNumber(), unsigned: true },
mode: { low: randomNumber(), high: randomNumber(), unsigned: true },
dev: { low: randomNumber(), high: randomNumber(), unsigned: true },
rdev: { low: randomNumber(), high: randomNumber(), unsigned: true },
ino: { low: randomNumber(), high: randomNumber(), unsigned: true },
nlink: { low: randomNumber(), high: randomNumber(), unsigned: true },
},
chunks: [randomString(32), randomString(32), randomString(32)],
sha256: randomString(32),
});
global.gc();
const memoryBefore = process.memoryUsage().heapUsed;
const time = Date.now();
const nbObjects = 1_300_000;
const testArray = new Array(nbObjects);
for (let i = 0; i < nbObjects; i++) {
testArray[i] = testObject();
}
console.log("Creation time: ", Date.now() - time);
global.gc();
const memoryAfter = process.memoryUsage().heapUsed;
console.log("Memory consumption: ", filesize.default(memoryAfter - memoryBefore));
console.log("Memory consumption by objects: ", filesize.default((memoryAfter - memoryBefore) / nbObjects));
const time2 = Date.now();
try {
fs.unlinkSync("test");
} catch (e) {}
const stream = fs.createWriteStream("test");
stream.on("close", () => {
console.log("Write to file time: ", Date.now() - time2);
const stats = fs.statSync("test");
console.log("Size of file on disk: ", filesize.default(stats.size));
console.log(
"Size of object in the file",
filesize.default(stats.size / nbObjects)
);
});
for (const obj of testArray) {
stream.write(serialize(obj));
}
stream.end();
En estimant rapidement la mémoire que la taille de l'objet aurait dû prendre, je l'estime à environ 432 octets (12
nombres de 2*64 bits + 1 octet de boolean + 128 caractères pour les chunks et 100 caractères pour le nom). ...