Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement JIT component experiment #30049

Merged
merged 9 commits into from
Aug 22, 2024
11 changes: 5 additions & 6 deletions .circleci/workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ mainBuildFilters: &mainBuildFilters
- 'update-v8-snapshot-cache-on-develop'
- 'jquery-patch-remove-unload'
- 'publish-binary'
- 'chore/use_build_docker_file_for_centos7'
- 'feat/experimentalJustInTimeCompile'

# usually we don't build Mac app - it takes a long time
# but sometimes we want to really confirm we are doing the right thing
Expand All @@ -43,7 +43,7 @@ macWorkflowFilters: &darwin-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'fix/webpack-batteries-included-for-yarn-pnp', << pipeline.git.branch >> ]
- equal: [ 'feat/experimentalJustInTimeCompile', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand All @@ -54,8 +54,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'chore/use_build_docker_file_for_centos7', << pipeline.git.branch >> ]
- equal: [ 'jquery-patch-remove-unload', << pipeline.git.branch >> ]
- equal: [ 'feat/experimentalJustInTimeCompile', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand All @@ -78,7 +77,7 @@ windowsWorkflowFilters: &windows-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'fix/webpack-batteries-included-for-yarn-pnp', << pipeline.git.branch >> ]
- equal: [ 'feat/experimentalJustInTimeCompile', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand Down Expand Up @@ -154,7 +153,7 @@ commands:
name: Set environment variable to determine whether or not to persist artifacts
command: |
echo "Setting SHOULD_PERSIST_ARTIFACTS variable"
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "chore/use_build_docker_file_for_centos7" ]]; then
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "feat/experimentalJustInTimeCompile" ]]; then
export SHOULD_PERSIST_ARTIFACTS=true
fi' >> "$BASH_ENV"
# You must run `setup_should_persist_artifacts` command and be using bash before running this command
Expand Down
10 changes: 8 additions & 2 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 13.13.4
## 13.14.0

_Released 8/27/2024 (PENDING)_

**Performance:**

- Fixed a potential memory leak in the Cypress server when re-connecting to an unintentionally disconnected CDP connection. Fixes [#29744](https://github.com/cypress-io/cypress/issues/29744). Addressed in [#29988](https://github.com/cypress-io/cypress/pull/29988)
- Fixed a potential memory leak in the Cypress server when re-connecting to an unintentionally disconnected CDP connection. Fixes [#29744](https://github.com/cypress-io/cypress/issues/29744). Addressed in [#29988](https://github.com/cypress-io/cypress/pull/29988).

**Features:**

- Added new
[`experimentalJustInTimeCompile`](/guides/references/experiments#Configuration)
configuration option for component testing. This option will only compile resources directly related to your spec, compiling them 'just-in-time' before spec execution. This should result in improved memory management and performance for component tests in `cypress open` and `cypress run` modes, in particular for large component testing suites. [`experimentalJustInTimeCompile`](/guides/references/experiments#Configuration) is currently supported for [`webpack`](https://www.npmjs.com/package/webpack) and [`vite`](https://www.npmjs.com/package/vite). Addresses [#29244](https://github.com/cypress-io/cypress/issues/29244).

**Dependency Updates:**

Expand Down
8 changes: 8 additions & 0 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3124,6 +3124,14 @@ declare namespace Cypress {
* @default null
*/
experimentalSkipDomainInjection: string[] | null
/**
* Allows for just-in-time compiling of a component test, which will only compile assets related to the component.
* This results in a smaller bundle under test, reducing resource constraints on a given machine. This option is recommended
* for users with large component testing projects and those who are running into webpack 'chunk load error' issues.
* Supported for vite and webpack. For component testing only.
* @see https://on.cypress.io/experiments#Configuration
*/
experimentalJustInTimeCompile: boolean
/**
* Enables AST-based JS/HTML rewriting. This may fix issues caused by the existing regex-based JS/HTML replacement algorithm.
* @default false
Expand Down
7 changes: 6 additions & 1 deletion npm/vite-dev-server/src/devServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ export async function devServer (config: ViteDevServerConfig): Promise<Cypress.R
port,
// Close is for unit testing only. We kill this child process which will handle the closing of the server
close (cb) {
return server.close().then(() => cb?.()).catch(cb)
debug('closing dev server')

return server.close().then(() => {
debug('closed dev server')
cb?.()
}).catch(cb)
},
}
}
Expand Down
1 change: 1 addition & 0 deletions npm/vite-dev-server/src/plugins/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const Cypress = (
let loader = fs.readFileSync(INIT_FILEPATH, 'utf8')

devServerEvents.on('dev-server:specs:changed', (specs: Spec[]) => {
debug(`dev-server:secs:changed: ${specs.map((spec) => spec.relative)}`)
specsPathsSet = getSpecsPathsSet(specs)
})

Expand Down
4 changes: 3 additions & 1 deletion npm/vite-dev-server/src/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const createViteDevServerConfig = async (config: ViteDevServerConfig, vit
function makeCypressViteConfig (config: ViteDevServerConfig, vite: Vite): InlineConfig | InlineConfig {
const {
cypressConfig: {
experimentalJustInTimeCompile,
port,
projectRoot,
devServerPublicPathRoute,
Expand Down Expand Up @@ -128,7 +129,8 @@ function makeCypressViteConfig (config: ViteDevServerConfig, vite: Vite): Inline
port: vitePort,
host: '127.0.0.1',
// Disable file watching and HMR when executing tests in `run` mode
...(isTextTerminal
// if experimentalJustInTimeCompile is configured, we need to watch for file changes as the spec entries are going to be updated per test in run mode
...(isTextTerminal && !experimentalJustInTimeCompile
? { watch: { ignored: '**/*' }, hmr: false }
: {}),
},
Expand Down
35 changes: 35 additions & 0 deletions npm/vite-dev-server/test/resolveConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,40 @@ describe('resolveConfig', function () {
expect(viteConfig.server?.hmr).to.be.undefined
})
})

describe('experimentalJustInTimeCompile', () => {
let viteDevServerConfig: ViteDevServerConfig

beforeEach(async () => {
const projectRoot = await scaffoldSystemTestProject(`vite${version}-inspect`)

viteDevServerConfig = getViteDevServerConfig(projectRoot)
viteDevServerConfig.cypressConfig.experimentalJustInTimeCompile = true
})

describe('open mode', () => {
beforeEach(() => {
viteDevServerConfig.cypressConfig.isTextTerminal = false
})

it('enables hmr and watching', async () => {
const viteConfig = await createViteDevServerConfig(viteDevServerConfig, discoveredVite)

expect(viteConfig.server.watch).to.be.undefined
})
})

describe('run mode', () => {
beforeEach(() => {
viteDevServerConfig.cypressConfig.isTextTerminal = true
})

it('enables hmr and watching', async () => {
const viteConfig = await createViteDevServerConfig(viteDevServerConfig, discoveredVite)

expect(viteConfig.server.watch).to.be.undefined
})
})
})
})
})
2 changes: 1 addition & 1 deletion npm/webpack-dev-server/src/devServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function devServer (devServerConfig: WebpackDevServerConfig): Promise<Cyp
return reject(new Error(`Expected port ${result.server.options.port} to be a number`))
}

debug('Component testing webpack server 4 started on port %s', result.server.options.port)
debug(`Component testing webpack server ${result.version} started on port %s`, result.server.options.port)

resolve({
port: result.server.options.port as number,
Expand Down
4 changes: 3 additions & 1 deletion npm/webpack-dev-server/src/makeDefaultWebpackConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function makeCypressWebpackConfig (
const {
devServerConfig: {
cypressConfig: {
experimentalJustInTimeCompile,
port,
projectRoot,
devServerPublicPathRoute,
Expand Down Expand Up @@ -98,7 +99,8 @@ export function makeCypressWebpackConfig (
devtool: 'inline-source-map',
} as any

if (isRunMode) {
// if experimentalJustInTimeCompile is configured, we need to watch for file changes as the spec entries are going to be updated per test
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to watch everything? Or is there a smaller subset of files that we can watch in the JIT case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually doing the later. The watching only applies to files that are in the entry, which is the support file, index file and related files, plus the spec entry. I contemplated narrowing this down to just the specpattern, but we have this weird hack where we save the index file with no changes to prompt the dev server to trigger a refresh. I wonder if we can just do the index html file and we get the same effect? Ill try it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanthemanuel So the watch options for webpack are a bit complicated, but I figure a quick win here is to at least ignore the node_modules directory. Vite ignores this by default. updated watch options in 77b5dee

if (isRunMode && !experimentalJustInTimeCompile) {
// Disable file watching when executing tests in `run` mode
finalConfig.watchOptions = {
ignored: '**/*',
Expand Down
64 changes: 64 additions & 0 deletions npm/webpack-dev-server/test/makeWebpackConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,4 +437,68 @@ describe('makeWebpackConfig', () => {
})
})
})

describe('experimentalJustInTimeCompile', () => {
let devServerConfig: WebpackDevServerConfig

const WEBPACK_MATRIX: {
webpack: 4 | 5
wds: 3 | 4 | 5
}[] = [
{
webpack: 4,
wds: 3,
},
{
webpack: 4,
wds: 4,
},
{
webpack: 5,
wds: 4,
},
{
webpack: 5,
wds: 5,
},
]

beforeEach(() => {
devServerConfig = {
specs: [],
cypressConfig: {
projectRoot: '.',
devServerPublicPathRoute: '/test-public-path',
experimentalJustInTimeCompile: true,
baseUrl: null,
} as Cypress.PluginConfigOptions,
webpackConfig: {
entry: { main: 'src/index.js' },
},
devServerEvents: new EventEmitter(),
}
})

WEBPACK_MATRIX.forEach(({ webpack, wds }) => {
describe(`webpack: v${webpack} with webpack-dev-server v${wds}`, () => {
describe('run mode', () => {
beforeEach(() => {
devServerConfig.cypressConfig.isTextTerminal = true
})

it('enables watching', async () => {
const actual = await makeWebpackConfig({
devServerConfig,
sourceWebpackModulesResult: createModuleMatrixResult({
webpack,
webpackDevServer: wds,
}),
})

expect(actual.watchOptions?.ignored).to.be.undefined
})
})
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { fixtureDirs } from '@tooling/system-tests'

type ProjectDirs = typeof fixtureDirs

const EXPERIMENTAL_JIT_DIR: ProjectDirs[number] = 'experimental-JIT'

const PROJECTS: {bundler: 'vite' | 'webpack'}[] = [
{ bundler: 'vite' },
{ bundler: 'webpack' },
]

for (const { bundler } of PROJECTS) {
const PROJECT_NAME = `${EXPERIMENTAL_JIT_DIR}/${bundler}`

describe(`CT experimentalJustInTimeCompile: ${bundler}`, { viewportWidth: 1500, defaultCommandTimeout: 30000 }, () => {
const visitComponentSpecAndVerifyPass = (specNumber: number) => {
cy.contains(`Component-${specNumber}.cy.jsx`).click()
cy.waitForSpecToFinish(undefined)
cy.get('[aria-label="Stats"] .passed > .num').should('contain', '1')
cy.get('[aria-label="Stats"] .failed > .num').should('contain', '--')
}

beforeEach(() => {
// @ts-expect-error
cy.scaffoldProject(PROJECT_NAME)
cy.findBrowsers()
}),

it(`can run multiple CT tests without interruption`, () => {
// @ts-expect-error
cy.openProject(PROJECT_NAME, ['--component'])
cy.startAppServer('component')
cy.visitApp()
cy.specsPageIsVisible()
visitComponentSpecAndVerifyPass(2)
cy.get('[data-cy="sidebar-link-specs-page"]').click()
visitComponentSpecAndVerifyPass(1)
cy.get('[data-cy="sidebar-link-specs-page"]').click()
visitComponentSpecAndVerifyPass(3)
})
})
}
4 changes: 2 additions & 2 deletions packages/app/src/runner/event-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ interface AddGlobalListenerOptions {
}

const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ')
const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame'.split(' ')
const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame dev-server:on-spec-update'.split(' ')
const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed visit:blank cypress:in:cypress:runner:event'.split(' ')
const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ')
const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:cookies'.split(' ')
const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:cookies dev-server:on-spec-updated'.split(' ')
const localToReporterEvents = 'reporter:log:add reporter:log:state:changed reporter:log:remove'.split(' ')

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/app/src/runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,18 @@ async function initialize () {
window.UnifiedRunner.MobX.runInAction(() => setupRunner())
}

async function updateDevServerWithSpec (spec: SpecFile) {
return new Promise<void>((resolve, _reject) => {
// currently, we don't have criteria to reject the promise
// as the dev-server can take a long time to compile, which is variable per user.
Cypress.once('dev-server:on-spec-updated', () => {
resolve()
})

Cypress.emit('dev-server:on-spec-update', spec)
})
}

/**
* This wraps all of the required interactions to run a spec.
* Here are the things that happen:
Expand Down Expand Up @@ -437,6 +449,13 @@ async function executeSpec (spec: SpecFile, isRerun: boolean = false) {
}

if (window.__CYPRESS_TESTING_TYPE__ === 'component') {
if (config.experimentalJustInTimeCompile && !config.isTextTerminal) {
// If running with experimentalJustInTimeCompile enabled and in open mode,
// send a signal to the dev server to load the spec before running
// since the spec and related resources are not yet compiled.
await updateDevServerWithSpec(spec)
}

return runSpecCT(config, spec)
}

Expand Down
3 changes: 3 additions & 0 deletions packages/config/__snapshots__/index.spec.ts.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1
'experimentalMemoryManagement': false,
'experimentalModifyObstructiveThirdPartyCode': false,
'experimentalSkipDomainInjection': null,
'experimentalJustInTimeCompile': false,
'experimentalOriginDependencies': false,
'experimentalSourceRewriting': false,
'experimentalSingleTabRunMode': false,
Expand Down Expand Up @@ -130,6 +131,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys f
'experimentalMemoryManagement': false,
'experimentalModifyObstructiveThirdPartyCode': false,
'experimentalSkipDomainInjection': null,
'experimentalJustInTimeCompile': false,
'experimentalOriginDependencies': false,
'experimentalSourceRewriting': false,
'experimentalSingleTabRunMode': false,
Expand Down Expand Up @@ -216,6 +218,7 @@ exports['config/src/index .getPublicConfigKeys returns list of public config key
'experimentalMemoryManagement',
'experimentalModifyObstructiveThirdPartyCode',
'experimentalSkipDomainInjection',
'experimentalJustInTimeCompile',
'experimentalOriginDependencies',
'experimentalSourceRewriting',
'experimentalSingleTabRunMode',
Expand Down
Loading
Loading