RSS
RSS is a reasonable measure for the "total memory usage of the Node.js interpreter process". You simply be able to run your program if that goes above the available RAM. Note however that it excludes some types of memory, so the actual memory consumption on a server that just runs a single process could be higher (VSZ is the worst case).
The concept of RSS is defined in the Linux kernel itself as mentioned at: What is RSS and VSZ in Linux memory management and measures the total memory usage of the process. This value can therefore be measured by external programs such as ps
without knowledge of Node.js internals, e.g. as shown at: Retrieve CPU usage and memory usage of a single process on Linux?
heapTotal
and heapUsed
These are concepts internal to the Node.js implementation. It would be good to look at the v8 source code to understand them more precisely, notably I wonder if they just obtain those values from glibc with functions such as those mentioned at: API call to get current heap size of process? of if it has its own heap management done on top of it.
For the concept of heap in general see also: What and where are the stack and heap? and What is the function of the push / pop instructions used on registers in x86 assembly? The heap is overwhelmingly likely to take the majority of memory in a JavaScript program, I don't think you will ever bother to try and look for that memory elsewhere (besides perhaps typed arrays perhaps, which show separately under process.memoryUsage()
).
Runnable test
The following code example can be used to do simple tests which I have tried to analyze at: https://cirosantilli.com/javascript-memory-usage-benchmark But unlike languages without garbage collection like C++, it is very difficult to predict why memory usage is so overblown sometimes, especially when we have smaller numbers of objects. I'm not sure other garbage collected languages do any better though.
You have to run the program with:
node --expose-gc main.js
main.js
#!/usr/bin/env node
// CLI arguments.
let arr = false
let array_buffer = false
let dealloc = false
let klass = false
let obj = false
let n = 1000000
let objn = 0
for (let i = 2; i < process.argv.length; i++) {
switch (process.argv[i]) {
case 'arr':
arr = true
break
case 'array-buffer':
array_buffer = true
break
case 'class':
klass = true
break
case 'dealloc':
dealloc = true
break
case 'obj':
obj = true
break
case 'n':
i++
n = parseInt(process.argv[i], 10)
break
case 'objn':
i++
objn = parseInt(process.argv[i], 10)
break
default:
console.error(`unknown option: ${process.argv[i]}`);
break
}
}
class MyClass {
constructor(a, b) {
this.a = a
this.b = b
}
}
let a
if (array_buffer) {
a = new Int32Array(new ArrayBuffer(n * 4))
for (let i = 0; i < n; i++) {
a[i] = i
}
} else if (obj) {
a = []
for (let i = 0; i < n; i++) {
a.push({ a: i, b: -i })
}
} else if (objn) {
a = []
for (let i = 0; i < n; i++) {
const obj = {}
for (let j = 0; j < objn; j++) {
obj[String.fromCharCode(65 + j)] = i
}
a.push(obj)
}
} else if (klass) {
a = []
for (let i = 0; i < n; i++) {
a.push({ a: i, b: -i })
}
} else if (klass) {
a = []
for (let i = 0; i < n; i++) {
a.push(new MyClass(i, -i))
}
} else if (arr) {
a = []
for (let i = 0; i < n; i++) {
a.push([i, -i])
}
} else {
a = []
for (let i = 0; i < n; i++) {
a.push(i)
}
}
if (dealloc) {
a = undefined
}
let j
while (true) {
if (!dealloc) {
j = 0
// The collector somehow removes a if we don't reference it here.
for (let i = 0; i < n; i++) {
if (obj || klass) {
j += a[i].a + a[i].b
} else if (objn) {
const obj = a[i]
for (let k = 0; k < objn; k++) {
j += obj[String.fromCharCode(65 + k)]
}
} else if (arr) {
j += a[i][0] + a[i][1]
} else {
j += a[i]
}
}
console.error(j)
}
global.gc()
console.error(process.memoryUsage())
}
Some things we learn on Node 16 Ubuntu 21.10:
- with
node --expose-gc bench_mem.js n 1
we see that the minimum RSS is 30 MiB and the minimum heapUsed
3.7 MB. RSS for a C hello world on the same system is 770 kB for comparison