Skip to content

Commit

Permalink
perf: improve performance of experimentalSourceRewriting (#29540)
Browse files Browse the repository at this point in the history
* perf: improve performance of `experimentalSourceRewriting`

This change is in addition to benjamn/recast#1399. This commit
focuses on Cypress' use of recast, with the following optimizations:

1. Avoid printing the source file again if no change was made.
   Printing an AST using recast does reuse the original text, but identifying
   for which parts of the AST reuse is possible comes with noticeable overhead.
   By tracking changes within the visitor it becomes possible to skip this
   phase entirely if no changes were made.
2. Avoid a scope lookup (`path.scope.declares(node.name)`) for all identifiers in
   the program, doing it only for identifiers that may have to be rewritten.

With these changes, a 2.6MB bundle that does not need rewriting has improved
from 4.4s to 2.3s, provided that the above-mentioned recast PR has been applied.

* chore: move `experimentalSourceRewriting` release note to pending release section

---------

Co-authored-by: Bill Glesias <[email protected]>
Co-authored-by: Jennifer Shehane <[email protected]>
  • Loading branch information
3 people committed Jun 21, 2024
1 parent c4c3154 commit a6b58a8
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 26 deletions.
4 changes: 4 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ _Released 7/02/2024 (PENDING)_

- Fixed an issue where Chrome launch instances would not recreate the browser CRI client correctly after recovering from an unexpected browser closure. Fixes [#27657](https://github.com/cypress-io/cypress/issues/27657). Fixed in [#29663](https://github.com/cypress-io/cypress/pull/29663).

**Performance:**

- Improved performance of `experimentalSourceRewriting` option. Fixed in [#29540](https://github.com/cypress-io/cypress/pull/29540).

**Bugfixes:**

- Fixed an issue where Firefox 129 (Firefox Nightly) would not launch with Cypress. Fixes [#29713](https://github.com/cypress-io/cypress/issues/29713). Fixed in [#29720](https://github.com/cypress-io/cypress/pull/29720).
Expand Down
43 changes: 23 additions & 20 deletions packages/rewriter/lib/js-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ function resolveLocationReference () {
)
}

const replaceableProps = ['parent', 'top', 'location']

/**
* Given an Identifier or a Literal, return a property name that should use `resolveWindowReference`.
* @param node
Expand All @@ -70,12 +72,12 @@ function getReplaceablePropOfMemberExpression (node: n.MemberExpression) {
const { property } = node

// something.(top|parent)
if (n.Identifier.check(property) && ['parent', 'top', 'location'].includes(property.name)) {
if (n.Identifier.check(property) && replaceableProps.includes(property.name)) {
return property.name
}

// something['(top|parent)']
if (n.Literal.check(property) && ['parent', 'top', 'location'].includes(String(property.value))) {
if (n.Literal.check(property) && replaceableProps.includes(String(property.value))) {
return String(property.value)
}

Expand Down Expand Up @@ -104,6 +106,7 @@ export const jsRules: Visitor<{}> = {
}

path.replace(resolveWindowReference(path.get('object').node, prop))
this.reportChanged()

return false
},
Expand All @@ -120,6 +123,7 @@ export const jsRules: Visitor<{}> = {
if (isAssignee && node.name === 'location') {
// `location = 'something'`, rewrite to intercepted href setter since relative urls can break this
path.replace(b.memberExpression(resolveLocationReference(), b.identifier('href')))
this.reportChanged()

return false
}
Expand Down Expand Up @@ -147,40 +151,38 @@ export const jsRules: Visitor<{}> = {
}
}

if (path.scope.declares(node.name)) {
// identifier has been declared in local scope, don't care about replacing
// identifier has been declared in local scope, don't care about replacing
if (!replaceableProps.includes(node.name) || path.scope.declares(node.name)) {
return this.traverse(path)
}

if (node.name === 'location') {
path.replace(resolveLocationReference())

return false
}
switch (node.name) {
case 'location':
path.replace(resolveLocationReference())
this.reportChanged()

if (['parent', 'top'].includes(node.name)) {
path.replace(resolveWindowReference(globalIdentifier, node.name))
return false
case 'parent':
case 'top':
path.replace(resolveWindowReference(globalIdentifier, node.name))
this.reportChanged()

return false
return false
default:
return this.traverse(path)
}

this.traverse(path)
},
visitAssignmentExpression (path) {
const { node } = path

const finish = () => {
this.traverse(path)
}

if (!n.MemberExpression.check(node.left)) {
return finish()
return this.traverse(path)
}

const propBeingSet = getReplaceablePropOfMemberExpression(node.left)

if (!propBeingSet) {
return finish()
return this.traverse(path)
}

if (node.operator !== '=') {
Expand All @@ -194,6 +196,7 @@ export const jsRules: Visitor<{}> = {
const objBeingSetOn = node.left.object

path.replace(resolveWindowReference(objBeingSetOn, propBeingSet, node.right))
this.reportChanged()

return false
},
Expand Down
30 changes: 25 additions & 5 deletions packages/rewriter/lib/js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ export function rewriteJsSourceMap (url: string, js: string, inputSourceMap: any

const ast = recast.parse(js, { sourceFileName })

astTypes.visit(ast, jsRules)
const visitor = astTypes.PathVisitor.fromMethodsObject(jsRules)

visitor.visit(ast)

if (!visitor.wasChangeReported() && inputSourceMap) {
return inputSourceMap
}

return recast.print(ast, {
inputSourceMap,
Expand All @@ -50,18 +56,32 @@ export function rewriteJsSourceMap (url: string, js: string, inputSourceMap: any
export function _rewriteJsUnsafe (url: string, js: string, deferSourceMapRewrite?: DeferSourceMapRewriteFn): string {
const ast = recast.parse(js)

let didRewrite: boolean

try {
astTypes.visit(ast, jsRules)
const visitor = astTypes.PathVisitor.fromMethodsObject(jsRules)

visitor.visit(ast)

didRewrite = visitor.wasChangeReported()
} catch (err: any) {
// if visiting fails, it points to a bug in our rewriting logic, so raise the error to the driver
return _generateDriverError(url, err)
}

const { code } = recast.print(ast, defaultPrintOpts)
let rewritten: string

if (didRewrite) {
const { code } = recast.print(ast, defaultPrintOpts)

rewritten = code
} else {
rewritten = js
}

if (!deferSourceMapRewrite) {
// no sourcemaps
return sourceMaps.stripMappingUrl(code)
return sourceMaps.stripMappingUrl(rewritten)
}

// get an ID that can be used to lazy-generate the source map later
Expand All @@ -71,7 +91,7 @@ export function _rewriteJsUnsafe (url: string, js: string, deferSourceMapRewrite
// using a relative URL ensures that required cookies + other headers are sent along
// and can be reused if the user's sourcemap requires an HTTP request to be made
`/__cypress/source-maps/${sourceMapId}.map`,
code,
rewritten,
)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/rewriter/test/unit/js-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe('js rewriter', function () {

err.stack = 'stack'

sinon.stub(astTypes, 'visit').throws(err)
sinon.stub(astTypes.PathVisitor, 'fromMethodsObject').throws(err)

const actual = _rewriteJsUnsafe(URL, 'console.log()')

Expand Down

5 comments on commit a6b58a8

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on a6b58a8 Jun 21, 2024

Choose a reason for hiding this comment

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

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.12.1/linux-x64/develop-a6b58a8b2ff10801267e5aaf20c24ce97a395c1e/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on a6b58a8 Jun 21, 2024

Choose a reason for hiding this comment

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

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.12.1/linux-arm64/develop-a6b58a8b2ff10801267e5aaf20c24ce97a395c1e/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on a6b58a8 Jun 21, 2024

Choose a reason for hiding this comment

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

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.12.1/win32-x64/develop-a6b58a8b2ff10801267e5aaf20c24ce97a395c1e/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on a6b58a8 Jun 21, 2024

Choose a reason for hiding this comment

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

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.12.1/darwin-arm64/develop-a6b58a8b2ff10801267e5aaf20c24ce97a395c1e/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on a6b58a8 Jun 21, 2024

Choose a reason for hiding this comment

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

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.12.1/darwin-x64/develop-a6b58a8b2ff10801267e5aaf20c24ce97a395c1e/cypress.tgz

Please sign in to comment.