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

feat: various v8 snapshot improvements #24909

Merged
merged 60 commits into from Dec 3, 2022
Merged

Conversation

ryanthemanuel
Copy link
Collaborator

@ryanthemanuel ryanthemanuel commented Nov 30, 2022

User facing changelog

Further improved the startup time and decreased the size of the binary by using bytenode to pre-compile the entry point.

Additional details

  • Bundle and precompile the entry point for the binary. This will improve start up time a bit and shrink the binary size even further. We use bytenode as the mechanism to precompile the entry point.
  • Establish integrity checks for code within the binary. bytenode and v8 already provide some level of integrity checking on the compiled code it generates, but we also added additional checks to protect the binary even further.

Steps to test

The best way to test this is with the downloaded binary attached to the last commit of this PR.

How has the user experience changed?

n/a

PR Tasks

  • [na] Have tests been added/updated?
  • [na] Has the original issue (or this PR, if no issue exists) been tagged with a release in ZenHub? (user-facing changes only)
  • [na] Has a PR for user-facing changes been opened in cypress-documentation?
  • [na] Have API changes been updated in the type definitions?

@cypress-bot
Copy link
Contributor

cypress-bot bot commented Nov 30, 2022

Thanks for taking the time to open a PR!

@ryanthemanuel ryanthemanuel marked this pull request as ready for review November 30, 2022 23:06

hookRequire(true)
}
hookRequire({ forceTypeScript: true })
Copy link
Member

Choose a reason for hiding this comment

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

how do we know to force typescript?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This file never runs in prod (see line 2). It only runs in development where we still need to force typescript compilation on the fly.

await cleanup(outputFolder)
// Build out the entry point and clean up prior to setting up v8 snapshots so that the state of the binary is correct
await buildEntryPointAndCleanup(outputFolder)
await setupV8Snapshots({ cypressAppPath: params.appOutDir, integrityCheckSource: getIntegrityCheckSource(outputFolder) })
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
await setupV8Snapshots({ cypressAppPath: params.appOutDir, integrityCheckSource: getIntegrityCheckSource(outputFolder) })
await setupV8Snapshots({
cypressAppPath: params.appOutDir, integrityCheckSource:
getIntegrityCheckSource(outputFolder),
})

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated: 5c1f981 (#24909)

const cleanup = async (buildAppDir) => {
const createServerEntryPointBundle = async (buildAppDir) => {
const unixBuildAppDir = buildAppDir.split(path.sep).join(path.posix.sep)
const entryPoints = [path.join(unixBuildAppDir, 'packages/server/index.js')]
Copy link
Member

Choose a reason for hiding this comment

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

Should this be:

Suggested change
const entryPoints = [path.join(unixBuildAppDir, 'packages/server/index.js')]
const entryPoints = [unixBuildAppDir, 'packages', 'server' , 'index.js'].join(path.posix.sep)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am trying to limit node internals references as much as I possibly can. path.posix.sep is something we already handle uniquely in the snapshot (since when we're generating the snapshot we don't have access to path or any other node internals), so I just decided to reuse what we had available.

See:

https://github.com/cypress-io/cypress/blob/develop/tooling/v8-snapshot/src/generator/blueprint.ts#L56

and

https://github.com/cypress-io/cypress/blob/develop/tooling/v8-snapshot/src/generator/blueprint.ts#L110

electron: true,
})

return [...Object.keys(esbuildResult.metafile.inputs)].map((input) => `./${input}`)
Copy link
Member

Choose a reason for hiding this comment

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

Should this be a hard-coded separator?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, the way these files are read in assumes "require" syntax not platform specific syntax.

// 1. Retrieve all dependencies that still need to be kept in the binary. In theory, we could use the bundles generated here as single files within the binary,
// but for now, we just track on the dependencies that get pulled in
const keptDependencies = [...await getDependencyPathsToKeep(buildAppDir), 'package.json', 'packages/server/server-entry.js']
const keptDependencies = [...await getDependencyPathsToKeep(buildAppDir), 'package.json']
Copy link
Member

Choose a reason for hiding this comment

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

Can we use promise.all for getting getDependencyPathsToKeep & createServerEntryPointBundle?

const [
  keptDependencies,
  serverPointEntries,
] = Promise.all(getDependencyPathsToKeep(buildAppDir), createServerEntryPointBundle(buildAppDir)]

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I like this idea. Will work on it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated: 5c1f981 (#24909)


process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV || 'production'
try {
require('./node_modules/bytenode/lib/index.js')
Copy link
Member

Choose a reason for hiding this comment

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

is index.js not the default export?

Suggested change
require('./node_modules/bytenode/lib/index.js')
require('bytenode/lib/index.js')

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm actually trying to purposefully reference the exact file here. I don't anyone to be able to use any kind of module loading weirdness to reference a different file (may not be possible, but just in case).

// eslint-disable-next-line no-unused-vars
const stackIntegrityCheck = function stackIntegrityCheck (options) {
const originalStackTraceLimit = OrigError.stackTraceLimit
const originalStackTrace = OrigError.prepareStackTrace
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const originalStackTrace = OrigError.prepareStackTrace
const originalPrepareStackTrace = OrigError.prepareStackTrace

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated: 5c1f981 (#24909)

Comment on lines 8 to 9
const originalStackTraceLimit = OrigError.stackTraceLimit
const originalStackTrace = OrigError.prepareStackTrace
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to create each time or can we make these global?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This function is really only called once. I like the idea of storing this off just in time since we are replacing them immediately after. The other global concepts (Error, captureStackTrace, etc.) are ones that we don't do a replacement dance with.

return stack
}

const tempError = new OrigError
Copy link
Member

Choose a reason for hiding this comment

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

Is is possible clone the class instance so we don't need to reset the overrides?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think there's some alternative things we can do here, but I think I like this pattern best.

const toString = Function.prototype.toString
const callFn = Function.call

// eslint-disable-next-line no-unused-vars
Copy link
Member

Choose a reason for hiding this comment

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

is this disable check needed? we are using it below without conditional checks

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yeah. This can be removed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed: 5c1f981 (#24909)

}

for (let index = 0; index < options.stackToMatch.length; index++) {
const expectedFunctionName = options.stackToMatch[index].functionName
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const expectedFunctionName = options.stackToMatch[index].functionName
const { expectedFunctionName, expectedFileName} = options.stackToMatch[index]

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated: 5c1f981 (#24909)

const expectedFileName = options.stackToMatch[index].fileName
const actualFileName = stack[index].getFileName()

if (expectedFunctionName && actualFunctionName !== expectedFunctionName) {
Copy link
Member

Choose a reason for hiding this comment

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

should we throw if expectedFunctionName or expectedFunctionName are undefined? can we get in this situation?

Copy link
Member

Choose a reason for hiding this comment

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

looking below looks like expectedFunctionName depends on the file

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We control what's passed in here so we shouldn't be in that situation.

}

function validateFs (fs) {
if (toString.call(fs.readFileSync) !== `function(t,r){const n=splitPath(t);if(!n.isAsar)return g.apply(this,arguments);const{asarPath:i,filePath:a}=n,o=getOrCreateArchive(i);if(!o)throw createError("INVALID_ARCHIVE",{asarPath:i});const c=o.getFileInfo(a);if(!c)throw createError("NOT_FOUND",{asarPath:i,filePath:a});if(0===c.size)return r?"":s.Buffer.alloc(0);if(c.unpacked){const t=o.copyFileOut(a);return e.readFileSync(t,r)}if(r){if("string"==typeof r)r={encoding:r};else if("object"!=typeof r)throw new TypeError("Bad arguments")}else r={encoding:null};const{encoding:f}=r,l=s.Buffer.alloc(c.size),u=o.getFdAndValidateIntegrityLater();if(!(u>=0))throw createError("NOT_FOUND",{asarPath:i,filePath:a});return logASARAccess(i,a,c.offset),e.readSync(u,l,0,c.size,c.offset),validateBufferIntegrity(l,c.integrity),f?l.toString(f):l}`) {
Copy link
Member

Choose a reason for hiding this comment

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

risk this will ever be different given it is internal node code?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is actually electron code not node code and there's not really a good way for me to get at it at package time. I think we are safe to update this as we update electron (if we need to). I'll add a comment for posterity though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Clarified: 5c1f981 (#24909)

Copy link
Member

@emilyrohrbough emilyrohrbough left a comment

Choose a reason for hiding this comment

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

solid changes 👍🏻 most comments are minor.

ryanthemanuel and others added 3 commits December 2, 2022 12:46
Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com>
Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com>
@ryanthemanuel ryanthemanuel merged commit 57b0eac into develop Dec 3, 2022
@ryanthemanuel ryanthemanuel deleted the ryanm/fix/v8-improvements branch December 3, 2022 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants