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

Optimizations #246

Merged
merged 6 commits into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 116 additions & 90 deletions lib/runtime/entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ function Entry(id) {
// The normalized namespace object that importers receive when they use
// `import * as namespace from "..."` syntax.
this.namespace = utils.createNamespace();
// Map from local names to snapshots of the corresponding local values, used
// to determine when local values have changed and need to be re-broadcast.
this.snapshots = Object.create(null);
}

var Ep = utils.setPrototypeOf(Entry.prototype, null);
Expand Down Expand Up @@ -205,20 +208,46 @@ function syncExportsToNamespace(entry, names) {
// Called whenever module.exports might have changed, to trigger any
// setters associated with the newly exported values. The names parameter
// is optional; without it, all getters and setters will run.
Ep.runSetters = function (names) {
// If the '*' setter needs to be run, but not the '*' getter (names includes
// all exports/getters that changed), the runNsSetter option can be enabled.
Ep.runSetters = function (names, runNsSetter) {
// Make sure entry.namespace and module.exports are up to date before we
// call getExportByName(entry, name).
this.runGetters(names);

if (runNsSetter && names !== void 0) {
names.push('*');
}

// Lazily-initialized object mapping parent module identifiers to parent
// module objects whose setters we might need to run.
var parents;
var parentNames;

forEachSetter(this, names, function (setter, name, value) {
if (parents === void 0) {
parents = Object.create(null);
}
parents[setter.parent.id] = setter.parent;

if (parentNames === void 0) {
parentNames = Object.create(null);
}

var parentId = setter.parent.id;

// When setters use the shorthand for re-exporting values, we know
// which exports in the parent module were modified, and can do less work
// when running the parent setters.
// parentNames[parentId] is set to false if there are any setters that we do
// not know which exports they modify
if (setter.exportAs !== void 0 && parentNames[parentId] !== false) {
parentNames[parentId] = parentNames[parentId] || [];
parentNames[parentId].push(setter.exportAs);
} else if (parentNames[parentId] !== false) {
parentNames[parentId] = false;
}

parents[parentId] = setter.parent;

// The param order for setters is `value` then `name` because the `name`
// param is only used by namespace exports.
Expand All @@ -243,119 +272,116 @@ Ep.runSetters = function (names) {
var parent = parents[parentIDs[i]];
var parentEntry = entryMap[parent.id];
if (parentEntry) {
parentEntry.runSetters();
parentEntry.runSetters(
parentNames[parentIDs[i]] || void 0,
!!parentNames[parentIDs[i]]
);
}
}
};

function callSetterIfNecessary(setter, name, value, callback) {
if (name === "__esModule") {
// Ignore setters asking for module.exports.__esModule.
return;
}

var shouldCall = false;

if (setter.last === void 0) {
setter.last = Object.create(null);
// Always call the setter if it has never been called before.
shouldCall = true;
}

function changed(name, value) {
var valueToCompare = value;
if (valueToCompare !== valueToCompare) {
valueToCompare = NAN;
} else if (valueToCompare === void 0) {
valueToCompare = UNDEFINED;
}

if (setter.last[name] === valueToCompare) {
return false;
}

setter.last[name] = valueToCompare;
return true;
}
function updateSnapshot(entry, name, newValue) {
var newSnapshot = Object.create(null);
var newKeys = [];

if (name === "*") {
var keys = safeKeys(value);
var keyCount = keys.length;
for (var i = 0; i < keyCount; ++i) {
var key = keys[i];
safeKeys(newValue).forEach(keyOfValue => {
// Evaluating value[key] is risky because the property might be
// defined by a getter function that logs a deprecation warning (or
// worse) when evaluated. For example, Node uses this trick to
// display a deprecation warning whenever crypto.createCredentials
// is accessed. Fortunately, when value[key] is defined by a getter
// worse) when evaluated. For example, Node uses this trick to display
// a deprecation warning whenever crypto.createCredentials is
// accessed. Fortunately, when value[key] is defined by a getter
// function, it's enough to check whether the getter function itself
// has changed, since we are careful elsewhere to preserve getters
// rather than prematurely evaluating them.
if (changed(key, utils.valueOrGetter(value, key))) {
shouldCall = true;
}
}
} else if (changed(name, value)) {
shouldCall = true;
newKeys.push(keyOfValue);
newSnapshot[keyOfValue] = normalizeSnapshotValue(
utils.valueOrGetter(newValue, keyOfValue)
);
});
} else {
newKeys.push(name);
newSnapshot[name] = normalizeSnapshotValue(newValue);
}

if (shouldCall) {
// Only invoke the callback if we have not called this setter
// (with a value of this name) before, or the current value is
// different from the last value we passed to this setter.
return callback(setter, name, value);
var oldSnapshot = entry.snapshots[name];
if (
oldSnapshot &&
newKeys.every(key => oldSnapshot[key] === newSnapshot[key]) &&
newKeys.length === Object.keys(oldSnapshot).length
) {
return oldSnapshot;
}

return entry.snapshots[name] = newSnapshot;
}

function normalizeSnapshotValue(value) {
if (value === void 0) return UNDEFINED;
if (value !== value && isNaN(value)) return NAN;
return value;
}

// Invoke the given callback once for every (setter, name, value) that needs to
// be called. Note that forEachSetter does not call any setters itself, only the
// given callback.
function forEachSetter(entry, names, callback) {
var needToCheckNames = true;

if (names === void 0) {
names = Object.keys(entry.setters);
needToCheckNames = false;
}

var nameCount = names.length;

for (var i = 0; i < nameCount; ++i) {
var name = names[i];

if (needToCheckNames &&
! hasOwn.call(entry.setters, name)) {
continue;
}

var setters = entry.setters[name];
var keys = Object.keys(setters);
var keyCount = keys.length;

for (var j = 0; j < keyCount; ++j) {
var key = keys[j];
var value = getExportByName(entry, name);

callSetterIfNecessary(setters[key], name, value, callback);

var getter = entry.getters[name];
if (typeof getter === "function" &&
// Sometimes a getter function will throw because it's called
// before the variable it's supposed to return has been
// initialized, so we need to know that the getter function has
// run to completion at least once.
getter.runCount > 0 &&
getter.constant) {
// If we happen to know that this getter function has run
// successfully, and will never return a different value, then we
// can forget the corresponding setter, because we've already
// reported that constant value. Note that we can't forget the
// getter, because we need to remember the original value in case
// anyone tampers with entry.module.exports[name].
delete setters[key];
names.forEach(name => {
// Ignore setters asking for module.exports.__esModule.
if (name === "__esModule") return;

var settersByKey = entry.setters[name];
if (!settersByKey) return;

var getter = entry.getters[name];
var alreadyCalledConstantGetter =
typeof getter === "function" &&
// Sometimes a getter function will throw because it's called
// before the variable it's supposed to return has been
// initialized, so we need to know that the getter function has
// run to completion at least once.
getter.runCount > 0 &&
getter.constant;

var value = getExportByName(entry, name);

// Although we may have multiple setter functions with different keys in
// settersByKey, we can compute a snapshot of value and check it against
// entry.snapshots[name] before iterating over the individual setter
// functions, which is convenient because then all the setter.snapshot
// properties will end up referring to the same snapshot object.
var snapshot = updateSnapshot(entry, name, value);

Object.keys(settersByKey).forEach(key => {
var setter = settersByKey[key];

// If value has not changed since the last time we broadcast it, then
// snapshot === entry.snapshots[name], so there's a good chance we can
// skip most/all of the setters that already have setter.snapshot ===
// snapshot. If value has changed, snapshot !== entry.snapshots[name], and
// we need to broadcast the new value to every setter.
if (setter.snapshot !== snapshot) {
setter.snapshot = snapshot;

// Invoke the setter function with the updated value.
callback(setter, name, value);

if (alreadyCalledConstantGetter) {
// If we happen to know this getter function has run successfully
// (getter.runCount > 0), and will never return a different value
// (getter.constant), then we can forget the corresponding setter,
// because we've already reported that constant value. Note that we
// can't forget the getter, because we need to remember the original
// value in case anyone tampers with entry.module.exports[name].
delete settersByKey[key];
}
}
}
}
});
});
}

function getExportByName(entry, name) {
Expand Down
8 changes: 7 additions & 1 deletion lib/runtime/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function moduleExportDefault(value) {
function moduleExportAs(name) {
var entry = this;
var includeDefault = name === "*+";
return function (value) {
var setter = function (value) {
if (name === "*" || name === "*+") {
Object.keys(value).forEach(function (key) {
if (includeDefault || key !== "default") {
Expand All @@ -105,6 +105,12 @@ function moduleExportAs(name) {
entry.exports[name] = value;
}
};

if (name !== '*+' && name !== "*") {
setter.exportAs = name;
}

return setter;
}

// Platform-specific code should find a way to call this method whenever
Expand Down