Skip to content

Instantly share code, notes, and snippets.

@billywhizz
Last active May 18, 2024 22:37
Show Gist options
  • Save billywhizz/1d6fee4d3aea1030324a7a93de444d31 to your computer and use it in GitHub Desktop.
Save billywhizz/1d6fee4d3aea1030324a7a93de444d31 to your computer and use it in GitHub Desktop.
JS Runtime ReadFileSync Investigation

This is a small benchmark to test JS runtime performance reading text files of varying sizes from the filesystem. It doesn't make any attempt to clear OS caches before reading files and is more interested in measuring any differences in overhead introduced by the different JS runtimes.

to run, install Deno, node.js and Bun

node read_as_text.mjs
deno run -A read_as_text.mjs
bun read_as_text.mjs

Results from a test run on Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz cpu, Linux inspiron 6.5.0-28-generic #29~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC linux.

  • node.js: v22.2.0
  • bun: 1.1.8
  • deno: 1.43.3
$ bun read_as_text.mjs 
readFileSync 256     time 1991 rate 150614 ns/iter 6639.49 rss 169140 usr  35 sys   64 tot  99
readFileSync 256     time 1959 rate 153083 ns/iter 6532.41 rss 290204 usr  35 sys   62 tot  97
readFileSync 256     time 1963 rate 152750 ns/iter 6546.65 rss 408988 usr  35 sys   62 tot  97
readFileSync 256     time 1958 rate 153188 ns/iter 6527.93 rss 530460 usr  37 sys   61 tot  98
readFileSync 256     time 1957 rate 153293 ns/iter 6523.46 rss 649116 usr  34 sys   63 tot  97
$ node read_as_text.mjs 
readFileSync 256     time 1819 rate 164895 ns/iter 6064.47 rss 55372 usr  44 sys   57 tot 101
readFileSync 256     time 1810 rate 165666 ns/iter 6036.25 rss 55532 usr  39 sys   59 tot  98
readFileSync 256     time 1784 rate 168089 ns/iter 5949.23 rss 55532 usr  39 sys   58 tot  97
readFileSync 256     time 1796 rate 166961 ns/iter 5989.43 rss 55532 usr  37 sys   60 tot  97
readFileSync 256     time 1781 rate 168379 ns/iter 5938.99 rss 55532 usr  40 sys   57 tot  97
$ deno run -A read_as_text.mjs 
readFileSync 256     time 3830 rate 78322 ns/iter 12767.81 rss 63292 usr  51 sys   50 tot 101
readFileSync 256     time 3788 rate 79194 ns/iter 12627.22 rss 63292 usr  50 sys   50 tot 100
readFileSync 256     time 3794 rate 79065 ns/iter 12647.83 rss 63420 usr  53 sys   47 tot 100
readFileSync 256     time 3789 rate 79167 ns/iter 12631.53 rss 63420 usr  53 sys   47 tot 100
readFileSync 256     time 3775 rate 79466 ns/iter 12584 rss 63548 usr  50 sys   50 tot 100

If we use strace -cf to look at the syscalls, we can see deno is doing 7 syscalls for every file, bun is doing 5 syscalls and node.js is doing only 4.

$ strace -cf bun read_as_text.mjs 256 10000

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- -------------------
 75.29    1.797961        4939       364        71 futex
  8.83    0.210958           2    100051           read
  6.94    0.165726           3     50036         1 openat
  4.35    0.103775           2     50033           close
  4.06    0.096926           1     50013           fstat

$ strace -cf node read_as_text.mjs 256 10000

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ------------------
 84.64    7.493321       24812       302        11 futex
 10.63    0.940835       52268        18           epoll_pwait
  1.93    0.171243           1    100059           read
  1.52    0.134423           2     50034           openat
  1.27    0.112390           2     50043           close

$ strace -cf deno run -A read_as_text.mjs 256 10000

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ------------------
 92.51    9.148326       13335       686       127 futex
  1.78    0.175890           1    100114           read
  1.63    0.161213           3     50066        28 openat
  1.22    0.120309           2     50042           close
  1.05    0.103404           2     50017           statx
  1.00    0.099123           1     50009           getcwd
  0.74    0.072838           1     50073           lseek
function is_a_tty () {
if (globalThis.Deno) return Deno.isatty(1)
if (globalThis.lo) return lo.core.isatty(1)
return process.stdout.isTTY
}
const isatty = is_a_tty()
const AD = isatty ? '\u001b[0m' : '' // ANSI Default
const A0 = isatty ? '\u001b[30m' : '' // ANSI Black
const AR = isatty ? '\u001b[31m' : '' // ANSI Red
const AG = isatty ? '\u001b[32m' : '' // ANSI Green
const AY = isatty ? '\u001b[33m' : '' // ANSI Yellow
const AB = isatty ? '\u001b[34m' : '' // ANSI Blue
const AM = isatty ? '\u001b[35m' : '' // ANSI Magenta
const AC = isatty ? '\u001b[36m' : '' // ANSI Cyan
const AW = isatty ? '\u001b[37m' : '' // ANSI White
const colors = { AD, AG, AY, AM, AD, AR, AB, AC, AW, A0 }
class Stats {
recv = 0
send = 0
conn = 0
log () {
const { send, recv, conn } = this
const [ usr, , sys ] = cputime()
console.log(`${AC}send${AD} ${to_size_string(send)} ${AC}recv${AD} ${to_size_string(recv)} ${AC}rss${AD} ${mem()} ${AC}con${AD} ${conn} ${AY}usr${AD} ${usr.toString().padStart(3, ' ')} ${AY}sys${AD} ${sys.toString().padStart(3, ' ')} ${AY}tot${AD} ${(usr + sys).toString().padStart(3, ' ')}`)
this.send = this.recv = 0
}
get runtime () {
return lo.hrtime() - lo.start
}
}
function pad (v, size, precision = 0) {
return v.toFixed(precision).padStart(size, ' ')
}
async function wrap_mem_usage () {
if (globalThis.Deno) {
if (Deno.build.os !== 'linux') return () => 0
const mem = () => Math.floor((Number((new TextDecoder()).decode(Deno.readFileSync('/proc/self/stat')).split(' ')[23]) * 4096) / (1024))
let lastusr = 0
let lastsys = 0
const decoder = new TextDecoder()
const cputime = () => {
const bytes = Deno.readFileSync('/proc/self/stat')
const str = decoder.decode(bytes)
const parts = str.split(' ')
const usr = Number(parts[13])
const sys = Number(parts[14])
//const uptime = Number(parts[21])
const res = [usr - lastusr, 0, sys - lastsys]
lastusr = usr
lastsys = sys
return res
}
return { mem, cputime }
}
if (globalThis.Bun) {
if (require('node:os').platform() !== 'linux') return () => 0
const fs = require('node:fs')
const mem = () => Math.floor((Number((new TextDecoder()).decode(fs.readFileSync('/proc/self/stat')).split(' ')[23]) * 4096) / (1024))
//let lastuptime = 0
let lastusr = 0
let lastsys = 0
const decoder = new TextDecoder()
const cputime = () => {
const bytes = fs.readFileSync('/proc/self/stat')
const str = decoder.decode(bytes)
const parts = str.split(' ')
const usr = Number(parts[13])
const sys = Number(parts[14])
//const uptime = Number(parts[21])
const res = [usr - lastusr, 0, sys - lastsys]
lastusr = usr
lastsys = sys
return res
}
return { mem, cputime }
}
if (globalThis.process) {
// node.js
const os = await import('os')
if (os.platform() !== 'linux') return () => 0
const fs = await import('fs')
const mem = () => Math.floor((Number((new TextDecoder()).decode(fs.readFileSync('/proc/self/stat')).split(' ')[23]) * 4096) / (1024))
let lastusr = 0
let lastsys = 0
const decoder = new TextDecoder()
const cputime = () => {
const bytes = fs.readFileSync('/proc/self/stat')
const str = decoder.decode(bytes)
const parts = str.split(' ')
const usr = Number(parts[13])
const sys = Number(parts[14])
//const uptime = Number(parts[21])
const res = [usr - lastusr, 0, sys - lastsys]
lastusr = usr
lastsys = sys
return res
}
return { mem, cputime }
}
if (globalThis.lo) {
const { mem, cputime } = await import('lib/proc.js')
return { mem: () => mem() / 1024, cputime }
}
}
function to_size_string (bytes) {
if (bytes < 1000) {
return `${bytes.toFixed(2)} Bps`
} else if (bytes < 1000 * 1000) {
return `${(Math.floor((bytes / 1000) * 100) / 100).toFixed(2)} KBps`
} else if (bytes < 1000 * 1000 * 1000) {
return `${(Math.floor((bytes / (1000 * 1000)) * 100) / 100).toFixed(2)} MBps`
}
return `${(Math.floor((bytes / (1000 * 1000 * 1000)) * 100) / 100).toFixed(2)} GBps`
}
function formatNanos (nanos) {
if (nanos >= 1000000000) return `${AY}sec/iter${AD} ${pad((nanos / 1000000000), 10, 2)}`
if (nanos >= 1000000) return `${AY}ms/iter${AD} ${pad((nanos / 1000000), 10, 2)}`
if (nanos >= 1000) return `${AY}μs/iter${AD} ${pad((nanos / 1000), 10, 2)}`
return `${AY}ns/iter${AD} ${pad(nanos, 10, 2)}`
}
function bench (name, fn, count, after = noop) {
const start = performance.now()
for (let i = 0; i < count; i++) fn()
const elapsed = (performance.now() - start)
const rate = Math.floor(count / (elapsed / 1000))
const nanos = 1000000000 / rate
const rss = mem()
console.log(`${name.slice(0, 32).padEnd(17, ' ')} ${pad(Math.floor(elapsed), 6)} ms ${AG}rate${AD} ${pad(rate, 10)} ${formatNanos(nanos)} ${AG}rss${AD} ${rss}`)
after()
return { name, count, elapsed, rate, nanos, rss, runtime }
}
async function benchAsync (name, fn, count, after = noop) {
const start = performance.now()
for (let i = 0; i < count; i++) await fn()
const elapsed = (performance.now() - start)
const rate = Math.floor((count / (elapsed / 1000)) * 100) / 100
const nanos = 1000000000 / rate
const rss = mem()
console.log(`${name.slice(0, 32).padEnd(17, ' ')} ${pad(Math.floor(elapsed), 6)} ms ${AG}rate${AD} ${pad(rate, 10)} ${formatNanos(nanos)} ${AG}rss${AD} ${rss}`)
after()
return { name, count, elapsed, rate, nanos, rss, runtime }
}
const runAsync = async (name, fn, count, repeat = 10, after = () => {}) => {
const runs = []
for (let i = 0; i < repeat; i++) {
runs.push(await benchAsync(name, fn, count, after))
}
return runs
}
const run = (name, fn, count, repeat = 10, after = () => {}) => {
const runs = []
for (let i = 0; i < repeat; i++) {
runs.push(bench(name, fn, count, after))
}
return runs
}
function arrayEquals (a, b) {
return Array.isArray(a) &&
Array.isArray(b) &&
a.length === b.length &&
a.every((val, index) => val === b[index])
}
class Bench {
#start = 0
#end = 0
#name = 'bench'
#display = true
#name_width = 20
constructor (display = true) {
this.#display = display
}
set name_width (len) {
this.#name_width = len
}
start (name = 'bench') {
this.#name = name.slice(0, 32).padEnd(32, ' ')
this.#start = performance.now()
}
end (count = 0) {
this.#end = performance.now()
const elapsed = this.#end - this.#start
const rate = Math.floor(count / (elapsed / 1000))
const nanos = Math.ceil((1000000000 / rate) * 100) / 100
const rss = mem()
//if (this.#display) console.log(`${this.#name} ${pad(Math.floor(elapsed), 6)} ms ${AG}rate${AD} ${pad(rate, 10)} ${formatNanos(nanos)} ${AG}rss${AD} ${rss}`)
let [ usr, , sys ] = cputime()
const cpu_time = elapsed / 10
usr = Math.floor((usr / cpu_time) * 100)
sys = Math.floor((sys / cpu_time) * 100)
if (this.#display) console.log(`${AM}${this.#name.trim().padEnd(this.#name_width, ' ')}${AD} ${AY}time${AD} ${Math.floor(elapsed)} ${AY}rate${AD} ${rate} ${AG}ns/iter${AD} ${nanos} ${AG}rss${AD} ${rss} ${AY}usr${AD} ${usr.toString().padStart(3, ' ')} ${AY}sys${AD} ${sys.toString().padStart(3, ' ')} ${AY}tot${AD} ${(usr + sys).toString().padStart(3, ' ')}`)
return { name: this.#name.trim(), count, elapsed, rate, nanos, rss, runtime }
}
}
const runtime = { name: '', version: '' }
if (globalThis.Deno) {
globalThis.args = Deno.args
runtime.name = 'deno'
runtime.version = Deno.version.deno
runtime.v8 = Deno.version.v8
globalThis.readFileAsText = async fn => decoder.decode(Deno.readFileSync(fn))
} else if (globalThis.lo) {
globalThis.performance = { now: () => lo.hrtime() / 1000000 }
globalThis.assert = lo.assert
globalThis.args = lo.args.slice(2)
runtime.name = 'lo'
runtime.version = lo.version.lo
runtime.v8 = lo.version.v8
const { readFile } = lo.core
globalThis.readFileAsText = async fn => decoder.decode(readFile(fn))
} else if (globalThis.Bun) {
globalThis.args = Bun.argv.slice(2)
runtime.name = 'bun'
runtime.version = Bun.version
globalThis.readFileAsText = async fn => (await Bun.file(fn).text())
} else if (globalThis.process) {
globalThis.args = process.argv.slice(2)
runtime.name = 'node'
runtime.version = process.version
runtime.v8 = process.versions.v8
const fs = await import('fs')
globalThis.readFileAsText = async fn => decoder.decode(fs.readFileSync(fn))
}
globalThis.colors = colors
globalThis.arrayEquals = arrayEquals
const noop = () => {}
const { mem, cputime } = await wrap_mem_usage()
const decoder = new TextDecoder()
if (!globalThis.assert) {
function assert (condition, message, ErrorType = Error) {
if (!condition) {
throw new ErrorType(message || "Assertion failed")
}
}
globalThis.assert = assert
}
export { pad, formatNanos, colors, run, runAsync, Bench, mem, runtime, to_size_string, Stats, cputime }
import { Bench } from './bench.mjs'
import { readFileSync, writeFileSync } from 'node:fs'
const file_name = './test.bin'
const size = parseInt(args[0] || '256', 10)
const runs = parseInt(args[1] || '300000', 10)
const encoder = new TextEncoder()
const str = 'A'.repeat(size)
writeFileSync(file_name, encoder.encode(str))
const bench = new Bench()
let iter = 5
for (let i = 0; i < iter; i++) {
bench.start(`readFileSync ${size}`)
for (let j = 0; j < runs; j++) {
assert(str === readFileSync(file_name, 'utf-8'))
}
bench.end(runs)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment