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: support activated service worker that is not handling requests #29349

Merged
merged 41 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c8f3cfa
updating controlledUrls
mschile Apr 7, 2024
fe79da5
updating to start handling requests
mschile Apr 8, 2024
fa9bc30
updating workflow [run ci]
mschile Apr 8, 2024
5e08502
bump cache
mschile Apr 8, 2024
49febb1
commenting out assertion
mschile Apr 8, 2024
4de2375
await clients.claim [run ci]
mschile Apr 8, 2024
67db370
fixing ci build issue [run ci]
mschile Apr 9, 2024
17c15e8
overriding clients.claim
mschile Apr 9, 2024
7958bb8
handling document requests [run ci]
mschile Apr 10, 2024
8b0caab
adding document check [run ci]
mschile Apr 11, 2024
49c29b3
updates
mschile Apr 12, 2024
fa06db9
updates
mschile Apr 12, 2024
446d5ea
updating workflow
mschile Apr 12, 2024
49b6a7d
skip injection for non-chromium browsers
mschile Apr 16, 2024
1b18303
Merge branch 'develop' into mschile/service_worker_uncontrolled
mschile Apr 17, 2024
c25d535
adding driver test
mschile Apr 17, 2024
b2f64ec
Merge branch 'develop' into mschile/service_worker_uncontrolled
mschile Apr 17, 2024
6bbb116
updating failed tests
mschile Apr 17, 2024
2275a4b
Merge branch 'mschile/service_worker_uncontrolled' of https://github.…
mschile Apr 17, 2024
c47a777
unit test updates
mschile Apr 18, 2024
e7962ed
update
mschile Apr 18, 2024
6fea57d
updating changelog
mschile Apr 18, 2024
d278c47
update changelog
mschile Apr 18, 2024
0ee0c45
Merge branch 'develop' into mschile/service_worker_uncontrolled
mschile Apr 18, 2024
b988108
Merge branch 'develop' into mschile/service_worker_uncontrolled
jennifer-shehane Apr 19, 2024
0b7c585
Apply suggestions from code review
mschile Apr 19, 2024
9ee2791
support redirected requests
mschile Apr 20, 2024
1a5bcb9
Apply suggestions from code review
mschile Apr 22, 2024
f73daf5
Update packages/proxy/test/unit/http/util/service-worker-manager.spec.ts
mschile Apr 22, 2024
a9c1c49
fixing type issue
mschile Apr 22, 2024
88474d3
revert stop only change
mschile Apr 22, 2024
328bdd1
Merge branch 'develop' into mschile/service_worker_uncontrolled
mschile Apr 22, 2024
0e80622
Merge branch 'develop' into mschile/service_worker_uncontrolled
jennifer-shehane Apr 22, 2024
1cedbc4
updating cache version
mschile Apr 22, 2024
a276cfc
update
mschile Apr 23, 2024
fc74262
Merge branch 'develop' into mschile/service_worker_uncontrolled
jennifer-shehane Apr 23, 2024
f3e9fbe
reset pendingPreRequestsToRemove
mschile Apr 23, 2024
bc767fd
sweep pendingPreRequestsToRemove
mschile Apr 23, 2024
7c15d36
Merge branch 'develop' into mschile/service_worker_uncontrolled
jennifer-shehane Apr 23, 2024
48d2525
adding timeout
mschile Apr 23, 2024
d13e2e4
Merge branch 'develop' into mschile/service_worker_uncontrolled
mschile Apr 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/cache-version.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Bump this version to force CI to re-create the cache from scratch.

04-15-24-macstadium-3
04-22-24
15 changes: 5 additions & 10 deletions .circleci/workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ mainBuildFilters: &mainBuildFilters
- develop
- /^release\/\d+\.\d+\.\d+$/
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- 'cacie/fix/websocket-closed'
- 'app-mem-mng-flag'
- 'publish-binary'
- 'update-v8-snapshot-cache-on-develop'

# 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 @@ -42,8 +40,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: [ 'cacie/fix/websocket-closed', << pipeline.git.branch >> ]
- equal: [ 'app-mem-mng-flag', << pipeline.git.branch >> ]
- equal: [ 'mschile/service_worker_uncontrolled', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand All @@ -54,8 +51,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: [ 'cacie/fix/websocket-closed', << pipeline.git.branch >> ]
- equal: [ 'app-mem-mng-flag', << pipeline.git.branch >> ]
- equal: [ 'mschile/service_worker_uncontrolled', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand All @@ -78,8 +74,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: [ 'cacie/fix/websocket-closed', << pipeline.git.branch >> ]
- equal: [ 'app-mem-mng-flag', << pipeline.git.branch >> ]
- equal: [ 'mschile/service_worker_uncontrolled', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
Expand Down Expand Up @@ -146,7 +141,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" != "publish-binary" && "$CIRCLE_BRANCH" != "feat/psrotocol_shadow_dom_support" && "$CIRCLE_BRANCH" != "feat/support_wds5" && "$CIRCLE_BRANCH" != "cacie/fix/websocket-closed" ]]; then
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "mschile/service_worker_uncontrolled" ]]; 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
6 changes: 5 additions & 1 deletion cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@

_Released 4/23/2024 (PENDING)_

**Performance:**

- Fixed a performance issue with activated service workers that aren't controlling clients which could lead to correlation timeouts. Fixes [#29333](https://github.com/cypress-io/cypress/issues/29333) and [#29126](https://github.com/cypress-io/cypress/issues/29126).

**Bugfixes:**

- Fixed a bug introduced in [`13.6.0`](https://docs.cypress.io/guides/references/changelog#13-6-0) where Cypress would occasionally exit with status code 1, even when a test run was successfully, due to an unhandled WebSocket exception (`Error: WebSocket connection closed`). Addresses [#28523](https://github.com/cypress-io/cypress/issues/28523).
- Fixed a regression introduced in [`13.6.0`](https://docs.cypress.io/guides/references/changelog#13-6-0) where Cypress would occasionally exit with status code 1, even when a test run was successful, due to an unhandled WebSocket exception (`Error: WebSocket connection closed`). Addresses [#28523](https://github.com/cypress-io/cypress/issues/28523).
- Fixed an issue where Cypress would hang on some commands when an invalid `timeout` option was provided. Fixes [#29323](https://github.com/cypress-io/cypress/issues/29323).

**Dependency Updates:**
Expand Down
47 changes: 47 additions & 0 deletions packages/driver/cypress/e2e/e2e/service-worker.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -666,4 +666,51 @@ describe('service workers', { defaultCommandTimeout: 1000, pageLoadTimeout: 1000
cy.get('#output').should('have.text', 'done')
validateFetchHandlers({ listenerCount: 1 })
})

it('supports a service worker that is activated but not handling fetch events', () => {
const script = () => {
self.addEventListener('fetch', function (event) {
event.respondWith(fetch(event.request))
})
}

cy.intercept('/fixtures/service-worker.js', (req) => {
req.reply(`(${script})()`,
{ 'Content-Type': 'application/javascript' })
})

cy.visit('fixtures/service-worker.html?skipReload')

cy.get('#output').should('have.text', 'done')
validateFetchHandlers({ listenerCount: 1 })
})

it('supports a redirected request', () => {
const script = () => {
self.addEventListener('fetch', function (event) {
return
})
}

cy.intercept('/fixtures/service-worker.js', (req) => {
req.reply(`(${script})()`,
{ 'Content-Type': 'application/javascript' })
})

cy.intercept('/fixtures/1mb*', (req) => {
req.reply({
statusCode: 302,
headers: {
location: '/fixtures/redirected',
},
})
})

cy.intercept('/fixtures/redirected', (req) => {
req.reply('redirected')
})

cy.visit('fixtures/service-worker.html')
cy.get('#output').should('have.text', 'done')
})
})
13 changes: 10 additions & 3 deletions packages/driver/cypress/fixtures/service-worker.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<title></title>
<script>
const makeRequest = () => {
fetch(`/fixtures/1mb?j=${Math.random()}`).then((response) => {
fetch(`/fixtures/1mb?j=${Math.random()}#hash`).then((response) => {
document.getElementById('output').innerText = 'done'
}).catch(() => {})
}
Expand All @@ -14,15 +14,22 @@
const serviceWorker = registration.installing || registration.waiting
if (serviceWorker) {
let controllerChangeCalled = false
// called when clients.claim() is called in the service worker
navigator.serviceWorker.addEventListener('controllerchange', (e) => {
controllerChangeCalled = true
makeRequest()
})

serviceWorker.addEventListener('statechange', (e) => {
if (e.target.state === 'activated' && !controllerChangeCalled) {
// service worker activated, reload page so it can intercept requests
location.reload()
// service worker activated, either skip reloading the page
// so we can test when an activated service worker is not intercepting requests
// or reload the page so it can intercept requests
if (location.search.includes('skipReload')) {
makeRequest()
} else {
location.reload()
}
}
})
} else {
Expand Down
6 changes: 5 additions & 1 deletion packages/proxy/lib/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type { Request, Response } from 'express'
import type { RemoteStates } from '@packages/server/lib/remote_states'
import type { CookieJar, SerializableAutomationCookie } from '@packages/server/lib/util/cookies'
import type { ResourceTypeAndCredentialManager } from '@packages/server/lib/util/resourceTypeAndCredentialManager'
import type { ProtocolManagerShape } from '@packages/types'
import type { FoundBrowser, ProtocolManagerShape } from '@packages/types'
import type Protocol from 'devtools-protocol'
import type { ServiceWorkerClientEvent } from './util/service-worker-manager'

Expand Down Expand Up @@ -101,6 +101,7 @@ export type ServerCtx = Readonly<{
socket: CyServer.Socket
request: any
serverBus: EventEmitter
getCurrentBrowser: () => FoundBrowser
}>

const READONLY_MIDDLEWARE_KEYS: (keyof HttpMiddlewareThis<{}>)[] = [
Expand Down Expand Up @@ -272,6 +273,7 @@ export class Http {
middleware: HttpMiddlewareStacks
netStubbingState: NetStubbingState
preRequests: PreRequests = new PreRequests()
getCurrentBrowser: () => FoundBrowser
request: any
socket: CyServer.Socket
serverBus: EventEmitter
Expand All @@ -296,6 +298,7 @@ export class Http {
this.serverBus = opts.serverBus
this.resourceTypeAndCredentialManager = opts.resourceTypeAndCredentialManager
this.getCookieJar = opts.getCookieJar
this.getCurrentBrowser = opts.getCurrentBrowser

if (typeof opts.middleware === 'undefined') {
this.middleware = defaultMiddleware
Expand Down Expand Up @@ -361,6 +364,7 @@ export class Http {
this.preRequests.removePendingRequest(pendingRequest)
},
protocolManager: this.protocolManager,
getCurrentBrowser: this.getCurrentBrowser,
}

const onError = (error: Error): Promise<void> => {
Expand Down
3 changes: 2 additions & 1 deletion packages/proxy/lib/http/response-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,8 @@ const MaybeInjectServiceWorker: ResponseMiddleware = function () {

span?.setAttributes({ hasServiceWorkerHeader: hasHeader })

if (!hasHeader) {
// skip if we don't have the header or we're not in chromium
if (!hasHeader || this.getCurrentBrowser().family !== 'chromium') {
span?.end()

return this.next()
Expand Down
47 changes: 46 additions & 1 deletion packages/proxy/lib/http/util/prerequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class PreRequests {
pendingPreRequests = new QueueMap<PendingPreRequest>()
pendingRequests = new QueueMap<PendingRequest>()
pendingUrlsWithoutPreRequests = new QueueMap<PendingUrlWithoutPreRequest>()
pendingPreRequestsToRemove: Map<string, number> = new Map()
sweepIntervalTimer: NodeJS.Timeout
protocolManager?: ProtocolManagerShape

Expand Down Expand Up @@ -144,21 +145,43 @@ export class PreRequests {
debugVerbose('timed out unmatched pre-request: %o', browserPreRequest)
metrics.unmatchedPreRequests++

this.pendingPreRequestsToRemove.delete(browserPreRequest.requestId)

return false
}

return true
})

this.pendingPreRequestsToRemove.forEach((timestamp, requestId) => {
if (timestamp + this.sweepInterval < now) {
this.pendingPreRequestsToRemove.delete(requestId)
}
})

this.pendingUrlsWithoutPreRequests.removeMatching(({ timestamp }) => {
return timestamp + this.sweepInterval >= now
})
}, this.sweepInterval)
}

checkAndRemovePendingPreRequest (requestId: string) {
return this.pendingPreRequestsToRemove.delete(requestId)
}

addPending (browserPreRequest: BrowserPreRequest) {
const key = `${browserPreRequest.method}-${tryDecodeURI(browserPreRequest.url)}`

// if we have a pending request to remove, we should remove it now and return
// since we don't want to proceed with this pre-request anymore. In practice,
// this typically happens when we receive a cached response while executing
// the async function to add the pre-request
if (this.checkAndRemovePendingPreRequest(browserPreRequest.requestId)) {
debugVerbose('Received previous request to remove pre-request %s, skipping adding', browserPreRequest.requestId)

return
}

metrics.browserPreRequestsReceived++
const pendingRequest = this.pendingRequests.shift(key)

Expand Down Expand Up @@ -225,9 +248,30 @@ export class PreRequests {
}

removePendingPreRequest (requestId: string) {
let removed = false

this.pendingPreRequests.removeMatching(({ browserPreRequest }) => {
return (browserPreRequest.requestId.includes('-retry-') && !browserPreRequest.requestId.startsWith(`${requestId}-`)) || (!browserPreRequest.requestId.includes('-retry-') && browserPreRequest.requestId !== requestId)
const matches = (browserPreRequest.requestId.includes('-retry-') && browserPreRequest.requestId.startsWith(`${requestId}-`)) ||
(!browserPreRequest.requestId.includes('-retry-') && browserPreRequest.requestId === requestId)

if (matches && !removed) {
removed = true
}

return !matches
})

// if we didn't find a pre-request to remove, add it to the pending list so we can remove it later,
// this typically happens when we receive a cached response while executing
// the async function to add the pre-request
if (!removed) {
debugVerbose('No pre-request found to remove, adding to pending list: %s', requestId)
this.pendingPreRequestsToRemove.set(requestId, Date.now())
} else {
debugVerbose('Removed pre-request %s', requestId)
}

return removed
}

get (req: CypressIncomingRequest, ctxDebug, callback: GetPreRequestCb) {
Expand Down Expand Up @@ -322,5 +366,6 @@ export class PreRequests {

this.pendingRequests = new QueueMap<PendingRequest>()
this.pendingUrlsWithoutPreRequests = new QueueMap<PendingUrlWithoutPreRequest>()
this.pendingPreRequestsToRemove = new Map()
}
}