Skip to content

Commit

Permalink
chore(aa): reorganize benchmarking
Browse files Browse the repository at this point in the history
  • Loading branch information
naugtur committed Mar 8, 2024
1 parent 68d54b2 commit f3151d1
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 154 deletions.
1 change: 1 addition & 0 deletions packages/aa/package.json
Expand Up @@ -23,6 +23,7 @@
"!tsconfig.json"
],
"scripts": {
"bench": "node ./test/benchmark/index.js",
"lint:deps": "depcheck",
"test": "npm run test:run",
"test:run": "ava"
Expand Down
160 changes: 160 additions & 0 deletions packages/aa/test/bench.js
@@ -0,0 +1,160 @@
const translateHeapStats = (stats = []) => {
const result = {}
for (const { space_name, space_used_size } of stats) {
result[space_name] = space_used_size
}
return result
}

const updateMaxEachKey = (current, update) => {
for (const key in current) {
current[key] = Math.max(current[key], update[key])
}
}

const diffEachKey = (a, b) => {
const result = {}
for (const key in a) {
result[key] = b[key] - a[key]
}
return result
}

const toHumanReadable = (obj) => {
const result = {}
for (const key in obj) {
if (obj[key] > 0) result[key] = `+${(obj[key] / 1024 / 1024).toFixed(4)} MB`
}
return result
}

const recordMemorySpike = (frequency = 10) => {
const v8 = require('node:v8')
const initial = translateHeapStats(v8.getHeapSpaceStatistics())
const result = { ...initial }
const collect = () =>
updateMaxEachKey(result, translateHeapStats(v8.getHeapSpaceStatistics()))
const interval = setInterval(collect, frequency)
return {
collect,
getResult: () => {
clearInterval(interval)
collect()
return toHumanReadable(diffEachKey(initial, result))
},
}
}

const history = (name, value) => {
const fs = require('node:fs')
const { execSync } = require('child_process')
const filePath = `.bench-${name}.log` // the intention is for the file to be gitignored and only referred locally
let log = {}
if (fs.existsSync(filePath)) {
log = JSON.parse(fs.readFileSync(filePath, 'utf8'))
}
const summary = {
current: value,
...log,
}
const commit =
'git:' + execSync('git rev-parse --short HEAD').toString().trim()
log[commit] = value
fs.writeFileSync(filePath, JSON.stringify(log, null, 2))

return summary
}

/**
* Benchmarks a synchronous function.
*
* @param {Function} fn - The synchronous function to be benchmarked.
* @param {string} name - The name of the benchmark.
* @param {number} [iterations=1000] - The number of iterations to run the
* benchmark. Default is `1000`
* @returns {Object} - The benchmark result, including the average time per
* iteration and memory spike.
*/
exports.simpleBench = (fn, name, iterations = 1000) => {
const t0 = performance.now()
for (let i = 0; i < iterations; i++) {
fn()
}
const t1 = performance.now()
return {
current: {
[name]: (t1 - t0) / iterations,
},
}
}
/**
* Benchmarks a synchronous function.
*
* @param {Function} fn - The synchronous function to be benchmarked.
* @param {string} name - The name of the benchmark.
* @param {number} [iterations=1000] - The number of iterations to run the
* benchmark. Default is `1000`
* @returns {Object} - The benchmark result, including the average time per
* iteration and memory spike.
*/
exports.bench = (fn, name, iterations = 1000) => {
const s = recordMemorySpike()
const t0 = performance.now()
for (let i = 0; i < iterations; i++) {
fn()
s.collect()
}
const t1 = performance.now()
return history(name, {
[name]: (t1 - t0) / iterations,
MEMORY_SPIKE: s.getResult(),
})
}

/**
* Benchmarks an asynchronous function.
*
* @param {Function} fn - The asynchronous function to be benchmarked.
* @param {string} name - The name of the benchmark.
* @param {number} [iterations=1000] - The number of iterations to run the
* benchmark. Default is `1000`
* @returns {Object} - The benchmark result, including the average time per
* iteration and memory spike.
*/
exports.benchAsync = async (fn, name, iterations = 1000) => {
const s = recordMemorySpike()
const t0 = performance.now()
for (let i = 0; i < iterations; i++) {
await fn()
s.collect()
}
const t1 = performance.now()
return history(name, {
[name]: (t1 - t0) / iterations,
MEMORY_SPIKE: s.getResult(),
})
}
/**
* Benchmarks multiple asynchronous function calls using Promise.all.
*
* @param {Function} fn - The asynchronous function to be benchmarked.
* @param {string} name - The name of the benchmark.
* @param {number} [iterations=1000] - The number of iterations to run the
* benchmark. Default is `1000`
* @returns {Object} - The benchmark result, including the average time per
* iteration and memory spike.
*/
exports.benchAsyncAll = async (fn, name, iterations = 1000) => {
const s = recordMemorySpike()

const calls = new Array(iterations).fill(0)
const t0 = performance.now()
await Promise.all(calls.map(() => fn()))
const t1 = performance.now()
return history(name, {
[name]: (t1 - t0) / iterations,
MEMORY_SPIKE: s.getResult(),
})
}

exports.recordMemorySpike = recordMemorySpike
23 changes: 23 additions & 0 deletions packages/aa/test/benchmark/index.js
@@ -0,0 +1,23 @@
const path = require('node:path')
const assert = require('node:assert')
const { benchAsync } = require('../bench')
const { loadCanonicalNameMap } = require('../../src/index')

async function run() {
const subject = loadCanonicalNameMap.bind(null, {
rootDir: path.join(__dirname, '..', '..', '..', '..'),
includeDevDeps: true,
})
// prewarm and check if the setup is still correct
const map = await subject()
assert(
map.size > 100,
'Expected map to have many entries. Check the rootDir path. It should point to the workspace root of the repo for this benchmark to be effective.'
)

const result = await benchAsync(subject, `loadCanonicalNameMap`, 10)

console.table(result)
}

run()
28 changes: 0 additions & 28 deletions packages/aa/test/benchmark2.spec.js

This file was deleted.

@@ -1,7 +1,8 @@
const { realpathSync, lstatSync } = require('node:fs')
const path = require('node:path')
const test = require('ava')
const { createProject4Symlink, bench } = require('./utils')
const { createProject4Symlink } = require('./utils')
const { simpleBench } = require('./bench')

function isSymlink(location) {
const info = lstatSync(location)
Expand All @@ -23,18 +24,18 @@ const ratioIsBelow = (a, b, expected) => {
test('[bench] isSymlink is significantly faster than realpathSync in a naive microbenchmark', async (t) => {
await createProject4Symlink()
const results = {
...bench(() => {
...simpleBench(() => {
isSymlink(symlink)
}, 'isSymlink(symlink)'),
...bench(() => {
}, 'isSymlink(symlink)').current,
...simpleBench(() => {
isSymlink(notsymlink)
}, 'isSymlink(notsymlink)'),
...bench(() => {
}, 'isSymlink(notsymlink)').current,
...simpleBench(() => {
realpathSync(symlink)
}, 'realpathSync(symlink)'),
...bench(() => {
}, 'realpathSync(symlink)').current,
...simpleBench(() => {
realpathSync(notsymlink)
}, 'realpathSync(notsymlink)'),
}, 'realpathSync(notsymlink)').current,
}

t.log({ results })
Expand Down
117 changes: 0 additions & 117 deletions packages/aa/test/utils.js
Expand Up @@ -2,71 +2,6 @@ const path = require('node:path')
const { symlink } = require('node:fs/promises')
const { existsSync } = require('node:fs')

/**
* @param {{space_name: string, space_used_size: number}[]} stats
*/
const translateHeapStats = (stats = []) => {
const result = {}
for (const { space_name, space_used_size } of stats) {
result[space_name] = space_used_size
}
return result
}

/**
* @param {Record<string, number>} current
* @param {Record<string, number>} update
*/
const updateMaxEachKey = (current, update) => {
for (const key in current) {
current[key] = Math.max(current[key], update[key])
}
}

/**
* @param {Record<string, number>} a
* @param {Record<string, number>} b
*/
const diffEachKey = (a, b) => {
/** @type {Record<string, number>} */
const result = {}
for (const key in a) {
result[key] = b[key] - a[key]
}
return result
}

/**
* @param {Record<string, number>} obj
*/
const toHumanReadable = (obj) => {
const result = {}
for (const key in obj) {
if (obj[key] > 0) result[key] = `+${(obj[key] / 1024 / 1024).toFixed(4)} MB`
}
return result
}

/**
* @param {number} frequency
*/
const recordMemorySpike = (frequency = 10) => {
const v8 = require('v8')
const initial = translateHeapStats(v8.getHeapSpaceStatistics())
const result = { ...initial }
const collect = () =>
updateMaxEachKey(result, translateHeapStats(v8.getHeapSpaceStatistics()))
const interval = setInterval(collect, frequency).unref()
return {
collect,
getResult: () => {
clearInterval(interval)
collect()
return toHumanReadable(diffEachKey(initial, result))
},
}
}

/**
* Normalizes paths in canonical name map entries
*
Expand All @@ -91,55 +26,3 @@ exports.createProject4Symlink = async () => {
const target = path.join(__dirname, 'projects', '4', 'packages', 'aaa')
await exports.osIndependentSymlink(target, src)
}

/**
* @param {(() => void)} fn
* @param {string} name
* @param {iterations} number
* @returns {{MEMORY_SPIKE: string, [x: string]: number}}
*/
exports.bench = (fn, name, iterations = 10000) => {
const s = recordMemorySpike()
const t0 = performance.now()
for (let i = 0; i < iterations; i++) {
fn()
s.collect()
}
const t1 = performance.now()
return { [name]: (t1 - t0) / iterations, MEMORY_SPIKE: s.getResult() }
}

/**
* @param {(() => void | Promise<void>)} fn
* @param {string} name
* @param {iterations} number
* @returns {Promise<{MEMORY_SPIKE: string, [x: string]: number}>
*/
exports.benchAsync = async (fn, name, iterations = 1000) => {
const s = recordMemorySpike()
const t0 = performance.now()
for (let i = 0; i < iterations; i++) {
await fn()
s.collect()
}
const t1 = performance.now()
return { [name]: (t1 - t0) / iterations, MEMORY_SPIKE: s.getResult() }
}

/**
* @param {(() => void | Promise<void>)} fn
* @param {string} name
* @param {iterations} number
* @returns {Promise<{MEMORY_SPIKE: string, [x: string]: number}>
*/
exports.benchAsyncAll = async (fn, name, iterations = 1000) => {
const s = recordMemorySpike()

const calls = new Array(iterations).fill(0)
const t0 = performance.now()
await Promise.all(calls.map(() => fn()))
const t1 = performance.now()
return { [name]: (t1 - t0) / iterations, MEMORY_SPIKE: s.getResult() }
}

exports.recordMemorySpike = recordMemorySpike

0 comments on commit f3151d1

Please sign in to comment.