Skip to content
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