00106b7ef4
## 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>
253 lines
6.5 KiB
JavaScript
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()
|
|
}
|