UNPKG

prepack

Version:

Execute a JS bundle, serialize global state and side effects to a snapshot that can be quickly restored.

615 lines (533 loc) 28.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ResidualFunctions = undefined; var _errors = require("../errors.js"); var _realm = require("../realm.js"); var _index = require("../values/index.js"); var _babelTypes = require("babel-types"); var t = _interopRequireWildcard(_babelTypes); var _babelTraverse = require("babel-traverse"); var _babelTraverse2 = _interopRequireDefault(_babelTraverse); var _invariant = require("../invariant.js"); var _invariant2 = _interopRequireDefault(_invariant); var _types = require("./types.js"); var _visitors = require("./visitors.js"); var _modules = require("../utils/modules.js"); var _ResidualFunctionInitializers = require("./ResidualFunctionInitializers.js"); var _internalizer = require("../utils/internalizer.js"); var _Referentializer = require("./Referentializer.js"); var _utils = require("./utils.js"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } class ResidualFunctions { constructor(realm, statistics, options, modules, requireReturns, locationService, prelude, initializerNameGenerator, factoryNameGenerator, residualFunctionInfos, residualFunctionInstances, residualClassMethodInstances, additionalFunctionValueInfos, additionalFunctionValueNestedFunctions, referentializer) { this.realm = realm; this.statistics = statistics; this.modules = modules; this.requireReturns = requireReturns; this.locationService = locationService; this.prelude = prelude; this.factoryNameGenerator = factoryNameGenerator; this.functionPrototypes = new Map(); this.firstFunctionUsages = new Map(); this.functions = new Map(); this.classes = new Map(); this.functionInstances = []; this.residualFunctionInitializers = new _ResidualFunctionInitializers.ResidualFunctionInitializers(locationService, prelude, initializerNameGenerator); this.residualFunctionInfos = residualFunctionInfos; this.residualFunctionInstances = residualFunctionInstances; this.residualClassMethodInstances = residualClassMethodInstances; this.additionalFunctionValueInfos = additionalFunctionValueInfos; this.referentializer = referentializer; for (let instance of residualFunctionInstances.values()) { (0, _invariant2.default)(instance !== undefined); if (!additionalFunctionValueInfos.has(instance.functionValue)) this.addFunctionInstance(instance); } this.additionalFunctionValueNestedFunctions = additionalFunctionValueNestedFunctions; this.simpleClosures = !!options.simpleClosures; } addFunctionInstance(instance) { this.functionInstances.push(instance); let code = instance.functionValue.$ECMAScriptCode; (0, _invariant2.default)(code != null); (0, _utils.getOrDefault)(this.functions, code, () => []).push(instance); } setFunctionPrototype(constructor, prototypeId) { this.functionPrototypes.set(constructor, prototypeId); } addFunctionUsage(val, bodyReference) { if (!this.firstFunctionUsages.has(val)) this.firstFunctionUsages.set(val, bodyReference); } _shouldUseFactoryFunction(funcBody, instances) { function shouldInlineFunction() { let shouldInline = true; if (funcBody.start && funcBody.end) { let bodySize = funcBody.end - funcBody.start; shouldInline = bodySize <= 30; } return shouldInline; } let functionInfo = this.residualFunctionInfos.get(funcBody); (0, _invariant2.default)(functionInfo); let { usesArguments } = functionInfo; return !shouldInlineFunction() && instances.length > 1 && !usesArguments && !this.simpleClosures; } // Note: this function takes linear time. Please do not call it inside loop. _hasRewrittenFunctionInstance(rewrittenAdditionalFunctions, instances) { return instances.find(instance => rewrittenAdditionalFunctions.has(instance.functionValue)) !== undefined; } _generateFactoryFunctionInfos(rewrittenAdditionalFunctions) { const factoryFunctionInfos = new Map(); for (const [functionBody, instances] of this.functions) { (0, _invariant2.default)(instances.length > 0); let factoryId; const suffix = instances[0].functionValue.__originalName || this.realm.debugNames ? "factoryFunction" : ""; if (this._shouldUseFactoryFunction(functionBody, instances)) { // Rewritten function should never use factory function. (0, _invariant2.default)(!this._hasRewrittenFunctionInstance(rewrittenAdditionalFunctions, instances)); factoryId = t.identifier(this.factoryNameGenerator.generate(suffix)); } else { // For inline function body case, use the first function as the factory function. factoryId = this.locationService.getLocation(instances[0].functionValue); } const functionUniqueTag = functionBody.uniqueOrderedTag; (0, _invariant2.default)(functionUniqueTag); const functionInfo = this.residualFunctionInfos.get(functionBody); (0, _invariant2.default)(functionInfo); factoryFunctionInfos.set(functionUniqueTag, { factoryId, functionInfo }); } return factoryFunctionInfos; } // Preserve residual functions' ordering based on its ast dfs traversal order. // This is necessary to prevent unexpected code locality issues. _sortFunctionByOriginalOrdering(functionEntries) { functionEntries.sort((funcA, funcB) => { const funcAUniqueTag = funcA[0].uniqueOrderedTag; (0, _invariant2.default)(funcAUniqueTag); const funcBUniqueTag = funcB[0].uniqueOrderedTag; (0, _invariant2.default)(funcBUniqueTag); return funcAUniqueTag - funcBUniqueTag; }); } spliceFunctions(rewrittenAdditionalFunctions) { this.residualFunctionInitializers.scrubFunctionInitializers(); let functionBodies = new Map(); // these need to get spliced in at the end let additionalFunctionPreludes = new Map(); let additionalFunctionModifiedBindingsSegment = new Map(); let getModifiedBindingsSegment = additionalFunction => (0, _utils.getOrDefault)(additionalFunctionModifiedBindingsSegment, additionalFunction, () => []); let getFunctionBody = instance => (0, _utils.getOrDefault)(functionBodies, instance, () => []); let globalPrelude = this.prelude; function getPrelude(instance) { let additionalFunction = instance.containingAdditionalFunction; let b; if (additionalFunction) { b = (0, _utils.getOrDefault)(additionalFunctionPreludes, additionalFunction, () => []); } else { b = globalPrelude; } return b; } let requireStatistics = { replaced: 0, count: 0 }; let functionEntries = Array.from(this.functions.entries()); this._sortFunctionByOriginalOrdering(functionEntries); this.statistics.functions = functionEntries.length; let unstrictFunctionBodies = []; let strictFunctionBodies = []; let funcNodes = new Map(); let defineFunction = (instance, funcId, funcOrClassNode) => { let { functionValue } = instance; if (instance.initializationStatements.length > 0) { // always add initialization statements to insertion point let initializationBody = getFunctionBody(instance); Array.prototype.push.apply(initializationBody, instance.initializationStatements); } let body; if (t.isFunctionExpression(funcOrClassNode)) { funcNodes.set(functionValue, funcOrClassNode); body = getPrelude(instance); } else { (0, _invariant2.default)(t.isCallExpression(funcOrClassNode) || t.isClassExpression(funcOrClassNode)); // .bind call body = getFunctionBody(instance); } body.push(t.variableDeclaration("var", [t.variableDeclarator(funcId, funcOrClassNode)])); let prototypeId = this.functionPrototypes.get(functionValue); if (prototypeId !== undefined) { let id = this.locationService.getLocation(functionValue); (0, _invariant2.default)(id !== undefined); body.push(t.variableDeclaration("var", [t.variableDeclarator(prototypeId, t.memberExpression(id, t.identifier("prototype")))])); } }; // Emit code for ModifiedBindings for additional functions for (let [funcValue, funcInfo] of this.additionalFunctionValueInfos) { for (let [, residualBinding] of funcInfo.modifiedBindings) { let scope = residualBinding.scope; // TODO #989: This should probably be an invariant once captures work properly // Currently we don't referentialize bindings in additional functions (but we // do for bindings nested in additional functions) if (!residualBinding.referentialized) continue; // Find the proper prelude to emit to (global vs additional function's prelude) let bodySegment = getModifiedBindingsSegment(funcValue); // binding has been referentialized, so setup the scope to be able to // access bindings from other __captured_scopes initializers if (scope && scope.containingAdditionalFunction !== funcValue) { let decl = t.variableDeclaration("var", [t.variableDeclarator(t.identifier(scope.name), t.numericLiteral(scope.id))]); let init = this.referentializer.getReferentializedScopeInitialization(scope); bodySegment.push(decl); // flow forces me to do this Array.prototype.push.apply(bodySegment, init); } let newValue = residualBinding.additionalValueSerialized; (0, _invariant2.default)(newValue); let binding_reference = residualBinding.serializedValue; (0, _invariant2.default)(binding_reference); (0, _invariant2.default)(t.isLVal(binding_reference), "Referentialized values are always LVals"); // This mutation is safe because it should always be either a global identifier (for global bindings) // or an accessor to a referentialized value. bodySegment.push(t.expressionStatement(t.assignmentExpression("=", binding_reference, newValue))); } } // Process Additional Functions for (let [funcValue, additionalFunctionInfo] of this.additionalFunctionValueInfos.entries()) { let { instance } = additionalFunctionInfo; let functionValue = funcValue; let params = functionValue.$FormalParameters; (0, _invariant2.default)(params !== undefined); let rewrittenBody = rewrittenAdditionalFunctions.get(funcValue); (0, _invariant2.default)(rewrittenBody); // rewritten functions shouldn't have references fixed up because the body, // consists of serialized code. For simplicity we emit their instances in a naive way let functionBody = t.blockStatement(rewrittenBody); let funcParams = params.slice(); let funcOrClassNode; if (this.residualClassMethodInstances.has(funcValue)) { let classMethodInstance = this.residualClassMethodInstances.get(funcValue); (0, _invariant2.default)(classMethodInstance); let { methodType, classMethodKeyNode, classSuperNode, classMethodComputed, classPrototype, classMethodIsStatic } = classMethodInstance; let isConstructor = methodType === "constructor"; (0, _invariant2.default)(classPrototype instanceof _index.ObjectValue); (0, _invariant2.default)(classMethodKeyNode && (t.isExpression(classMethodKeyNode) || t.isIdentifier(classMethodKeyNode))); // we use the classPrototype as the key to get the class expression ast node funcOrClassNode = this._getOrCreateClassNode(classPrototype); let classMethod = t.classMethod(methodType, classMethodKeyNode, funcParams, functionBody, classMethodComputed, classMethodIsStatic); // add the class method to the class expression node body if (isConstructor) { funcOrClassNode.body.body.unshift(classMethod); } else { funcOrClassNode.body.body.push(classMethod); } // we only return the funcOrClassNode if this is the constructor if (!isConstructor) { continue; } // handle the class super if (classSuperNode !== undefined) { funcOrClassNode.superClass = classSuperNode; } } else { funcOrClassNode = t.functionExpression(null, funcParams, functionBody); } let id = this.locationService.getLocation(funcValue); (0, _invariant2.default)(id !== undefined); if (funcValue.$Strict) { strictFunctionBodies.push(funcOrClassNode); } else { unstrictFunctionBodies.push(funcOrClassNode); } defineFunction(instance, id, funcOrClassNode); } // Process normal functions const factoryFunctionInfos = this._generateFactoryFunctionInfos(rewrittenAdditionalFunctions); for (let [funcBody, instances] of functionEntries) { let functionInfo = this.residualFunctionInfos.get(funcBody); (0, _invariant2.default)(functionInfo); let { unbound, modified, usesThis } = functionInfo; let params = instances[0].functionValue.$FormalParameters; (0, _invariant2.default)(params !== undefined); // Split instances into normal or nested in an additional function let normalInstances = []; let additionalFunctionNestedInstances = []; for (let instance of instances) { if (this.additionalFunctionValueNestedFunctions.has(instance.functionValue)) additionalFunctionNestedInstances.push(instance);else normalInstances.push(instance); } let naiveProcessInstances = instancesToSplice => { this.statistics.functionClones += instancesToSplice.length - 1; for (let instance of instancesToSplice) { let { functionValue, residualFunctionBindings, scopeInstances } = instance; let funcOrClassNode; if (this.residualClassMethodInstances.has(functionValue)) { let classMethodInstance = this.residualClassMethodInstances.get(functionValue); (0, _invariant2.default)(classMethodInstance); let { classSuperNode, classMethodKeyNode, methodType, classMethodComputed, classPrototype, classMethodIsStatic } = classMethodInstance; let isConstructor = methodType === "constructor"; (0, _invariant2.default)(classPrototype instanceof _index.ObjectValue); (0, _invariant2.default)(classMethodKeyNode); (0, _invariant2.default)(t.isExpression(classMethodKeyNode) || t.isIdentifier(classMethodKeyNode)); // we use the classPrototype as the key to get the class expression ast node funcOrClassNode = this._getOrCreateClassNode(classPrototype); // if we are dealing with a constructor, don't serialize it if the original // had an empty user-land constructor (because we create a constructor behind the scenes for them) let hasEmptyConstructor = !!functionValue.$HasEmptyConstructor; if (!isConstructor || isConstructor && !hasEmptyConstructor) { let methodParams = params.slice(); let methodBody = t.cloneDeep(funcBody); // create the class method AST let classMethod = t.classMethod(methodType, classMethodKeyNode, methodParams, methodBody, classMethodComputed, classMethodIsStatic); // traverse and replace refs in the class method (0, _babelTraverse2.default)(t.file(t.program([t.expressionStatement(t.classExpression(null, null, t.classBody([classMethod]), []))])), _visitors.ClosureRefReplacer, null, { residualFunctionBindings, modified, requireReturns: this.requireReturns, requireStatistics, getModuleIdIfNodeIsRequireFunction: this.modules.getGetModuleIdIfNodeIsRequireFunction(methodParams, [functionValue]), factoryFunctionInfos }); // add the class method to the class expression node body if (isConstructor) { funcOrClassNode.body.body.unshift(classMethod); } else { funcOrClassNode.body.body.push(classMethod); } } // we only return the funcOrClassNode if this is the constructor if (!isConstructor) { continue; } // handle the class super if (classSuperNode !== undefined) { funcOrClassNode.superClass = classSuperNode; } } else { let funcParams = params.slice(); funcOrClassNode = t.functionExpression(null, funcParams, t.cloneDeep(funcBody)); let scopeInitialization = []; for (let [scopeName, scope] of scopeInstances) { scopeInitialization.push(t.variableDeclaration("var", [t.variableDeclarator(t.identifier(scopeName), t.numericLiteral(scope.id))])); scopeInitialization = scopeInitialization.concat(this.referentializer.getReferentializedScopeInitialization(scope)); } funcOrClassNode.body.body = scopeInitialization.concat(funcOrClassNode.body.body); (0, _babelTraverse2.default)(t.file(t.program([t.expressionStatement(funcOrClassNode)])), _visitors.ClosureRefReplacer, null, { residualFunctionBindings, modified, requireReturns: this.requireReturns, requireStatistics, getModuleIdIfNodeIsRequireFunction: this.modules.getGetModuleIdIfNodeIsRequireFunction(funcParams, [functionValue]), factoryFunctionInfos }); } let id = this.locationService.getLocation(functionValue); (0, _invariant2.default)(id !== undefined); if (functionValue.$Strict) { strictFunctionBodies.push(funcOrClassNode); } else { unstrictFunctionBodies.push(funcOrClassNode); } (0, _invariant2.default)(id !== undefined); (0, _invariant2.default)(funcOrClassNode !== undefined); defineFunction(instance, id, funcOrClassNode); } }; if (additionalFunctionNestedInstances.length > 0) naiveProcessInstances(additionalFunctionNestedInstances); if (!this._shouldUseFactoryFunction(funcBody, normalInstances)) { naiveProcessInstances(normalInstances); } else if (normalInstances.length > 0) { const functionUniqueTag = funcBody.uniqueOrderedTag; (0, _invariant2.default)(functionUniqueTag); const factoryInfo = factoryFunctionInfos.get(functionUniqueTag); (0, _invariant2.default)(factoryInfo); const { factoryId } = factoryInfo; // filter included variables to only include those that are different let factoryNames = []; let sameResidualBindings = new Map(); for (let name of unbound) { let isDifferent = false; let lastBinding; let firstBinding = normalInstances[0].residualFunctionBindings.get(name); (0, _invariant2.default)(firstBinding); if (firstBinding.modified) { // Must modify for traversal sameResidualBindings.set(name, firstBinding); continue; } for (let _ref of normalInstances) { let { residualFunctionBindings } = _ref; let residualBinding = residualFunctionBindings.get(name); (0, _invariant2.default)(residualBinding); (0, _invariant2.default)(!residualBinding.modified); if (!lastBinding) { lastBinding = residualBinding; } else if (!(0, _types.AreSameResidualBinding)(this.realm, residualBinding, lastBinding)) { isDifferent = true; break; } } if (isDifferent) { factoryNames.push(name); } else { (0, _invariant2.default)(lastBinding); sameResidualBindings.set(name, lastBinding); } } let factoryParams = []; for (let key of factoryNames) { factoryParams.push(t.identifier(key)); } let scopeInitialization = []; for (let [scopeName, scope] of normalInstances[0].scopeInstances) { factoryParams.push(t.identifier(scopeName)); scopeInitialization = scopeInitialization.concat(this.referentializer.getReferentializedScopeInitialization(scope)); } factoryParams = factoryParams.concat(params).slice(); // The Replacer below mutates the AST while the original AST may still be referenced // by another outer residual function so let's clone the original AST to avoid modifying it. let factoryNode = t.functionExpression(null, factoryParams, t.cloneDeep(funcBody)); if (normalInstances[0].functionValue.$Strict) { strictFunctionBodies.push(factoryNode); } else { unstrictFunctionBodies.push(factoryNode); } factoryNode.body.body = scopeInitialization.concat(factoryNode.body.body); // factory functions do not depend on any nested generator scope, so they go to the prelude let factoryDeclaration = t.variableDeclaration("var", [t.variableDeclarator(factoryId, factoryNode)]); this.prelude.push(factoryDeclaration); (0, _babelTraverse2.default)(t.file(t.program([t.expressionStatement(factoryNode)])), _visitors.ClosureRefReplacer, null, { residualFunctionBindings: sameResidualBindings, modified, requireReturns: this.requireReturns, requireStatistics, getModuleIdIfNodeIsRequireFunction: this.modules.getGetModuleIdIfNodeIsRequireFunction(factoryParams, normalInstances.map(instance => instance.functionValue)), factoryFunctionInfos }); for (let instance of normalInstances) { let { functionValue, residualFunctionBindings, insertionPoint } = instance; let functionId = this.locationService.getLocation(functionValue); (0, _invariant2.default)(functionId !== undefined); let hasFunctionArg = false; let flatArgs = factoryNames.map(name => { let residualBinding = residualFunctionBindings.get(name); (0, _invariant2.default)(residualBinding); let serializedValue = residualBinding.serializedValue; hasFunctionArg = hasFunctionArg || residualBinding.value && residualBinding.value instanceof _index.FunctionValue; (0, _invariant2.default)(serializedValue); return serializedValue; }); for (let entry of instance.scopeInstances) { flatArgs.push(t.numericLiteral(entry[1].id)); } let funcNode; let firstUsage = this.firstFunctionUsages.get(functionValue); (0, _invariant2.default)(insertionPoint !== undefined); if ( // The same free variables in shared instances may refer to objects with different initialization values // so a stub forward function is needed during delay initializations. this.residualFunctionInitializers.hasInitializerStatement(functionValue) || usesThis || hasFunctionArg || firstUsage !== undefined && !firstUsage.isNotEarlierThan(insertionPoint) || this.functionPrototypes.get(functionValue) !== undefined || this.simpleClosures) { let callArgs = [t.thisExpression()]; for (let flatArg of flatArgs) callArgs.push(flatArg); for (let param of params) { if (param.type !== "Identifier") { throw new _errors.FatalError("TODO: do not know how to deal with non-Identifier parameters"); } callArgs.push(param); } let callee = t.memberExpression(factoryId, t.identifier("call")); let childBody = t.blockStatement([t.returnStatement(t.callExpression(callee, callArgs))]); funcNode = t.functionExpression(null, params, childBody); if (functionValue.$Strict) { strictFunctionBodies.push(funcNode); } else { unstrictFunctionBodies.push(funcNode); } } else { funcNode = t.callExpression(t.memberExpression(factoryId, t.identifier("bind")), [_internalizer.nullExpression].concat(flatArgs)); } defineFunction(instance, functionId, funcNode); } } } for (let referentializationScope of this.referentializer.referentializationState.keys()) { let prelude = this.prelude; // Get the prelude for this additional function value if (referentializationScope !== "GLOBAL") { let additionalFunction = referentializationScope; prelude = (0, _utils.getOrDefault)(additionalFunctionPreludes, additionalFunction, () => []); } prelude.unshift(this.referentializer.createCaptureScopeAccessFunction(referentializationScope)); prelude.unshift(this.referentializer.createCapturedScopesArrayInitialization(referentializationScope)); } for (let instance of this.functionInstances.reverse()) { let functionBody = functionBodies.get(instance); if (functionBody !== undefined) { let insertionPoint = instance.insertionPoint; (0, _invariant2.default)(insertionPoint instanceof _types.BodyReference); // v8 seems to do something clever with array splicing, so this potentially // expensive operations seems to be actually cheap. Array.prototype.splice.apply(insertionPoint.body.entries, [insertionPoint.index, 0].concat(functionBody)); } } for (let [additionalFunction, body] of Array.from(rewrittenAdditionalFunctions.entries()).reverse()) { let additionalFunctionInfo = this.additionalFunctionValueInfos.get(additionalFunction); (0, _invariant2.default)(additionalFunctionInfo); let bodySegment = additionalFunctionModifiedBindingsSegment.get(additionalFunction); let funcBody = getFunctionBody(additionalFunctionInfo.instance); let prelude = additionalFunctionPreludes.get(additionalFunction); let insertionPoint = additionalFunctionInfo.instance.insertionPoint; (0, _invariant2.default)(insertionPoint); Array.prototype.splice.apply(insertionPoint.body.entries, [insertionPoint.index, 0].concat(funcBody)); if (prelude) body.unshift(...prelude); if (bodySegment) { if (body.length > 0 && additionalFunctionInfo.hasReturn) { let returnStatement = body.pop(); body.push(...bodySegment, returnStatement); } else { body.push(...bodySegment); } } } // Inject initializer code for indexed vars into functions (for delay initializations) for (let [functionValue, funcNode] of funcNodes) { let initializerStatement = this.residualFunctionInitializers.getInitializerStatement(functionValue); if (initializerStatement !== undefined) { (0, _invariant2.default)(t.isFunctionExpression(funcNode)); let blockStatement = funcNode.body; blockStatement.body.unshift(initializerStatement); } } return { unstrictFunctionBodies, strictFunctionBodies, requireStatistics }; } _getOrCreateClassNode(classPrototype) { if (!this.classes.has(classPrototype)) { let funcOrClassNode = t.classExpression(null, null, t.classBody([]), []); this.classes.set(classPrototype, funcOrClassNode); return funcOrClassNode; } else { let funcOrClassNode = this.classes.get(classPrototype); (0, _invariant2.default)(funcOrClassNode && t.isClassExpression(funcOrClassNode)); return funcOrClassNode; } } } exports.ResidualFunctions = ResidualFunctions; /** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ //# sourceMappingURL=ResidualFunctions.js.map