Skip to content

fix: optimize regressions from editions implementations #2066

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

Merged
merged 9 commits into from
May 8, 2025
40 changes: 39 additions & 1 deletion bench/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
// in between.

var newSuite = require("./suite"),
payload = require("./data/bench.json");
payload = require("./data/bench.json"),
protobuf = require("..");

var Buffer_from = Buffer.from !== Uint8Array.from && Buffer.from || function(value, encoding) { return new Buffer(value, encoding); };

Expand Down Expand Up @@ -88,3 +89,40 @@ newSuite("combined")
jspbCls.deserializeBinary(jspbMsg.serializeBinary());
})
.run();

var json = require("../tests/data/test.json");
newSuite("fromJSON")
.add("isolated", function() {
protobuf.Root.fromJSON(json);
})
.add("isolated (resolveAll)", function() {
protobuf.Root.fromJSON(json).resolveAll();
})
.add("shared (unique)", function() {
var root = protobuf.Root.fromJSON(json);
for (var i = 0; i < 1000; ++i) {
var jsonCopy = {
options: json.options,
nested: {}
};
// eslint-disable-next-line no-loop-func
Object.keys(json).forEach(key => {
jsonCopy.nested[key + i] = json[key];
});

protobuf.Root.fromJSON(jsonCopy, root);
}
}).run();

var resolveAllRoot = protobuf.Root.fromJSON(json);
newSuite("resolveAll")
.add("isolated", function() {
resolveAllRoot.resolveAll();
}).run();

newSuite("load")
.add("sync", function() {
protobuf.loadSync("bench/data/bench.proto");
})
.run();

6 changes: 6 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,9 @@ export abstract class NamespaceBase extends ReflectionObject {
/** Nested objects by name. */
public nested?: { [k: string]: ReflectionObject };

/** Whether or not objects contained in this namespace need feature resolution. */
protected _needsRecursiveFeatureResolution: boolean;

/** Nested objects of this namespace as an array for iteration. */
public readonly nestedArray: ReflectionObject[];

Expand Down Expand Up @@ -909,6 +912,9 @@ export abstract class ReflectionObject {
/** Resolved Features. */
public _features: object;

/** Whether or not features have been resolved. */
public _featuresResolved: boolean;

/** Parent namespace. */
public parent: (Namespace|null);

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
"build:bundle": "gulp --gulpfile scripts/gulpfile.js",
"build:types": "node cli/bin/pbts --main --global protobuf --out index.d.ts src/ lib/aspromise/index.js lib/base64/index.js lib/codegen/index.js lib/eventemitter/index.js lib/float/index.js lib/fetch/index.js lib/inquire/index.js lib/path/index.js lib/pool/index.js lib/utf8/index.js",
"changelog": "node scripts/changelog -w",
"coverage": "nyc tape -r ./lib/tape-adapter tests/*.js tests/node/*.js",
"coverage": "npm run coverage:test && npm run coverage:report",
"coverage:test": "nyc --silent tape -r ./lib/tape-adapter tests/*.js tests/node/*.js",
"coverage:report": "nyc report --reporter=lcov --reporter=text",
"docs": "jsdoc -c config/jsdoc.json -R README.md --verbose --pedantic",
"lint": "npm run lint:sources && npm run lint:types",
"lint:sources": "eslint \"**/*.js\" -c config/eslint.json",
Expand Down
12 changes: 9 additions & 3 deletions src/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,12 +370,18 @@ Field.prototype._inferLegacyProtoFeatures = function _inferLegacyProtoFeatures(e
}

var features = {};
this.resolve();

if (this.rule === "required") {
features.field_presence = "LEGACY_REQUIRED";
}
if (this.resolvedType instanceof Type && this.resolvedType.group) {
features.message_encoding = "DELIMITED";
if (this.parent && types.defaults[this.type] === undefined) {
// We can't use resolvedType because types may not have been resolved yet. However,
// legacy groups are always in the same scope as the field so we don't have to do a
// full scan of the tree.
var type = this.parent.get(this.type.split(".").pop());
if (type && type instanceof Type && type.group) {
features.message_encoding = "DELIMITED";
}
}
if (this.getOption("packed") === true) {
features.repeated_field_encoding = "PACKED";
Expand Down
80 changes: 68 additions & 12 deletions src/namespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,33 @@ function Namespace(name, options) {
* @private
*/
this._nestedArray = null;

/**
* Cache lookup calls for any objects contains anywhere under this namespace.
* This drastically speeds up resolve for large cross-linked protos where the same
* types are looked up repeatedly.
* @type {Object.<string,ReflectionObject|null>}
* @private
*/
this._lookupCache = {};

/**
* Whether or not objects contained in this namespace need feature resolution.
* @type {boolean}
* @protected
*/
this._needsRecursiveFeatureResolution = true;
}

function clearCache(namespace) {
namespace._nestedArray = null;
namespace._lookupCache = {};

// Also clear parent caches, since they include nested lookups.
var parent = namespace;
while(parent = parent.parent) {
parent._lookupCache = {};
}
return namespace;
}

Expand Down Expand Up @@ -249,6 +272,14 @@ Namespace.prototype.add = function add(object) {
}
}

this._needsRecursiveFeatureResolution = true;

// Also clear parent caches, since they need to recurse down.
var parent = this;
while(parent = parent.parent) {
parent._needsRecursiveFeatureResolution = true;
}

object.onAdd(this);
return clearCache(this);
};
Expand Down Expand Up @@ -324,6 +355,9 @@ Namespace.prototype.resolveAll = function resolveAll() {
* @override
*/
Namespace.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursive(edition) {
if (!this._needsRecursiveFeatureResolution) return this;
this._needsRecursiveFeatureResolution = false;

edition = this._edition || edition;

ReflectionObject.prototype._resolveFeaturesRecursive.call(this, edition);
Expand All @@ -341,7 +375,6 @@ Namespace.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursi
* @returns {ReflectionObject|null} Looked up object or `null` if none could be found
*/
Namespace.prototype.lookup = function lookup(path, filterTypes, parentAlreadyChecked) {

/* istanbul ignore next */
if (typeof filterTypes === "boolean") {
parentAlreadyChecked = filterTypes;
Expand All @@ -360,25 +393,48 @@ Namespace.prototype.lookup = function lookup(path, filterTypes, parentAlreadyChe
if (path[0] === "")
return this.root.lookup(path.slice(1), filterTypes);

var found = this._lookupImpl(path);
if (found && (!filterTypes || filterTypes.indexOf(found.constructor) > -1)) {
return found;
}

// If there hasn't been a match, try again at the parent
if (this.parent === null || parentAlreadyChecked)
return null;
return this.parent.lookup(path, filterTypes);
};

/**
* Internal helper for lookup that handles searching just at this namespace and below along with caching.
* @param {string[]} path Path to look up
* @returns {ReflectionObject|null} Looked up object or `null` if none could be found
* @private
*/
Namespace.prototype._lookupImpl = function lookup(path) {
var flatPath = path.join(".");
if(Object.prototype.hasOwnProperty.call(this._lookupCache, flatPath)) {
return this._lookupCache[flatPath];
}

// Test if the first part matches any nested object, and if so, traverse if path contains more
var found = this.get(path[0]);
var exact = null;
if (found) {
if (path.length === 1) {
if (!filterTypes || filterTypes.indexOf(found.constructor) > -1)
return found;
} else if (found instanceof Namespace && (found = found.lookup(path.slice(1), filterTypes, true)))
return found;
exact = found;
} else if (found instanceof Namespace && (found = found._lookupImpl(path.slice(1))))
exact = found;

// Otherwise try each nested namespace
} else
} else {
for (var i = 0; i < this.nestedArray.length; ++i)
if (this._nestedArray[i] instanceof Namespace && (found = this._nestedArray[i].lookup(path, filterTypes, true)))
return found;
if (this._nestedArray[i] instanceof Namespace && (found = this._nestedArray[i]._lookupImpl(path)))
exact = found;
}

// If there hasn't been a match, try again at the parent
if (this.parent === null || parentAlreadyChecked)
return null;
return this.parent.lookup(path, filterTypes);
// Set this even when null, so that when we walk up the tree we can quickly bail on repeated checks back down.
this._lookupCache[flatPath] = exact;
return exact;
};

/**
Expand Down
18 changes: 14 additions & 4 deletions src/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ function ReflectionObject(name, options) {
*/
this._features = {};

/**
* Whether or not features have been resolved.
* @type {boolean}
*/
this._featuresResolved = false;

/**
* Parent namespace.
* @type {Namespace|null}
Expand Down Expand Up @@ -172,10 +178,8 @@ ReflectionObject.prototype.onRemove = function onRemove(parent) {
ReflectionObject.prototype.resolve = function resolve() {
if (this.resolved)
return this;
if (this instanceof Root) {
this._resolveFeaturesRecursive(this._edition);
this.resolved = true;
}
if (this.root instanceof Root)
this.resolved = true; // only if part of a root
return this;
};

Expand All @@ -194,6 +198,10 @@ ReflectionObject.prototype._resolveFeaturesRecursive = function _resolveFeatures
* @returns {undefined}
*/
ReflectionObject.prototype._resolveFeatures = function _resolveFeatures(edition) {
if (this._featuresResolved) {
return;
}

var defaults = {};

/* istanbul ignore if */
Expand All @@ -217,6 +225,7 @@ ReflectionObject.prototype._resolveFeatures = function _resolveFeatures(edition)
throw new Error("Unknown edition: " + edition);
}
this._features = Object.assign(defaults, protoFeatures || {});
this._featuresResolved = true;
return;
}

Expand All @@ -238,6 +247,7 @@ ReflectionObject.prototype._resolveFeatures = function _resolveFeatures(edition)
// Sister fields should have the same features as their extensions.
this.extensionField._features = this._features;
}
this._featuresResolved = true;
};

/**
Expand Down
11 changes: 6 additions & 5 deletions src/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Root.fromJSON = function fromJSON(json, root) {
root = new Root();
if (json.options)
root.setOptions(json.options);
return root.addJSON(json.nested).resolveAll();
return root.addJSON(json.nested)._resolveFeaturesRecursive();
};

/**
Expand Down Expand Up @@ -99,6 +99,9 @@ Root.prototype.load = function load(filename, options, callback) {

// Finishes loading by calling the callback (exactly once)
function finish(err, root) {
if (root) {
root._resolveFeaturesRecursive();
}
/* istanbul ignore if */
if (!callback) {
return;
Expand All @@ -108,9 +111,6 @@ Root.prototype.load = function load(filename, options, callback) {
}
var cb = callback;
callback = null;
if (root) {
root.resolveAll();
}
cb(err, root);
}

Expand Down Expand Up @@ -218,8 +218,8 @@ Root.prototype.load = function load(filename, options, callback) {
for (var i = 0, resolved; i < filename.length; ++i)
if (resolved = self.resolvePath("", filename[i]))
fetch(resolved);
self.resolveAll();
if (sync) {
self._resolveFeaturesRecursive();
return self;
}
if (!queued) {
Expand Down Expand Up @@ -272,6 +272,7 @@ Root.prototype.resolveAll = function resolveAll() {
throw Error("unresolvable extensions: " + this.deferred.map(function(field) {
return "'extend " + field.extend + "' in " + field.parent.fullName;
}).join(", "));
this._resolveFeaturesRecursive(this._edition);
return Namespace.prototype.resolveAll.call(this);
};

Expand Down
2 changes: 2 additions & 0 deletions src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ Service.prototype.resolveAll = function resolveAll() {
* @override
*/
Service.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursive(edition) {
if (!this._needsRecursiveFeatureResolution) return this;

edition = this._edition || edition;

Namespace.prototype._resolveFeaturesRecursive.call(this, edition);
Expand Down
2 changes: 2 additions & 0 deletions src/type.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ Type.prototype.resolveAll = function resolveAll() {
* @override
*/
Type.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursive(edition) {
if (!this._needsRecursiveFeatureResolution) return this;

edition = this._edition || edition;

Namespace.prototype._resolveFeaturesRecursive.call(this, edition);
Expand Down