Woodstock Backup - Optimiser la consommation mémoire de Node.js avec Rust
Posté le 10. May 2023 dans Programmation • Tags: woodstock, backup, sauvegarde, javascript, nodejs
Temps de lecture: 15 min
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");
// Utilisation des méthodes de sérialisation et de désérialisation du moteur V8
const { serialize, deserialize } = require("v8");
// Méthode pour générer une chaîne de caractère contenant des caractères aléatoires
function randomString(size) {
const buffer = Buffer.alloc(size);
for (let i = 0; i < size; i++) {
buffer[i] = Math.floor(Math.random() * 256);
}
return buffer;
}
// Méthode pour générer un nombre aléatoire de la taille d'un nombre de 53 bits
function randomNumber() {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
}
// Création d'un objet de test en javascript contenant que des données aléatoires
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),
});
// Lancement du GC pour s'assurer que la mémoire utilisé ne contient que les objets de test
global.gc();
// On recupère la mémoire utilisé avant le test
const memoryBefore = process.memoryUsage().heapUsed;
// On recupère le temps avant le test (pour mesurer le temps de traitement)
const time = Date.now();
// Création des objets. J'ai du lancer plusieurs fois le script pour trouver une valeur qui ne causé pas de crash de
// Node.JS pour cause de manque de mémoire
const nbObjects = 1_300_000;
const testArray = new Array(nbObjects);
for (let i = 0; i < nbObjects; i++) {
testArray[i] = testObject();
}
// Combien de temps à pris la création des objets
console.log("Creation time: ", Date.now() - time);
// Lancement du GC pour s'assurer que nous n'avons pas d'autres reliquats
global.gc();
// Récupération de la mémoire après le test
const memoryAfter = process.memoryUsage().heapUsed;
console.log("Memory consumption: ", filesize.default(memoryAfter - memoryBefore));
console.log("Memory consumption by objects: ", filesize.default((memoryAfter - memoryBefore) / nbObjects));
// Dans la suite on va écrire un fichier contenant le contenu de la mémoire. Cela a été fait initiallement pour
// s'assurer que le GC ne supprime pas mes objets car non utilisés.
const time2 = Date.now();
// Remove test file if exist
try {
fs.unlinkSync("test");
} catch (e) {}
const stream = fs.createWriteStream("test");
// C'est moche, mais c'est pour tester
stream.on("close", () => {
console.log("Write to file time: ", Date.now() - time2);
// Size of file test on disk
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). ...
Lire la suite ...