Optionally remove optimized module factory functions (#2602)

Summary:
When module factory functions optimized by Prepack are reachable from the global code, they are residualized, even though there is an implicit contract that they will not be used. This PR implements an option to remove them. This is a short-term fix, and may be refined in the future and generalized to be more flexible.

Test cases coming up.
Pull Request resolved: https://github.com/facebook/prepack/pull/2602

Differential Revision: D10402515

Pulled By: sb98052

fbshipit-source-id: 7ad3a84a754e4347774e0e95df197c2348f9d332
This commit is contained in:
Sapan Bhatia 2018-10-16 08:01:12 -07:00 committed by Facebook Github Bot
parent 06736f0df5
commit 2954b937be
9 changed files with 182 additions and 0 deletions

View File

@ -375,11 +375,15 @@ function runTest(name, code, options: PrepackOptions, args) {
serialize: true,
uniqueSuffix: "",
arrayNestedOptimizedFunctionsEnabled: false,
removeModuleFactoryFunctions: false,
modulesToInitialize,
}): any): PrepackOptions); // Since PrepackOptions is an exact type I have to cast
if (code.includes("// arrayNestedOptimizedFunctionsEnabled")) {
options.arrayNestedOptimizedFunctionsEnabled = true;
}
if (code.includes("// removeModuleFactoryFunctions")) {
options.removeModuleFactoryFunctions = true;
}
if (code.includes("// throws introspection error")) {
try {
let realmOptions = {

View File

@ -61,6 +61,7 @@ export type RealmOptions = {
abstractValueImpliesMax?: number,
arrayNestedOptimizedFunctionsEnabled?: boolean,
reactFailOnUnsupportedSideEffects?: boolean,
removeModuleFactoryFunctions?: boolean,
};
export type SerializerOptions = {

View File

@ -71,6 +71,7 @@ function run(
--statsFile The name of the output file where statistics will be written to.
--heapGraphFilePath The name of the output file where heap graph will be written to.
--dumpIRFilePath The name of the output file where the intermediate representation will be written to.
--removeModuleFactoryFunctions Forces optimized module factory functions to be removed, even if they are reachable.
--inlineExpressions When generating code, tells prepack to avoid naming expressions when they are only used once,
and instead inline them where they are used.
--invariantLevel 0: no invariants (default); 1: checks for abstract values; 2: checks for accessed built-ins; 3: internal consistency
@ -123,6 +124,7 @@ function run(
debugNames: false,
emitConcreteModel: false,
inlineExpressions: false,
removeModuleFactoryFunctions: false,
logStatistics: false,
logModules: false,
delayInitializations: false,

View File

@ -45,6 +45,7 @@ export type PrepackOptions = {|
serialize?: boolean,
check?: Array<number>,
inlineExpressions?: boolean,
removeModuleFactoryFunctions?: boolean,
sourceMaps?: boolean,
modulesToInitialize?: Set<string | number> | "ALL",
statsFile?: string,
@ -90,6 +91,7 @@ export function getRealmOptions({
debugReproArgs,
arrayNestedOptimizedFunctionsEnabled,
reactFailOnUnsupportedSideEffects,
removeModuleFactoryFunctions,
}: PrepackOptions): RealmOptions {
return {
compatibility,
@ -116,6 +118,7 @@ export function getRealmOptions({
debugReproArgs,
arrayNestedOptimizedFunctionsEnabled,
reactFailOnUnsupportedSideEffects,
removeModuleFactoryFunctions,
};
}

View File

@ -292,6 +292,7 @@ export class Realm {
}
this.collectedNestedOptimizedFunctionEffects = new Map();
this.moduleFactoryFunctionsToRemove = new Map();
this.tracers = [];
// These get initialized in construct_realm to avoid the dependency
@ -354,6 +355,7 @@ export class Realm {
this.optimizedFunctions = new Map();
this.arrayNestedOptimizedFunctionsEnabled =
opts.arrayNestedOptimizedFunctionsEnabled || opts.instantRender || false;
this.removeModuleFactoryFunctions = opts.removeModuleFactoryFunctions || false;
}
statistics: RealmStatistics;
@ -467,6 +469,8 @@ export class Realm {
simplifyAndRefineAbstractCondition: AbstractValue => Value;
collectedNestedOptimizedFunctionEffects: Map<ECMAScriptSourceFunctionValue, Effects>;
removeModuleFactoryFunctions: boolean;
moduleFactoryFunctionsToRemove: Map<number, string>;
tracers: Array<Tracer>;
MOBILE_JSC_VERSION = "jsc-600-1-4-17";

View File

@ -99,17 +99,20 @@ export class ResidualFunctionInstantiator<
T: BabelNodeClassMethod | BabelNodeFunctionExpression | BabelNodeArrowFunctionExpression
> {
factoryFunctionInfos: Map<number, FactoryFunctionInfo>;
factoryFunctionsToRemove: Map<number, string>;
identifierReplacements: Map<BabelNodeIdentifier, Replacement>;
callReplacements: Map<BabelNodeCallExpression, Replacement>;
root: T;
constructor(
factoryFunctionInfos: Map<number, FactoryFunctionInfo>,
factoryFunctionsToRemove: Map<number, string>,
identifierReplacements: Map<BabelNodeIdentifier, Replacement>,
callReplacements: Map<BabelNodeCallExpression, Replacement>,
root: T
) {
this.factoryFunctionInfos = factoryFunctionInfos;
this.factoryFunctionsToRemove = factoryFunctionsToRemove;
this.identifierReplacements = identifierReplacements;
this.callReplacements = callReplacements;
this.root = root;
@ -203,6 +206,16 @@ export class ResidualFunctionInstantiator<
const { factoryId } = duplicateFunctionInfo;
return t.callExpression(t.memberExpression(factoryId, t.identifier("bind")), [nullExpression]);
}
if (this.factoryFunctionsToRemove.has(functionTag)) {
let newFunctionExpression = Object.assign({}, node);
newFunctionExpression.body = t.blockStatement([
t.throwStatement(
t.newExpression(t.identifier("Error"), [t.stringLiteral("Function was specialized out by Prepack")])
),
]);
return newFunctionExpression;
}
}
}

View File

@ -500,6 +500,7 @@ export class ResidualFunctions {
let methodParams = params.slice();
let classMethod = new ResidualFunctionInstantiator(
factoryFunctionInfos,
this.realm.moduleFactoryFunctionsToRemove,
this._getIdentifierReplacements(funcBody, residualFunctionBindings),
this._getCallReplacements(funcBody),
t.classMethod(
@ -531,6 +532,7 @@ export class ResidualFunctions {
let isLexical = instance.functionValue.$ThisMode === "lexical";
funcOrClassNode = new ResidualFunctionInstantiator(
factoryFunctionInfos,
this.realm.moduleFactoryFunctionsToRemove,
this._getIdentifierReplacements(funcBody, residualFunctionBindings),
this._getCallReplacements(funcBody),
this._createFunctionExpression(params, funcBody, isLexical)
@ -623,6 +625,7 @@ export class ResidualFunctions {
factoryParams = factoryParams.concat(params).slice();
let factoryNode = new ResidualFunctionInstantiator(
factoryFunctionInfos,
this.realm.moduleFactoryFunctionsToRemove,
this._getIdentifierReplacements(funcBody, sameResidualBindings),
this._getCallReplacements(funcBody),
this._createFunctionExpression(factoryParams, funcBody, false)

View File

@ -13,10 +13,12 @@ import { GlobalEnvironmentRecord, DeclarativeEnvironmentRecord } from "../enviro
import { FatalError } from "../errors.js";
import { Realm, Tracer } from "../realm.js";
import type { Effects } from "../realm.js";
import type { FunctionBodyAstNode } from "../types.js";
import { Get } from "../methods/index.js";
import { Environment } from "../singletons.js";
import {
Value,
BoundFunctionValue,
FunctionValue,
ObjectValue,
NumberValue,
@ -39,6 +41,7 @@ import invariant from "../invariant.js";
import { Logger } from "./logger.js";
import { SerializerStatistics } from "../serializer/statistics.js";
import { PropertyDescriptor } from "../descriptors.js";
import ECMAScriptSourceFunctionValue from "../../lib/values/ECMAScriptSourceFunctionValue.js";
function downgradeErrorsToWarnings(realm: Realm, f: () => any) {
let savedHandler = realm.errorHandler;
@ -191,6 +194,18 @@ export class ModuleTracer extends Tracer {
argumentsList[2],
"Third argument to define function is present but not a concrete array."
);
// Remove if explicitly marked at optimization time
let realm = factoryFunction.$Realm;
if (realm.removeModuleFactoryFunctions) {
let targetFunction = factoryFunction;
if (factoryFunction instanceof BoundFunctionValue) targetFunction = factoryFunction.$BoundTargetFunction;
invariant(targetFunction instanceof ECMAScriptSourceFunctionValue);
let body = ((targetFunction.$ECMAScriptCode: any): FunctionBodyAstNode);
let uniqueOrderedTag = body.uniqueOrderedTag;
invariant(uniqueOrderedTag !== undefined);
realm.moduleFactoryFunctionsToRemove.set(uniqueOrderedTag, "" + moduleId.value);
}
} else
this.modules.logger.logError(factoryFunction, "First argument to define function is not a function value.");
} else
@ -246,6 +261,11 @@ export class Modules {
}
}
}
let moduleFactoryFunctionsToRemove = this.realm.moduleFactoryFunctionsToRemove;
for (let [functionId, moduleIdOfFunction] of this.realm.moduleFactoryFunctionsToRemove) {
if (!this.initializedModules.has(moduleIdOfFunction)) moduleFactoryFunctionsToRemove.delete(functionId);
}
this.getStatistics().initializedModules = this.initializedModules.size;
this.getStatistics().totalModules = this.moduleIds.size;
}

View File

@ -0,0 +1,132 @@
// es6
// removeModuleFactoryFunctions
// does not contain:require(0)
// does not contain:magic-string-1
// does contain:magic-string-2
var modules = Object.create(null);
__d = define;
function require(moduleId) {
var moduleIdReallyIsNumber = moduleId;
var module = modules[moduleIdReallyIsNumber];
return module && module.isInitialized ? module.exports : guardedLoadModule(moduleIdReallyIsNumber, module);
}
function define(factory, moduleId, dependencyMap) {
if (moduleId in modules) {
return;
}
modules[moduleId] = {
dependencyMap: dependencyMap,
exports: undefined,
factory: factory,
hasError: false,
isInitialized: false,
};
var _verboseName = arguments[3];
if (_verboseName) {
modules[moduleId].verboseName = _verboseName;
global.verboseNamesToModuleIds[_verboseName] = moduleId;
}
}
var inGuard = false;
function guardedLoadModule(moduleId, module) {
if (!inGuard && global.ErrorUtils) {
inGuard = true;
var returnValue = void 0;
try {
returnValue = loadModuleImplementation(moduleId, module);
} catch (e) {
global.ErrorUtils.reportFatalError(e);
}
inGuard = false;
return returnValue;
} else {
return loadModuleImplementation(moduleId, module);
}
}
function loadModuleImplementation(moduleId, module) {
var nativeRequire = global.nativeRequire;
if (!module && nativeRequire) {
nativeRequire(moduleId);
module = modules[moduleId];
}
if (!module) {
throw unknownModuleError(moduleId);
}
if (module.hasError) {
throw moduleThrewError(moduleId);
}
module.isInitialized = true;
var exports = (module.exports = {});
var _module = module,
factory = _module.factory,
dependencyMap = _module.dependencyMap;
try {
var _moduleObject = { exports: exports };
factory(global, require, _moduleObject, exports, dependencyMap);
module.factory = undefined;
return (module.exports = _moduleObject.exports);
} catch (e) {
module.hasError = true;
module.isInitialized = false;
module.exports = undefined;
throw e;
}
}
function unknownModuleError(id) {
var message = 'Requiring unknown module "' + id + '".';
return Error(message);
}
function moduleThrewError(id) {
return Error('Requiring module "' + id + '", which threw an exception.');
}
// === End require code ===
function defineModules() {
define(function(global, require, module, exports) {
let z = "magic-string-1";
module.exports = { foo: " hello " };
}, 0, null);
define(function(global, require, module, exports) {
var x = require(0);
var y = require(2);
let z = "magic-string-2";
module.exports = {
bar: " goodbye",
foo2: x.foo,
baz: y.baz,
};
}, 1, null);
define(function(global, require, module, exports) {
module.exports = { baz: " foo " };
}, 2, null);
}
defineModules();
var x = require(0);
function f() {
return x.foo === " hello " && modules[1].exports === undefined && require(1).bar === " goodbye";
}
inspect = function() {
// the require( 0) should be entirely eliminated from 1's factory function
// but the require(2) will remain
return f();
};