Skip to content

Commit 6406d4c

Browse files
fix: optimize regressions from editions implementations (#2066)
* Add benchmarks covering resolveAll * Improve shared benchmark * Rework feature resolution to not rely on resolved types * Fix lint warning * Cache feature resolution state to avoid potential O(n^2) work. Features should never change * Restore caching of object resolution that was accidentally removed * Add caching to namespace lookup to drastically improve speed during resolveAll (30x in benchmarks) * Fix lint issues * Cache feature resolution at the namespace level to avoid any O(n^2) scaling
1 parent 56782bf commit 6406d4c

File tree

9 files changed

+149
-26
lines changed

9 files changed

+149
-26
lines changed

bench/index.js

+39-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
// in between.
1111

1212
var newSuite = require("./suite"),
13-
payload = require("./data/bench.json");
13+
payload = require("./data/bench.json"),
14+
protobuf = require("..");
1415

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

@@ -88,3 +89,40 @@ newSuite("combined")
8889
jspbCls.deserializeBinary(jspbMsg.serializeBinary());
8990
})
9091
.run();
92+
93+
var json = require("../tests/data/test.json");
94+
newSuite("fromJSON")
95+
.add("isolated", function() {
96+
protobuf.Root.fromJSON(json);
97+
})
98+
.add("isolated (resolveAll)", function() {
99+
protobuf.Root.fromJSON(json).resolveAll();
100+
})
101+
.add("shared (unique)", function() {
102+
var root = protobuf.Root.fromJSON(json);
103+
for (var i = 0; i < 1000; ++i) {
104+
var jsonCopy = {
105+
options: json.options,
106+
nested: {}
107+
};
108+
// eslint-disable-next-line no-loop-func
109+
Object.keys(json).forEach(key => {
110+
jsonCopy.nested[key + i] = json[key];
111+
});
112+
113+
protobuf.Root.fromJSON(jsonCopy, root);
114+
}
115+
}).run();
116+
117+
var resolveAllRoot = protobuf.Root.fromJSON(json);
118+
newSuite("resolveAll")
119+
.add("isolated", function() {
120+
resolveAllRoot.resolveAll();
121+
}).run();
122+
123+
newSuite("load")
124+
.add("sync", function() {
125+
protobuf.loadSync("bench/data/bench.proto");
126+
})
127+
.run();
128+

index.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,9 @@ export abstract class NamespaceBase extends ReflectionObject {
750750
/** Nested objects by name. */
751751
public nested?: { [k: string]: ReflectionObject };
752752

753+
/** Whether or not objects contained in this namespace need feature resolution. */
754+
protected _needsRecursiveFeatureResolution: boolean;
755+
753756
/** Nested objects of this namespace as an array for iteration. */
754757
public readonly nestedArray: ReflectionObject[];
755758

@@ -909,6 +912,9 @@ export abstract class ReflectionObject {
909912
/** Resolved Features. */
910913
public _features: object;
911914

915+
/** Whether or not features have been resolved. */
916+
public _featuresResolved: boolean;
917+
912918
/** Parent namespace. */
913919
public parent: (Namespace|null);
914920

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
"build:bundle": "gulp --gulpfile scripts/gulpfile.js",
3434
"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",
3535
"changelog": "node scripts/changelog -w",
36-
"coverage": "nyc tape -r ./lib/tape-adapter tests/*.js tests/node/*.js",
36+
"coverage": "npm run coverage:test && npm run coverage:report",
37+
"coverage:test": "nyc --silent tape -r ./lib/tape-adapter tests/*.js tests/node/*.js",
38+
"coverage:report": "nyc report --reporter=lcov --reporter=text",
3739
"docs": "jsdoc -c config/jsdoc.json -R README.md --verbose --pedantic",
3840
"lint": "npm run lint:sources && npm run lint:types",
3941
"lint:sources": "eslint \"**/*.js\" -c config/eslint.json",

src/field.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -370,12 +370,18 @@ Field.prototype._inferLegacyProtoFeatures = function _inferLegacyProtoFeatures(e
370370
}
371371

372372
var features = {};
373-
this.resolve();
373+
374374
if (this.rule === "required") {
375375
features.field_presence = "LEGACY_REQUIRED";
376376
}
377-
if (this.resolvedType instanceof Type && this.resolvedType.group) {
378-
features.message_encoding = "DELIMITED";
377+
if (this.parent && types.defaults[this.type] === undefined) {
378+
// We can't use resolvedType because types may not have been resolved yet. However,
379+
// legacy groups are always in the same scope as the field so we don't have to do a
380+
// full scan of the tree.
381+
var type = this.parent.get(this.type.split(".").pop());
382+
if (type && type instanceof Type && type.group) {
383+
features.message_encoding = "DELIMITED";
384+
}
379385
}
380386
if (this.getOption("packed") === true) {
381387
features.repeated_field_encoding = "PACKED";

src/namespace.js

+68-12
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,33 @@ function Namespace(name, options) {
108108
* @private
109109
*/
110110
this._nestedArray = null;
111+
112+
/**
113+
* Cache lookup calls for any objects contains anywhere under this namespace.
114+
* This drastically speeds up resolve for large cross-linked protos where the same
115+
* types are looked up repeatedly.
116+
* @type {Object.<string,ReflectionObject|null>}
117+
* @private
118+
*/
119+
this._lookupCache = {};
120+
121+
/**
122+
* Whether or not objects contained in this namespace need feature resolution.
123+
* @type {boolean}
124+
* @protected
125+
*/
126+
this._needsRecursiveFeatureResolution = true;
111127
}
112128

113129
function clearCache(namespace) {
114130
namespace._nestedArray = null;
131+
namespace._lookupCache = {};
132+
133+
// Also clear parent caches, since they include nested lookups.
134+
var parent = namespace;
135+
while(parent = parent.parent) {
136+
parent._lookupCache = {};
137+
}
115138
return namespace;
116139
}
117140

@@ -249,6 +272,14 @@ Namespace.prototype.add = function add(object) {
249272
}
250273
}
251274

275+
this._needsRecursiveFeatureResolution = true;
276+
277+
// Also clear parent caches, since they need to recurse down.
278+
var parent = this;
279+
while(parent = parent.parent) {
280+
parent._needsRecursiveFeatureResolution = true;
281+
}
282+
252283
object.onAdd(this);
253284
return clearCache(this);
254285
};
@@ -324,6 +355,9 @@ Namespace.prototype.resolveAll = function resolveAll() {
324355
* @override
325356
*/
326357
Namespace.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursive(edition) {
358+
if (!this._needsRecursiveFeatureResolution) return this;
359+
this._needsRecursiveFeatureResolution = false;
360+
327361
edition = this._edition || edition;
328362

329363
ReflectionObject.prototype._resolveFeaturesRecursive.call(this, edition);
@@ -341,7 +375,6 @@ Namespace.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursi
341375
* @returns {ReflectionObject|null} Looked up object or `null` if none could be found
342376
*/
343377
Namespace.prototype.lookup = function lookup(path, filterTypes, parentAlreadyChecked) {
344-
345378
/* istanbul ignore next */
346379
if (typeof filterTypes === "boolean") {
347380
parentAlreadyChecked = filterTypes;
@@ -360,25 +393,48 @@ Namespace.prototype.lookup = function lookup(path, filterTypes, parentAlreadyChe
360393
if (path[0] === "")
361394
return this.root.lookup(path.slice(1), filterTypes);
362395

396+
var found = this._lookupImpl(path);
397+
if (found && (!filterTypes || filterTypes.indexOf(found.constructor) > -1)) {
398+
return found;
399+
}
400+
401+
// If there hasn't been a match, try again at the parent
402+
if (this.parent === null || parentAlreadyChecked)
403+
return null;
404+
return this.parent.lookup(path, filterTypes);
405+
};
406+
407+
/**
408+
* Internal helper for lookup that handles searching just at this namespace and below along with caching.
409+
* @param {string[]} path Path to look up
410+
* @returns {ReflectionObject|null} Looked up object or `null` if none could be found
411+
* @private
412+
*/
413+
Namespace.prototype._lookupImpl = function lookup(path) {
414+
var flatPath = path.join(".");
415+
if(Object.prototype.hasOwnProperty.call(this._lookupCache, flatPath)) {
416+
return this._lookupCache[flatPath];
417+
}
418+
363419
// Test if the first part matches any nested object, and if so, traverse if path contains more
364420
var found = this.get(path[0]);
421+
var exact = null;
365422
if (found) {
366423
if (path.length === 1) {
367-
if (!filterTypes || filterTypes.indexOf(found.constructor) > -1)
368-
return found;
369-
} else if (found instanceof Namespace && (found = found.lookup(path.slice(1), filterTypes, true)))
370-
return found;
424+
exact = found;
425+
} else if (found instanceof Namespace && (found = found._lookupImpl(path.slice(1))))
426+
exact = found;
371427

372428
// Otherwise try each nested namespace
373-
} else
429+
} else {
374430
for (var i = 0; i < this.nestedArray.length; ++i)
375-
if (this._nestedArray[i] instanceof Namespace && (found = this._nestedArray[i].lookup(path, filterTypes, true)))
376-
return found;
431+
if (this._nestedArray[i] instanceof Namespace && (found = this._nestedArray[i]._lookupImpl(path)))
432+
exact = found;
433+
}
377434

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

384440
/**

src/object.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ function ReflectionObject(name, options) {
6767
*/
6868
this._features = {};
6969

70+
/**
71+
* Whether or not features have been resolved.
72+
* @type {boolean}
73+
*/
74+
this._featuresResolved = false;
75+
7076
/**
7177
* Parent namespace.
7278
* @type {Namespace|null}
@@ -172,10 +178,8 @@ ReflectionObject.prototype.onRemove = function onRemove(parent) {
172178
ReflectionObject.prototype.resolve = function resolve() {
173179
if (this.resolved)
174180
return this;
175-
if (this instanceof Root) {
176-
this._resolveFeaturesRecursive(this._edition);
177-
this.resolved = true;
178-
}
181+
if (this.root instanceof Root)
182+
this.resolved = true; // only if part of a root
179183
return this;
180184
};
181185

@@ -194,6 +198,10 @@ ReflectionObject.prototype._resolveFeaturesRecursive = function _resolveFeatures
194198
* @returns {undefined}
195199
*/
196200
ReflectionObject.prototype._resolveFeatures = function _resolveFeatures(edition) {
201+
if (this._featuresResolved) {
202+
return;
203+
}
204+
197205
var defaults = {};
198206

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

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

243253
/**

src/root.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Root.fromJSON = function fromJSON(json, root) {
5151
root = new Root();
5252
if (json.options)
5353
root.setOptions(json.options);
54-
return root.addJSON(json.nested).resolveAll();
54+
return root.addJSON(json.nested)._resolveFeaturesRecursive();
5555
};
5656

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

100100
// Finishes loading by calling the callback (exactly once)
101101
function finish(err, root) {
102+
if (root) {
103+
root._resolveFeaturesRecursive();
104+
}
102105
/* istanbul ignore if */
103106
if (!callback) {
104107
return;
@@ -108,9 +111,6 @@ Root.prototype.load = function load(filename, options, callback) {
108111
}
109112
var cb = callback;
110113
callback = null;
111-
if (root) {
112-
root.resolveAll();
113-
}
114114
cb(err, root);
115115
}
116116

@@ -218,8 +218,8 @@ Root.prototype.load = function load(filename, options, callback) {
218218
for (var i = 0, resolved; i < filename.length; ++i)
219219
if (resolved = self.resolvePath("", filename[i]))
220220
fetch(resolved);
221-
self.resolveAll();
222221
if (sync) {
222+
self._resolveFeaturesRecursive();
223223
return self;
224224
}
225225
if (!queued) {
@@ -272,6 +272,7 @@ Root.prototype.resolveAll = function resolveAll() {
272272
throw Error("unresolvable extensions: " + this.deferred.map(function(field) {
273273
return "'extend " + field.extend + "' in " + field.parent.fullName;
274274
}).join(", "));
275+
this._resolveFeaturesRecursive(this._edition);
275276
return Namespace.prototype.resolveAll.call(this);
276277
};
277278

src/service.js

+2
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ Service.prototype.resolveAll = function resolveAll() {
121121
* @override
122122
*/
123123
Service.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursive(edition) {
124+
if (!this._needsRecursiveFeatureResolution) return this;
125+
124126
edition = this._edition || edition;
125127

126128
Namespace.prototype._resolveFeaturesRecursive.call(this, edition);

src/type.js

+2
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ Type.prototype.resolveAll = function resolveAll() {
317317
* @override
318318
*/
319319
Type.prototype._resolveFeaturesRecursive = function _resolveFeaturesRecursive(edition) {
320+
if (!this._needsRecursiveFeatureResolution) return this;
321+
320322
edition = this._edition || edition;
321323

322324
Namespace.prototype._resolveFeaturesRecursive.call(this, edition);

0 commit comments

Comments
 (0)