Skip to content

Commit 706cf01

Browse files
authored
perf: limit visibility checks on command log entries in non-performant scenarios (#32937)
* limit number of elements to check visibility on for reporter entries with attached elements * 100 ct limit * changelog * rename stress tests * make e2e test use configured limit, reduce limit due to crash in electron at count=100 * Update CHANGELOG.md * fix boundaries * fix * improve visibility notice copy * merge in visibility check skip from log:false entries * fix command model * remove dead code from dom stress test * rm list * further fix
1 parent 607050d commit 706cf01

File tree

8 files changed

+556
-4
lines changed

8 files changed

+556
-4
lines changed

cli/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
_Released 11/18/2025 (PENDING)_
55

6+
**Performance:**
7+
8+
- Limits the number of matched elements that are tested for visibility when added to a command log entry. Fixes a crash scenario related to rapid successive DOM additions in conjunction with a large number of elements returned from a query. Addressed in [#32937](https://github.com/cypress-io/cypress/pull/32937).
9+
610
**Bugfixes:**
711

812
- Fixed an issue where [`cy.wrap()`](https://docs.cypress.io/api/commands/wrap) would cause infinite recursion and freeze the Cypress App when called with objects containing circular references. Fixes [#24715](https://github.com/cypress-io/cypress/issues/24715). Addressed in [#32917](https://github.com/cypress-io/cypress/pull/32917).

packages/driver/cypress/e2e/cypress/log.cy.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { create, Log, LogUtils } = require('@packages/driver/src/cypress/log')
2+
const { MAX_VISIBILITY_CHECK_ELEMENTS } = require('@packages/types')
23

34
const objectDiff = (newAttrs, oldAttrs) => {
45
return Object.entries(newAttrs).reduce(
@@ -169,6 +170,55 @@ describe('src/cypress/log', function () {
169170
expect(log.setElAttrs).to.have.been.called
170171
})
171172

173+
it('skips visibility check when numElements exceeds MAX_VISIBILITY_CHECK_ELEMENTS', function () {
174+
const log = new Log(this.createSnapshot, this.state, this.config, this.fireChangeEvent)
175+
const $el = Cypress.$('<div />')
176+
177+
// Create a jQuery object with more elements than the limit
178+
const $largeSet = Cypress.$()
179+
180+
for (let i = 0; i < MAX_VISIBILITY_CHECK_ELEMENTS + 1; i++) {
181+
$largeSet.push($el.clone()[0])
182+
}
183+
184+
log.set({ $el: $largeSet })
185+
const attrs = log.get()
186+
187+
expect(attrs.numElements).to.eq(MAX_VISIBILITY_CHECK_ELEMENTS + 1)
188+
expect(attrs.visible).to.be.undefined
189+
})
190+
191+
describe('when log is hidden', function () {
192+
it('does not filter by :visible when setting el attributes', function () {
193+
const log = new Log(this.createSnapshot, this.state, this.config, this.fireChangeEvent)
194+
195+
log.set({ hidden: true })
196+
const el = cy.$$('<div />')
197+
198+
cy.spy(el, 'filter')
199+
log.set({ $el: el })
200+
expect(el.filter).not.to.have.been.called
201+
})
202+
})
203+
204+
it('performs visibility check when numElements is within limit', function () {
205+
const log = new Log(this.createSnapshot, this.state, this.config, this.fireChangeEvent)
206+
const $el = Cypress.$('<div />')
207+
208+
// Create a jQuery object with elements within the limit
209+
const $smallSet = Cypress.$()
210+
211+
for (let i = 0; i < MAX_VISIBILITY_CHECK_ELEMENTS; i++) {
212+
$smallSet.push($el.clone()[0])
213+
}
214+
215+
log.set({ $el: $smallSet })
216+
const attrs = log.get()
217+
218+
expect(attrs.numElements).to.eq(MAX_VISIBILITY_CHECK_ELEMENTS)
219+
expect(attrs.visible).to.be.a('boolean')
220+
})
221+
172222
it('only triggers change event if the log has already triggered the add event', function () {
173223
const log = new Log(this.createSnapshot, this.state, this.config, this.fireChangeEvent)
174224

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* DOM Stress Tests
3+
*
4+
* This test suite is designed to test browser stability and Cypress performance
5+
* when dealing with extremely large non-virtualized DOM trees (up to 100,000 DOM nodes).
6+
*
7+
* These tests verify that Cypress can handle pages with massive DOM trees without
8+
* crashing, which is important for testing applications that may have performance
9+
* issues or anti-patterns like rendering all list items at once instead of using
10+
* virtual scrolling.
11+
*
12+
* Test scenarios include:
13+
* - Loading lists with 100 to 100,000 DOM elements rendered simultaneously
14+
* - Scrolling performance with large DOM trees
15+
* - Browser stability under memory pressure from massive DOM trees
16+
* - Rapid scrolling patterns that stress the browser
17+
*/
18+
19+
describe(`DOM Stress Tests`, () => {
20+
const counts = [
21+
100,
22+
1000,
23+
3992,
24+
10000,
25+
]
26+
27+
for (const count of counts) {
28+
describe(`basic list (${count} count)`, () => {
29+
beforeEach(() => {
30+
cy.log(`loading basic list with ${count}items`)
31+
cy.visit('/fixtures/dom-stress-test.html')
32+
cy.get('input[data-cy="item-count"]').clear().type(count)
33+
cy.get('input[data-cy="list-id"]').clear().type('basic-list')
34+
cy.get('button[data-cy="add-list"]').click()
35+
})
36+
37+
it(`should load the large DOM tree (${count} count) without crashing`, () => {
38+
cy.get('#basic-list .item')
39+
.should('have.length.greaterThan', 0, { log: false })
40+
})
41+
42+
it('handles normal scrolling without crashing', () => {
43+
cy.get('#basic-list').scrollTo(0, 1000)
44+
cy.get('#basic-list').scrollTo(0, 2000)
45+
cy.get('#basic-list').scrollTo(0, 5000)
46+
})
47+
48+
it('handles rapid scrolling without crashing', () => {
49+
cy.get('#basic-list').scrollTo(0, 100)
50+
cy.get('#basic-list').scrollTo(0, 300)
51+
cy.get('#basic-list').scrollTo(0, 600)
52+
cy.get('#basic-list').scrollTo(0, 1000)
53+
cy.get('#basic-list').scrollTo(0, 1500)
54+
cy.get('#basic-list').scrollTo(0, 2000)
55+
})
56+
57+
it('handles stress scrolling without crashing', () => {
58+
cy.get('button[data-cy="stress-scroll"]').click()
59+
cy.wait(2000)
60+
cy.get('#basic-list').scrollTo(0, 10000)
61+
})
62+
})
63+
}
64+
})

0 commit comments

Comments
 (0)