mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
* [monorepo] reduce writes from chrome in cypress Chrome puts its shared memory in /dev/shm, and docker's default 64MB is far too small for it - which is why Cypress passes --disable-dev-shm-usage, spilling those segments to files on the container's writable layer instead. Under one spec that is a few hundred MB of write-then-delete churn; under the CI fan-out it was most of the disk bandwidth on the whole box. So every service built from dockerfiles/cypress gets shm_size 1gb and every cypress config drops the flag again: the SaaS e2e service, web's CI and local component-test services plus its standalone cypress one, and server-ce's e2e and native cypress services. Both e2e-open variants inherit the setting through `extends`; the native one is a service of its own, and would be left running Chrome against 64MB once the flag is gone. * [monorepo] adopt cypress-pnp-reporter Materializing the reporter packages copied 267 packages / 3779 files (59MB) into /tmp on every container start, once per spec. Resolving them through PnP inside Cypress's Electron process instead drops that to zero: the entrypoint shell went from 55.6MB of write_bytes to 0. * [web] serve built assets from a prepared buffer between builds Serving a chunk cost ~2ms whatever its size, and an editor page pulls ~144 of them, so webpack-dev-server burned 2-3 cores through an e2e run purely answering requests for output that had not changed since the last build. It is not bytes: cutting the bundle 2.6x with a cheaper devtool moved CPU by 8%, and an empty 304 cost nearly as much as the full body. It is the per-request work - reading the asset out of the in-memory filesystem and re-hashing the whole body for a strong ETag, on every request. Three things a build does that this has to respect, each found by tracing the hooks rather than by reading them: - `done` fires twice per build, for the compilation and for the manifest, both with the same hash. A flag would have been cleared by the second while the first was still running, so the gate is a counter. - An incremental `done` reports only the assets it rewrote - 16, not 630 - so asset names accumulate. Resetting them on `invalid` left the manifest's two behind and nothing was served from here again. - webpack-dev-middleware holds requests during a rebuild. This sits in front of it, so it steps aside for that window and drops the cache when a rebuild starts. Whether a response can be kept is then decided as it ends rather than as it began: an epoch bumped on `invalid` names the build its bytes were read from, and only a response from the current build with no rebuild in flight is stored. A rebuild starting mid-response can no longer write the previous build's bytes in behind the cache being dropped, which eviction by asset name could not catch anyway - an incremental build does not name the assets it left alone. Measured on the editor entrypoint, 16 concurrent clients: 0.26-0.31s -> 0.045-0.050s of webpack CPU per page load, and 0.0455s immediately after a rebuild. Same chunk graph, byte-identical output. GitOrigin-RevId: 08b4ab2d665414069bc01b4763dd64faa9b4e5cb
96 lines
3.9 KiB
JavaScript
96 lines
3.9 KiB
JavaScript
/**
|
|
* Serve each built asset from a prepared response, reused until it is rebuilt.
|
|
*
|
|
* webpack-dev-middleware redoes the same work on every request: it looks the
|
|
* file up in the in-memory filesystem, reads it, and hashes the whole body to
|
|
* produce a strong ETag. Most of that output does not change between builds, so
|
|
* it is recomputed for an answer already known - and under CI ~30 browsers
|
|
* request the same chunks over and over.
|
|
*
|
|
* Only what the compilation emitted is served from here, taken from its own
|
|
* asset list rather than an extension allowlist: a build emits .js and .css but
|
|
* also .bcmap, .dic, .aff, .pfb and .woff2, and anything else reaching this
|
|
* middleware belongs to somebody else.
|
|
*
|
|
* Two things a build does that this has to respect:
|
|
*
|
|
* - It reports `done` more than once - once for the compilation and once for the
|
|
* assets-manifest plugin's own compilation, which has two assets in it - and
|
|
* an incremental `done` reports only the assets it rewrote, 16 rather than 630.
|
|
* So asset names only ever accumulate, and the gate on a running build is a
|
|
* counter rather than a flag, which the manifest's `done` would reopen while
|
|
* the compilation it belongs to is still running.
|
|
* - While a rebuild is running, webpack-dev-middleware holds requests until it
|
|
* finishes. This sits in front of it, so it steps aside for that window and
|
|
* drops the cache when a rebuild starts. Whether a response can be kept is
|
|
* decided when it ends rather than when it began: an epoch, bumped on every
|
|
* `invalid`, names the build its bytes were read from, and only a response
|
|
* from the current build with no rebuild in flight is stored. Nothing a
|
|
* rebuild has invalidated can land behind it, so every entry in the cache
|
|
* belongs to the build that is current and none has to be evicted by name.
|
|
*/
|
|
module.exports = function serveBuiltAssets(devServer) {
|
|
const cache = new Map()
|
|
const known = new Set()
|
|
const compiler = devServer.compiler
|
|
let building = 0
|
|
let epoch = 0
|
|
|
|
compiler.hooks.invalid.tap('serve-built-assets', () => {
|
|
building++
|
|
epoch++
|
|
cache.clear()
|
|
})
|
|
|
|
compiler.hooks.done.tap('serve-built-assets', stats => {
|
|
building = Math.max(0, building - 1)
|
|
for (const asset of stats.toJson({ all: false, assets: true }).assets) {
|
|
if (asset.name) known.add('/' + asset.name)
|
|
}
|
|
})
|
|
|
|
return function serveBuiltAsset(req, res, next) {
|
|
if (building > 0) return next()
|
|
if (req.method !== 'GET' && req.method !== 'HEAD') return next()
|
|
// Ranges are served from the file itself; this only deals in whole bodies.
|
|
if (req.headers.range) return next()
|
|
|
|
const url = req.url.split('?')[0]
|
|
if (!known.has(url)) return next()
|
|
|
|
const hit = cache.get(url)
|
|
if (hit) {
|
|
if (req.headers['if-none-match'] === hit.headers.etag) {
|
|
res.statusCode = 304
|
|
return res.end()
|
|
}
|
|
for (const [name, value] of Object.entries(hit.headers)) {
|
|
res.setHeader(name, value)
|
|
}
|
|
res.statusCode = 200
|
|
return req.method === 'HEAD' ? res.end() : res.end(hit.body)
|
|
}
|
|
|
|
// Miss: let webpack-dev-middleware answer, and keep what it produced.
|
|
const readEpoch = epoch
|
|
const chunks = []
|
|
const write = res.write.bind(res)
|
|
const end = res.end.bind(res)
|
|
res.write = (chunk, ...rest) => {
|
|
if (chunk) chunks.push(Buffer.from(chunk))
|
|
return write(chunk, ...rest)
|
|
}
|
|
res.end = (chunk, ...rest) => {
|
|
if (chunk && typeof chunk !== 'function') chunks.push(Buffer.from(chunk))
|
|
const headers = res.getHeaders()
|
|
// Kept only while the build these bytes came from is the current one: a rebuild starting mid-response leaves them one behind.
|
|
const current = building === 0 && epoch === readEpoch
|
|
if (current && res.statusCode === 200 && headers.etag && chunks.length) {
|
|
cache.set(url, { body: Buffer.concat(chunks), headers })
|
|
}
|
|
return end(chunk, ...rest)
|
|
}
|
|
next()
|
|
}
|
|
}
|