Skip to content

Commit 5e20d59

Browse files
committed
feat: add --ram option to measure RAM
1 parent c586567 commit 5e20d59

File tree

9 files changed

+248
-43
lines changed

9 files changed

+248
-43
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Options
2727
--debug, -d Run a development build instead of a production build to aid debugging.
2828
--devtools, -t Run Chrome in windowed mode with the devtools open.
2929
--cpuThrottle=X Run Chrome with CPU throttled X times.
30+
--ram, -r Also measures RAM consumption.
3031
--version Prints the version.
3132
--help Prints this message.
3233
@@ -77,7 +78,7 @@ Path to the benchmark file to run. See the [Usage](#usage) section for more deta
7778
#### options
7879

7980
Type: `Object`
80-
Default: `{ debug: false, devtools: false, cpuThrottle: 1 }`
81+
Default: `{ debug: false, devtools: false, cpuThrottle: 1, isRamMeasured: false }`
8182

8283
Optional object containing additional options.
8384

@@ -102,6 +103,16 @@ Default: `1`
102103

103104
Run Chrome with CPU throttled X times. Useful to receive more precise results between runs.
104105

106+
##### isRamMeasured
107+
108+
Type: `boolean`<br>
109+
Default: `false`
110+
111+
If `true` RAM measurement is enabled. In this case, 2 metrics are being recorded between the runs:
112+
113+
- Heap size (`JSHeapUsedSize`) represents how much RAM was consumed at the end of a test iteration.
114+
- `Object.prototype` represents how many objects were created in RAM at the end of a test iteration. **Why is it interesting?** JS engine sometimes optimizes a code in different ways which in turn changes its memory footprint. [source / look at the "Counting all the objects" section](https://media-codings.com/articles/automatically-detect-memory-leaks-with-puppeteer)
115+
105116
### Events
106117

107118
#### webpack

lib/chrome.js

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,39 @@
22
const EventEmitter = require('events')
33
const puppeteer = require('puppeteer')
44

5+
/** https://media-codings.com/articles/automatically-detect-memory-leaks-with-puppeteer */
6+
const countObjects = async (page) => {
7+
const prototype = await page.evaluateHandle(() => {
8+
return Object.prototype
9+
})
10+
const objects = await page.queryObjects(prototype)
11+
const numberOfObjects = await page.evaluate(
12+
(instances) => instances.length,
13+
objects
14+
)
15+
16+
await prototype.dispose()
17+
await objects.dispose()
18+
19+
return numberOfObjects
20+
}
21+
522
module.exports = class Chrome extends EventEmitter {
623
constructor() {
724
super()
825

926
this.chrome = null
1027
}
1128

12-
async start(port, devtools, { cpuThrottle }) {
29+
async start(port, devtools, { cpuThrottle, isRamMeasured }) {
1330
let completed = false
14-
15-
this.chrome = await puppeteer.launch({ devtools })
31+
const chromeArgs = []
32+
if (isRamMeasured) {
33+
chromeArgs.push('--js-flags=--expose-gc')
34+
}
35+
this.chrome = await puppeteer.launch({ devtools, args: chromeArgs })
36+
const heapSizeMeasurements = []
37+
const objectCountMeasurements = []
1638
const page = await this.chrome.newPage()
1739
const client = await page.target().createCDPSession()
1840

@@ -57,15 +79,33 @@ module.exports = class Chrome extends EventEmitter {
5779
this.emit('error', error)
5880
})
5981

60-
page.exposeFunction('benchmarkProgress', (data) => {
82+
page.exposeFunction('benchmarkProgress', async (data) => {
6183
const benchmark = JSON.parse(data)
62-
this.emit('progress', benchmark)
84+
if (isRamMeasured) {
85+
// eslint-disable-next-line no-undef
86+
await page.evaluate(() => gc())
87+
const { JSHeapUsedSize } = await page.metrics()
88+
heapSizeMeasurements.push(JSHeapUsedSize)
89+
const n = await countObjects(page)
90+
objectCountMeasurements.push(n)
91+
}
92+
this.emit(
93+
'progress',
94+
benchmark,
95+
heapSizeMeasurements,
96+
objectCountMeasurements
97+
)
6398
})
6499

65100
page.exposeFunction('benchmarkComplete', (data) => {
66101
const benchmark = JSON.parse(data)
67102
completed = true
68-
this.emit('complete', benchmark)
103+
this.emit(
104+
'complete',
105+
benchmark,
106+
heapSizeMeasurements,
107+
objectCountMeasurements
108+
)
69109
})
70110

71111
this.emit('start')

lib/cli.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Options
1616
--debug, -d Run a development build instead of a production build to aid debugging.
1717
--devtools, -t Run Chrome in windowed mode with the devtools open.
1818
--cpuThrottle=X Run Chrome with CPU throttled X times.
19+
--ram, -r Also measures RAM consumption.
1920
--version Prints the version.
2021
--help Prints this message.
2122
@@ -37,6 +38,11 @@ Examples
3738
type: 'number',
3839
default: 1,
3940
},
41+
ram: {
42+
type: 'boolean',
43+
default: false,
44+
alias: 'r',
45+
},
4046
},
4147
})
4248

@@ -49,7 +55,7 @@ async function main() {
4955
}
5056

5157
const [filepath] = cli.input
52-
const { debug, devtools, cpuThrottle } = cli.flags
58+
const { debug, devtools, cpuThrottle, ram } = cli.flags
5359

5460
spinner = ora().start()
5561

@@ -72,8 +78,8 @@ async function main() {
7278
spinner.text = 'Starting benchmark '
7379
})
7480

75-
reactBenchmark.on('progress', (benchmark) => {
76-
spinner.text = formatBenchmark(benchmark)
81+
reactBenchmark.on('progress', (...a) => {
82+
spinner.text = formatBenchmark(...a)
7783
})
7884

7985
reactBenchmark.on('console', (log) => {
@@ -87,10 +93,11 @@ async function main() {
8793
debug,
8894
devtools,
8995
cpuThrottle,
96+
isRamMeasured: ram,
9097
})
9198

9299
spinner.stop()
93-
console.log(formatBenchmark(result))
100+
console.log(formatBenchmark(...result))
94101
}
95102

96103
main().catch((error) => {

lib/format-benchmark.js

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,105 @@
22
const humanizeNumber = require('humanize-number')
33
const pluralize = require('pluralize')
44

5-
module.exports = (benchmark) => {
5+
/**
6+
* Computes the arithmetic mean of a sample.
7+
* https://github.com/bestiejs/benchmark.js/blob/42f3b732bac3640eddb3ae5f50e445f3141016fd/benchmark.js
8+
* @private
9+
* @param {Array} sample The sample.
10+
* @returns {number} The mean.
11+
*/
12+
function getMean(sample = []) {
13+
return sample.reduce((sum, x) => sum + x, 0) / sample.length
14+
}
15+
16+
function bytesToSize(bytes) {
17+
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
18+
if (bytes == 0) return '0 Byte'
19+
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)))
20+
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]
21+
}
22+
23+
/**
24+
* T-Distribution two-tailed critical values for 95% confidence.
25+
* For more info see http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm.
26+
*/
27+
const tTable = {
28+
1: 12.706,
29+
2: 4.303,
30+
3: 3.182,
31+
4: 2.776,
32+
5: 2.571,
33+
6: 2.447,
34+
7: 2.365,
35+
8: 2.306,
36+
9: 2.262,
37+
10: 2.228,
38+
11: 2.201,
39+
12: 2.179,
40+
13: 2.16,
41+
14: 2.145,
42+
15: 2.131,
43+
16: 2.12,
44+
17: 2.11,
45+
18: 2.101,
46+
19: 2.093,
47+
20: 2.086,
48+
21: 2.08,
49+
22: 2.074,
50+
23: 2.069,
51+
24: 2.064,
52+
25: 2.06,
53+
26: 2.056,
54+
27: 2.052,
55+
28: 2.048,
56+
29: 2.045,
57+
30: 2.042,
58+
infinity: 1.96,
59+
}
60+
61+
/** https://github.com/bestiejs/benchmark.js/blob/42f3b732bac3640eddb3ae5f50e445f3141016fd/benchmark.js */
62+
function getRme(sample, mean) {
63+
const varOf = function (sum, x) {
64+
return sum + Math.pow(x - mean, 2)
65+
}
66+
// Compute the sample variance (estimate of the population variance).
67+
const variance = sample.reduce(varOf, 0) / (sample.length - 1) || 0
68+
// Compute the sample standard deviation (estimate of the population standard deviation).
69+
const sd = Math.sqrt(variance)
70+
// Compute the standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean).
71+
const sem = sd / Math.sqrt(sample.length)
72+
// Compute the degrees of freedom.
73+
const df = sample.length - 1
74+
// Compute the critical value.
75+
const critical = tTable[Math.round(df) || 1] || tTable.infinity
76+
// Compute the margin of error.
77+
const moe = sem * critical
78+
// Compute the relative margin of error.
79+
const rme = (moe / mean) * 100 || 0
80+
return rme
81+
}
82+
83+
module.exports = (benchmark, heapSizeMeasurements, objectCountMeasurements) => {
684
const ops = benchmark.hz // Can be null on the first run if it executes really quickly
785
? humanizeNumber(benchmark.hz.toFixed(benchmark.hz < 100 ? 2 : 0))
886
: 0
987
const marginOfError = benchmark.stats.rme.toFixed(2)
1088
const runs = pluralize('run', benchmark.stats.sample.length, true)
11-
return `${ops} ops/sec ±${marginOfError}% (${runs} sampled)`
89+
let s = `${runs} sampled: ${ops} ops/sec ±${marginOfError}%`
90+
if (heapSizeMeasurements && heapSizeMeasurements.length) {
91+
const averageRam = getMean(heapSizeMeasurements)
92+
const ramMarginOfError = getRme(heapSizeMeasurements, averageRam).toFixed(2)
93+
s += ` / RAM: ${bytesToSize(averageRam)} ±${ramMarginOfError}%`
94+
}
95+
if (objectCountMeasurements && objectCountMeasurements.length) {
96+
const averageObjectsCount = getMean(objectCountMeasurements)
97+
const objectsCountMarginOfError = getRme(
98+
objectCountMeasurements,
99+
averageObjectsCount
100+
).toFixed(2)
101+
s += ` / Objects: ${averageObjectsCount.toFixed(
102+
0
103+
)} ±${objectsCountMarginOfError}%`
104+
}
105+
return s
12106
}

lib/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ export interface RunOptions {
2222
* @default 1
2323
*/
2424
cpuThrottle?: number
25+
/**
26+
* If `true` RAM measurement is enabled. In this case, 2 metrics are being recorded between the runs:
27+
* - Heap size (`JSHeapUsedSize`) represents how much RAM was consumed at the end of a test iteration.
28+
* - `Object.prototype` represents how many objects were created in RAM at the end of a test iteration.
29+
* @default false
30+
*/
31+
isRamMeasured?: boolean
2532
}
2633

2734
export default class ReactBenchmark extends EventEmitter {

lib/index.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ module.exports = class ReactBenchmark extends EventEmitter {
1717
this.chrome.on('start', () => {
1818
this.emit('start')
1919
})
20-
this.chrome.on('progress', (benchmark) => {
21-
this.emit('progress', benchmark)
20+
this.chrome.on('progress', (...a) => {
21+
this.emit('progress', ...a)
2222
})
2323
this.chrome.on('console', (log) => {
2424
this.emit('console', log)
@@ -36,7 +36,12 @@ module.exports = class ReactBenchmark extends EventEmitter {
3636

3737
async run(
3838
filepath,
39-
{ debug = false, devtools = false, cpuThrottle = 1 } = {}
39+
{
40+
debug = false,
41+
devtools = false,
42+
cpuThrottle = 1,
43+
isRamMeasured = false,
44+
} = {}
4045
) {
4146
if (this.running) {
4247
throw new Error('Benchmark is already running')
@@ -61,11 +66,11 @@ module.exports = class ReactBenchmark extends EventEmitter {
6166
const port = await this.server.start(outputPath)
6267

6368
return new Promise((resolve, reject) => {
64-
this.chrome.once('complete', async (benchmark) => {
69+
this.chrome.once('complete', async (...a) => {
6570
if (!devtools) {
6671
await this._shutdown()
6772
}
68-
resolve(benchmark)
73+
resolve([...a])
6974
})
7075

7176
this.chrome.once('error', async (err) => {
@@ -77,7 +82,7 @@ module.exports = class ReactBenchmark extends EventEmitter {
7782

7883
this.emit('chrome')
7984

80-
this.chrome.start(port, devtools, { cpuThrottle })
85+
this.chrome.start(port, devtools, { cpuThrottle, isRamMeasured })
8186
})
8287
}
8388
}

test/cli.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test('runs benchmark', async (t) => {
88

99
const result = await execa(binPath, [fixturePath])
1010

11-
t.regex(result.stdout, /[0-9,]+ ops\/sec ±[0-9.]+% \(\d+ runs sampled\)/)
11+
t.regex(result.stdout, /\d+ runs sampled: [0-9,]+ ops\/sec ±[0-9.]+%/)
1212
})
1313

1414
test('throttles CPU', async (t) => {
@@ -29,3 +29,15 @@ test('throttles CPU', async (t) => {
2929
'The difference between throttled and not throttled execution is less then normal'
3030
)
3131
})
32+
33+
test('measures RAM', async (t) => {
34+
const binPath = path.resolve(__dirname, '../lib/cli.js')
35+
const fixturePath = path.resolve(__dirname, 'fixtures/benchmark.js')
36+
37+
const result = await execa(binPath, [fixturePath, '-r'])
38+
39+
t.regex(
40+
result.stdout,
41+
/\d+ runs sampled: [0-9,]+ ops\/sec ±[0-9.]+% \/ RAM: [0-9]+ MB ±[0-9.]+% \/ Objects: [0-9]+ ±[0-9.]+%/
42+
)
43+
})

0 commit comments

Comments
 (0)