rsnext/bench/nested-deps-app-router/bench.mjs
Tim Neutkens 00106b7ef4
Optimize webpack memory cache garbage collection (#54397)
## What

Adds a reworked version of webpack's [built-in memory cache garbage
collection
plugin](853bfda35a/lib/cache/MemoryWithGcCachePlugin.js (L15))
that is more aggressive in cleaning up unused modules than the default.

The default marks 1/5th of the modules as "up for potentially being
garbage collected". The new plugin always checks all modules. In my
testing this does not cause much overhead compared to the current
approach which leverages writing to two separate maps. The change also
makes the memory cache eviction more predictable: when an item has not
been accessed for 5 compilations it is evicted from the memory cache, it
could still be in the disk cache.


In order to test this change I had to spin up the benchmarks but these
were a bit outdated so I've cleaned up the benchmark applications.

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-08-23 15:46:32 +02:00

253 lines
6.5 KiB
JavaScript

import { execSync, spawn } from 'child_process'
import { join } from 'path'
import { fileURLToPath } from 'url'
import fetch from 'node-fetch'
import {
existsSync,
readFileSync,
writeFileSync,
unlinkSync,
promises as fs,
} from 'fs'
import prettyMs from 'pretty-ms'
import treeKill from 'tree-kill'
const ROOT_DIR = join(fileURLToPath(import.meta.url), '..', '..', '..')
const CWD = join(ROOT_DIR, 'bench', 'nested-deps')
const NEXT_BIN = join(ROOT_DIR, 'packages', 'next', 'dist', 'bin', 'next')
const [, , command = 'all'] = process.argv
async function killApp(instance) {
await new Promise((resolve, reject) => {
treeKill(instance.pid, (err) => {
if (err) {
if (
process.platform === 'win32' &&
typeof err.message === 'string' &&
(err.message.includes(`no running instance of the task`) ||
err.message.includes(`not found`))
) {
// Windows throws an error if the process is already stopped
//
// Command failed: taskkill /pid 6924 /T /F
// ERROR: The process with PID 6924 (child process of PID 6736) could not be terminated.
// Reason: There is no running instance of the task.
return resolve()
}
return reject(err)
}
resolve()
})
})
}
class File {
constructor(path) {
this.path = path
this.originalContent = existsSync(this.path)
? readFileSync(this.path, 'utf8')
: null
}
write(content) {
if (!this.originalContent) {
this.originalContent = content
}
writeFileSync(this.path, content, 'utf8')
}
replace(pattern, newValue) {
const currentContent = readFileSync(this.path, 'utf8')
if (pattern instanceof RegExp) {
if (!pattern.test(currentContent)) {
throw new Error(
`Failed to replace content.\n\nPattern: ${pattern.toString()}\n\nContent: ${currentContent}`
)
}
} else if (typeof pattern === 'string') {
if (!currentContent.includes(pattern)) {
throw new Error(
`Failed to replace content.\n\nPattern: ${pattern}\n\nContent: ${currentContent}`
)
}
} else {
throw new Error(`Unknown replacement attempt type: ${pattern}`)
}
const newContent = currentContent.replace(pattern, newValue)
this.write(newContent)
}
prepend(line) {
const currentContent = readFileSync(this.path, 'utf8')
this.write(line + '\n' + currentContent)
}
delete() {
unlinkSync(this.path)
}
restore() {
this.write(this.originalContent)
}
}
function runNextCommandDev(argv, opts = {}) {
const env = {
...process.env,
NODE_ENV: undefined,
__NEXT_TEST_MODE: 'true',
FORCE_COLOR: 3,
...opts.env,
}
const nodeArgs = opts.nodeArgs || []
return new Promise((resolve, reject) => {
const instance = spawn(NEXT_BIN, [...nodeArgs, ...argv], {
cwd: CWD,
env,
})
let didResolve = false
function handleStdout(data) {
const message = data.toString()
const bootupMarkers = {
dev: /compiled .*successfully/i,
start: /started server/i,
}
if (
(opts.bootupMarker && opts.bootupMarker.test(message)) ||
bootupMarkers[opts.nextStart ? 'start' : 'dev'].test(message)
) {
if (!didResolve) {
didResolve = true
resolve(instance)
instance.removeListener('data', handleStdout)
}
}
if (typeof opts.onStdout === 'function') {
opts.onStdout(message)
}
if (opts.stdout !== false) {
process.stdout.write(message)
}
}
function handleStderr(data) {
const message = data.toString()
if (typeof opts.onStderr === 'function') {
opts.onStderr(message)
}
if (opts.stderr !== false) {
process.stderr.write(message)
}
}
instance.stdout.on('data', handleStdout)
instance.stderr.on('data', handleStderr)
instance.on('close', () => {
instance.stdout.removeListener('data', handleStdout)
instance.stderr.removeListener('data', handleStderr)
if (!didResolve) {
didResolve = true
resolve()
}
})
instance.on('error', (err) => {
reject(err)
})
})
}
function waitFor(millis) {
return new Promise((resolve) => setTimeout(resolve, millis))
}
await fs.rm('.next', { recursive: true }).catch(() => {})
const file = new File(join(CWD, 'pages/index.jsx'))
const results = []
try {
if (command === 'dev' || command === 'all') {
const instance = await runNextCommandDev(['dev', '--port', '3000'])
function waitForCompiled() {
return new Promise((resolve) => {
function waitForOnData(data) {
const message = data.toString()
const compiledRegex =
/compiled client and server successfully in (\d*[.]?\d+)\s*(m?s) \((\d+) modules\)/gm
const matched = compiledRegex.exec(message)
if (matched) {
resolve({
'time (ms)': (matched[2] === 's' ? 1000 : 1) * Number(matched[1]),
modules: Number(matched[3]),
})
instance.stdout.removeListener('data', waitForOnData)
}
}
instance.stdout.on('data', waitForOnData)
})
}
const [res, initial] = await Promise.all([
fetch('http://localhost:3000/'),
waitForCompiled(),
])
if (res.status !== 200) {
throw new Error('Fetching / failed')
}
results.push(initial)
file.prepend('// First edit')
results.push(await waitForCompiled())
await waitFor(1000)
file.prepend('// Second edit')
results.push(await waitForCompiled())
await waitFor(1000)
file.prepend('// Third edit')
results.push(await waitForCompiled())
console.table(results)
await killApp(instance)
}
if (command === 'build' || command === 'all') {
// ignore error
await fs.rm('.next', { recursive: true, force: true }).catch(() => {})
execSync(`node ${NEXT_BIN} build ./bench/nested-deps`, {
cwd: ROOT_DIR,
stdio: 'inherit',
env: {
...process.env,
TRACE_TARGET: 'jaeger',
},
})
const traceString = await fs.readFile(join(CWD, '.next', 'trace'), 'utf8')
const traces = traceString
.split('\n')
.filter((line) => line)
.map((line) => JSON.parse(line))
const { duration } = traces.pop().find(({ name }) => name === 'next-build')
console.info('next build duration: ', prettyMs(duration / 1000))
}
} finally {
file.restore()
}