Skip to content

perf: updates for potential reporter rendering performance #32002

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

Draft
wants to merge 13 commits into
base: release/15.0.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions packages/app/src/runner/aut-iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export class AutIframe {
debouncedToggleSelectorPlayground: DebouncedFunc<(isEnabled: any) => void>
$iframe?: JQuery<HTMLIFrameElement>
_highlightedEl?: Element
// Cache the blank page content to avoid repeated generation
private _cachedTestIsolationContent?: string
private _cachedInitialContent?: string
Comment on lines +23 to +25
Copy link
Member Author

@jennifer-shehane jennifer-shehane Jul 9, 2025

Choose a reason for hiding this comment

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

Our initial blank page and the test isolation page that shows in between each test is just a string, HTML string. This can be cached upfront so we don't generate this string over and over. Likely not a huge win, but this empty screen can be shown a lot with a lot of tests.


constructor (
private projectName: string,
Expand Down Expand Up @@ -50,11 +53,21 @@ export class AutIframe {
}

_showInitialBlankPage () {
this._showContents(blankContents.initial())
// Cache the content to avoid repeated HTML generation
if (!this._cachedInitialContent) {
this._cachedInitialContent = blankContents.initial()
}

this._showContents(this._cachedInitialContent)
}

_showTestIsolationBlankPage () {
this._showContents(blankContents.testIsolationBlankPage())
// Cache the content to avoid repeated HTML generation
if (!this._cachedTestIsolationContent) {
this._cachedTestIsolationContent = blankContents.testIsolationBlankPage()
}

this._showContents(this._cachedTestIsolationContent)
}

showVisitFailure = (props) => {
Expand Down
6 changes: 1 addition & 5 deletions packages/app/src/runner/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@ export function getReporterElement () {
}

export function empty (el: HTMLElement) {
while (el.lastChild) {
if (el && el.firstChild) {
el.removeChild(el.firstChild)
}
}
el.innerHTML = ''
Copy link
Member Author

@jennifer-shehane jennifer-shehane Jul 9, 2025

Choose a reason for hiding this comment

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

This function is called to empty the #unified-runner element on start of e2e or ct tests. This is a parent div of the AUT iframe. At a cursory look, setting el.innerHTML at face value is described as less performant, but calling removeChild over and over causes recalculateStyles, which impacts performance.

}

export const togglePlayground = (autIframe: AutIframe) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/settings/SettingsContainer.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div
class="space-y-[32px] h-[calc(100vh-[64px])] p-[32px] overflow-auto"
class="space-y-[32px] h-[calc(100vh-64px)] p-[32px] overflow-auto"
Copy link
Member Author

Choose a reason for hiding this comment

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

This tailwind class is invalid, just noticed it while doing other things.

data-cy="settings"
>
<div class="space-y-[24px]">
Expand Down
16 changes: 16 additions & 0 deletions packages/driver/cypress/e2e/commands/storage.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ describe('src/cy/commands/storage', () => {
})

context('#clearLocalStorage', () => {
beforeEach(() => {
cy.visit('/fixtures/set-storage-on-multiple-origins.html')
})

it('passes keys onto Cypress.LocalStorage.clear', () => {
const clear = cy.spy(Cypress.LocalStorage, 'clear')

Expand Down Expand Up @@ -406,6 +410,18 @@ describe('src/cy/commands/storage', () => {
expect(lastLog.get('snapshots')[0]).to.be.an('object')
})
})

it('consoleProps includes the empty storage yielded', () => {
const ls = cy.state('window').localStorage

cy.clearLocalStorage().then(() => {
const consoleProps = logs[1].get('consoleProps')()

expect(consoleProps.name).to.equal('clearLocalStorage')
expect(consoleProps.type).to.equal('command')
expect(consoleProps.props.Yielded).to.deep.equal(ls)
})
Comment on lines +414 to +423
Copy link
Member Author

@jennifer-shehane jennifer-shehane Jul 9, 2025

Choose a reason for hiding this comment

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

This is me trying to verify my assumption around what clearLocalStorage yields in the consoleProps outside of origin (noted below more clearly as to why). There wasn't a test around this. Read comments below first.

})
})

describe('without log', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ context('cy.origin storage', { browser: '!webkit' }, () => {

expect(consoleProps.name).to.equal('clearLocalStorage')
expect(consoleProps.type).to.equal('command')
expect(consoleProps.props.Yielded).to.be.null
expect(consoleProps.props.Yielded).to.deep.equal({})
Copy link
Member Author

@jennifer-shehane jennifer-shehane Jul 9, 2025

Choose a reason for hiding this comment

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

Alright....I see absolutely no reason why this Yielded should ever have been null...but this previous test is now failing in my PR - see here. My only guess is that some improved performance has resulted in this change of behavior - where it wasn't set in time before, but it is now??

  1. clearLocalStorage returns a Storage object (state('window').localStorage)
  2. The default consoleProps() function automatically sets Yielded to the command's return value
  3. When the Storage object gets serialized for cross-origin logging, it becomes {}
  4. The test should expect {}, not null

})
})
})
Expand Down
109 changes: 28 additions & 81 deletions packages/driver/src/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,23 @@ class $Cypress {
this.emit('cypress:in:cypress:runner:event', ...args)
}

private _handleMochaEvent (eventType: string, args: unknown[], isTextTerminal: boolean) {
this.maybeEmitCypressInCypress('mocha', eventType, ...args)

if (isTextTerminal) {
return this.emit('mocha', eventType, ...args)
}
}
Comment on lines +412 to +418
Copy link
Member Author

Choose a reason for hiding this comment

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

refactor pulling this out


action (eventName, ...args) {
// normalizes all the various ways
// other objects communicate intent
// and 'action' to Cypress
debug(eventName)

const isTextTerminal = this.config('isTextTerminal')
const isInteractive = this.config('isInteractive')
Comment on lines +426 to +427
Copy link
Member Author

@jennifer-shehane jennifer-shehane Jul 9, 2025

Choose a reason for hiding this comment

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

Pulling these out to just call once.


switch (eventName) {
case 'recorder:frame':
return this.emit('recorder:frame', args[0])
Expand All @@ -434,13 +446,7 @@ class $Cypress {
return
}

this.maybeEmitCypressInCypress('mocha', 'start', args[0])

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'start', args[0])
}

break
return this._handleMochaEvent('start', args, isTextTerminal)

case 'runner:end':
$sourceMapUtils.destroySourceMapConsumers()
Expand All @@ -451,110 +457,51 @@ class $Cypress {
// TODO: it would be nice to await this emit before preceding.
this.emit('run:end')

this.maybeEmitCypressInCypress('mocha', 'end', args[0])

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'end', args[0])
}

break
return this._handleMochaEvent('end', args, isTextTerminal)

case 'runner:suite:start':
// mocha runner started processing a suite
this.maybeEmitCypressInCypress('mocha', 'suite', ...args)

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'suite', ...args)
}
return this._handleMochaEvent('suite', args, isTextTerminal)

break
case 'runner:suite:end':
// mocha runner finished processing a suite
this.maybeEmitCypressInCypress('mocha', 'suite end', ...args)

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'suite end', ...args)
}
return this._handleMochaEvent('suite end', args, isTextTerminal)

break
case 'runner:hook:start':
// mocha runner started processing a hook

this.maybeEmitCypressInCypress('mocha', 'hook', ...args)

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'hook', ...args)
}

break
return this._handleMochaEvent('hook', args, isTextTerminal)

case 'runner:hook:end':
// mocha runner finished processing a hook
this.maybeEmitCypressInCypress('mocha', 'hook end', ...args)

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'hook end', ...args)
}

break
return this._handleMochaEvent('hook end', args, isTextTerminal)

case 'runner:test:start':
// mocha runner started processing a hook
this.maybeEmitCypressInCypress('mocha', 'test', ...args)

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'test', ...args)
}

break
return this._handleMochaEvent('test', args, isTextTerminal)

case 'runner:test:end':
this.maybeEmitCypressInCypress('mocha', 'test end', ...args)

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'test end', ...args)
}

break
return this._handleMochaEvent('test end', args, isTextTerminal)

case 'runner:pass':
// mocha runner calculated a pass
// this is delayed from when mocha would normally fire it
// since we fire it after all afterEach hooks have ran
this.maybeEmitCypressInCypress('mocha', 'pass', ...args)

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'pass', ...args)
}

break
return this._handleMochaEvent('pass', args, isTextTerminal)

case 'runner:pending':
// mocha runner calculated a pending test
this.maybeEmitCypressInCypress('mocha', 'pending', ...args)

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'pending', ...args)
}

break
return this._handleMochaEvent('pending', args, isTextTerminal)

case 'runner:fail': {
this.maybeEmitCypressInCypress('mocha', 'fail', ...args)

if (this.config('isTextTerminal')) {
return this.emit('mocha', 'fail', ...args)
}

break
return this._handleMochaEvent('fail', args, isTextTerminal)
}
// retry event only fired in mocha version 6+
// https://github.com/mochajs/mocha/commit/2a76dd7589e4a1ed14dd2a33ab89f182e4c4a050
case 'runner:retry': {
// mocha runner calculated a pass
this.maybeEmitCypressInCypress('mocha', 'retry', ...args)

if (this.config('isTextTerminal')) {
if (isTextTerminal) {
this.emit('mocha', 'retry', ...args)
}

Expand All @@ -567,7 +514,7 @@ class $Cypress {
case 'runner:test:before:run':
this.maybeEmitCypressInCypress('mocha', 'test:before:run', args[0])

if (this.config('isTextTerminal')) {
if (isTextTerminal) {
// needed for handling test retries
this.emit('mocha', 'test:before:run', args[0])
}
Expand Down Expand Up @@ -602,7 +549,7 @@ class $Cypress {
this.emit('test:after:run', ...args)
this.maybeEmitCypressInCypress('mocha', 'test:after:run', args[0])

if (this.config('isTextTerminal')) {
if (isTextTerminal) {
// needed for calculating wallClockDuration
// and the timings of after + afterEach hooks
return this.emit('mocha', 'test:after:run', args[0])
Expand All @@ -628,7 +575,7 @@ class $Cypress {
return // do not emit hidden logs to public apis
}

this.runner?.addLog(args[0], this.config('isInteractive'))
this.runner?.addLog(args[0], isInteractive)

return this.emit('log:added', ...args)

Expand All @@ -641,7 +588,7 @@ class $Cypress {

// Cypress logs will only trigger an update every 4 ms so there is a
// chance the runner has been torn down when the update is triggered.
this.runner?.addLog(args[0], this.config('isInteractive'))
this.runner?.addLog(args[0], isInteractive)

return this.emit('log:changed', ...args)

Expand Down
Loading
Loading