mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
[saas-e2e] reduce disk IO and webpack overhead (#36997)
* [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
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
let reporterOptions = {}
|
||||
if (process.env.CI) {
|
||||
reporterOptions = {
|
||||
reporter: require.resolve('mocha-multi-reporters'),
|
||||
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
|
||||
}
|
||||
}
|
||||
const all = {
|
||||
require: 'test/setup.js',
|
||||
...reporterOptions,
|
||||
}
|
||||
|
||||
module.exports = all
|
||||
@@ -0,0 +1,76 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* cypress-multi-reporters, loadable from inside Cypress.
|
||||
*
|
||||
* Cypress builds mocha reporters in its Electron process. That process resolves
|
||||
* modules through PackherdModuleLoader and does not inherit NODE_OPTIONS, so
|
||||
* Yarn PnP is not installed there and `require('cypress-multi-reporters')`
|
||||
* fails with "Cannot find module".
|
||||
*
|
||||
* Installing PnP for the rest of that process is not an option either: Cypress
|
||||
* asks packherd for its own bundled modules under ids like
|
||||
* `packages/server/lib/browsers/`, which PnP reads as paths inside this
|
||||
* workspace and then rejects ("it isn't declared in your dependencies"),
|
||||
* breaking the browser launch.
|
||||
*
|
||||
* So install PnP, pull the reporters in through it, and hand resolution back to
|
||||
* Cypress. Requires made by the modules that came in through PnP keep going
|
||||
* through PnP, so anything they load later still resolves.
|
||||
*
|
||||
* Point Cypress at this package rather than at cypress-multi-reporters:
|
||||
*
|
||||
* reporter: require.resolve('@overleaf/cypress-pnp-reporter')
|
||||
*/
|
||||
|
||||
const Module = require('node:module')
|
||||
const path = require('node:path')
|
||||
|
||||
function loadReporters() {
|
||||
const MultiReporters = require('cypress-multi-reporters')
|
||||
|
||||
// MultiReporters resolves the reporters named in reporterEnabled when a run
|
||||
// starts, long after PnP is gone. It reads mocha's own registry before
|
||||
// falling back to require(), so register mocha-junit-reporter there while it
|
||||
// can still be found.
|
||||
require('mocha').reporters[
|
||||
'mocha-junit-reporter'
|
||||
] = require('mocha-junit-reporter')
|
||||
|
||||
return MultiReporters
|
||||
}
|
||||
|
||||
if (process.versions.pnp) {
|
||||
module.exports = loadReporters()
|
||||
} else {
|
||||
const stockLoad = Module._load
|
||||
const stockResolveFilename = Module._resolveFilename
|
||||
const stockFindPath = Module._findPath
|
||||
|
||||
require(path.join(__dirname, '..', '..', '.pnp.cjs')).setup()
|
||||
|
||||
const pnpResolveFilename = Module._resolveFilename
|
||||
const cachedBefore = new Set(Object.keys(Module._cache))
|
||||
const loadedViaPnp = new Set()
|
||||
|
||||
try {
|
||||
module.exports = loadReporters()
|
||||
} finally {
|
||||
for (const filename of Object.keys(Module._cache)) {
|
||||
if (!cachedBefore.has(filename)) loadedViaPnp.add(filename)
|
||||
}
|
||||
|
||||
// Leave the patched fs in place: the reporters live in zip archives, so
|
||||
// anything they read lazily still has to go through it.
|
||||
Module._load = stockLoad
|
||||
Module._findPath = stockFindPath
|
||||
Module._resolveFilename = function (request, parent, ...args) {
|
||||
if (!parent || !loadedViaPnp.has(parent.filename)) {
|
||||
return stockResolveFilename.call(this, request, parent, ...args)
|
||||
}
|
||||
const filename = pnpResolveFilename.call(this, request, parent, ...args)
|
||||
loadedViaPnp.add(filename)
|
||||
return filename
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@overleaf/cypress-pnp-reporter",
|
||||
"description": "cypress-multi-reporters, loadable from Cypress's Electron process, which has no Yarn PnP",
|
||||
"main": "index.cjs",
|
||||
"files": [
|
||||
"index.cjs"
|
||||
],
|
||||
"author": "Overleaf (https://www.overleaf.com)",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint --cache --cache-location ../../.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 .",
|
||||
"lint:fix": "eslint --cache --cache-location ../../.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
|
||||
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
|
||||
"test:ci": "yarn run test:unit",
|
||||
"test:unit": "mocha --exit test/**/*.{js,cjs}",
|
||||
"types:check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"cypress-multi-reporters": "^2.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.5.2",
|
||||
"chai": "^4.3.6",
|
||||
"eslint": "^10.4.0",
|
||||
"mocha": "^11.1.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"typescript": "^5.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
reporterEnabled: 'spec, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
mochaFile: `reports/junit-mocha-${process.env.TEST_NAME_PATTERN}.xml`,
|
||||
includePending: true,
|
||||
jenkinsMode: true,
|
||||
output: true,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// no setup yet
|
||||
@@ -0,0 +1,52 @@
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
const Mocha = require('mocha')
|
||||
const JUnitReporter = require('mocha-junit-reporter')
|
||||
|
||||
const MultiReporters = require('../../index.cjs')
|
||||
|
||||
function buildReporters(reporterEnabled) {
|
||||
const runner = new Mocha.Runner(new Mocha.Suite(''))
|
||||
const reporter = new MultiReporters(runner, {
|
||||
reporterOptions: { reporterEnabled },
|
||||
})
|
||||
return reporter._reporters
|
||||
}
|
||||
|
||||
describe('cypress-pnp-reporter', function () {
|
||||
it('exports cypress-multi-reporters', function () {
|
||||
expect(MultiReporters).to.equal(require('cypress-multi-reporters'))
|
||||
})
|
||||
|
||||
it('registers mocha-junit-reporter on mocha', function () {
|
||||
expect(Mocha.reporters['mocha-junit-reporter']).to.equal(JUnitReporter)
|
||||
})
|
||||
|
||||
it('builds the reporters used across the Cypress suites', function () {
|
||||
const reporters = buildReporters('spec, mocha-junit-reporter')
|
||||
|
||||
expect(reporters).to.have.length(2)
|
||||
expect(reporters[0]).to.be.an.instanceOf(Mocha.reporters.spec)
|
||||
expect(reporters[1]).to.be.an.instanceOf(JUnitReporter)
|
||||
})
|
||||
|
||||
it('prefers the reporter registered on mocha over resolving it by name', function () {
|
||||
// Cypress builds the reporters after the shim has handed module resolution
|
||||
// back, so mocha's registry is the only place mocha-junit-reporter can
|
||||
// still be found. Everything above this still passes if
|
||||
// cypress-multi-reporters resolves it by name instead, so pin the lookup
|
||||
// order with a reporter that is only reachable through the registry.
|
||||
class Sentinel {}
|
||||
const registered = Mocha.reporters['mocha-junit-reporter']
|
||||
Mocha.reporters['mocha-junit-reporter'] = Sentinel
|
||||
|
||||
try {
|
||||
expect(buildReporters('mocha-junit-reporter')[0]).to.be.an.instanceOf(
|
||||
Sentinel
|
||||
)
|
||||
} finally {
|
||||
Mocha.reporters['mocha-junit-reporter'] = registered
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.backend.json",
|
||||
"include": ["**/*.js", "**/*.cjs", "**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user