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

perf: cri client memory leak #29988

Merged
merged 34 commits into from
Aug 16, 2024
Merged

Conversation

cacieprins
Copy link
Contributor

Additional details

Event listeners added directly to the CDP Client in CriClient were never removed, and referenced the CDP client via closure. Because new CDP clients were created every reconnection attempt, this could lead to zombie references, preventing old CDP Clients from being garbage collected.

To fix, the number of event listeners that are being added directly to the CDP.Client is no longer variable, is limited to 2, and they are removed when a CDP.Client connection is closed.

To accomplish that:

  • Most CDP related files that have been modified recently were moved to a remote-interface directory in @packages/server. cdp_automation.ts was left in place, due to being similar in function to webkit-automation.ts.
  • Connection & Reconnection logic was extracted to the CDPConnection class. This class is responsible for:
    • Connecting to an ephemeral CDP.Client. When indicated, CDPConnection will create a new CDP.Client when it receives the disconnect event.
    • Cleanly closing CDP connections. This includes removing event listeners that are added directly to the CDP.Client.
    • Persisting an internal EventEmitter, so that event listeners do not need to be removed/added between ephemeral CDP.Clients.
    • Unifying the potential connection errors thrown by CDP.Client.send(), so downstream logic can determine if a command needs to be enqueued
    • Publishing connection lifecycle events that are subscribed to by CriClient
  • CriClient's event listeners are added to CDPConnection instance when the instance is created, rather than when the ephemeral CDP.Client instance is created. This simplifies the connection logic flow, at the expense of (slightly) increasing the complexity of the CriClient constructor. This change reveals a further refactor opportunity: everything necessary to construct the CDPConnection and initialize its listeners is actually passed in as parameters to CriClient. The next step for this set of files is to extract the CDPConnection creation & subscription logic up one level, and pass in the CDPConnection instance to the CriClient. This reduces the parameter list of the CriClient, removing unnecessary information sharing (CriClient is only provided CDPConnection parameters and callbacks in order to instantiate the CDPConnection).

Steps to test

There should be no changes in functionality of Cypress, other than a potential reduction in memory leak issues.

How has the user experience changed?

PR Tasks

CDPConnection class instantiates a persistent event emitter, and creates
a CDP Client instance via connect(). When this client emits a disconnect
event, the CDPConnection will automatically attempt to reconnect, if the
connection was not intentionally closed and the connection is created with
the reconnect flag. It only listens to 'event' and 'disconnect' events itself,
making it easy to unsubscribe from events in the event of a disconnection,
helping ease mental load when considering potential memory leaks.

Because it uses its own persistent event emitter, event listeners added
to the CDPConnection do not need to be re-added when the client reconnects.

The CriClient itself no longer handles reconnection logic: it only reacts to
connection state changes in order to re-send failed commands and enablements
when the CDPConnection restores the CDPClient.
@cacieprins cacieprins marked this pull request as ready for review August 6, 2024 15:59
Copy link

cypress bot commented Aug 8, 2024

cypress    Run #56578

Run Properties:  status check passed Passed #56578  •  git commit f056b90b35: Merge branch 'develop' into cacie/perf/cri-client-memory-leak
Project cypress
Branch Review cacie/perf/cri-client-memory-leak
Run status status check passed Passed #56578
Run duration 53m 22s
Commit git commit f056b90b35: Merge branch 'develop' into cacie/perf/cri-client-memory-leak
Committer Cacie Prins
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 5
Tests that did not run due to a developer annotating a test with .skip  Pending 1328
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 29312
View all changes introduced in this branch ↗︎
UI Coverage  43.26%
  Untested elements 218  
  Tested elements 170  
Accessibility  91.25%
  Failed rules  5 critical   10 serious   2 moderate   2 minor
  Failed elements 945  

cli/CHANGELOG.md Outdated
@@ -3,6 +3,10 @@

_Released 8/13/2024_

**Performance:**

- Fixed a potential memory leak 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)
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 want to call out that this is a server side leak? Maybe to help avoid confusion with some of the browser-side ones we've got going on.


inFlightCommand?.deferred.resolve(response)
} catch (err) {
debug('error re-enabling %s: ', command, err)
if (this._isConnectionError(err)) {
if (CDPDisconnectedError.isCDPDisconnectedError(err)) {
// below comment is no longer accurate - ephemeral connection is wrapped by
Copy link
Collaborator

Choose a reason for hiding this comment

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

If the comment's no longer accurate, should we delete 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.

Thanks for calling attention - noticed a bug in a path that wasn't getting tested because of it. addressed in 1fb6a52


debug('sent command, received ', { response })
enqueued.deferred.resolve(response)
debug('resolved enqueued promise')
} catch (e) {
debug('enqueued command %s failed:', enqueued.command, e)
if (this._isConnectionError(e)) {
if (CDPDisconnectedError.isCDPDisconnectedError(e)) {
// below comment is no longer accurate - ephemeral connection is wrapped by
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same as above.

Copy link
Collaborator

@ryanthemanuel ryanthemanuel left a comment

Choose a reason for hiding this comment

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

Looks great! I like the direction we're going with all of these refactors. This new code is definitely easier to follow.

cli/CHANGELOG.md Outdated Show resolved Hide resolved
@cacieprins cacieprins merged commit 8b54d3e into develop Aug 16, 2024
82 of 84 checks passed
@cacieprins cacieprins deleted the cacie/perf/cri-client-memory-leak branch August 16, 2024 14:13
@cypress-bot
Copy link
Contributor

cypress-bot bot commented Aug 27, 2024

Released in 13.14.0.

This comment thread has been locked. If you are still experiencing this issue after upgrading to
Cypress v13.14.0, please open a new issue.

@cypress-bot cypress-bot bot locked as resolved and limited conversation to collaborators Aug 27, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Potential memory leak in CriClient
3 participants