Bug 1391405: Part 3a - Speed up schema normalization for choices types. r=zombie

For choices types, when one choice fails, we don't need the original error
string, since another choice may succeed, and we generate the final error
based on all of the options. Nevertheless, we spend a lot of time generating
JSON strings for the failed inputs in those cases, which adds up to about 12%
of the remaining overhead at this point.

MozReview-Commit-ID: 6nXBAv2W20V

--HG--
extra : rebase_source : 5894bc4b9e8d64ac9505f27240ea4fabfcb5f02f
extra : histedit_source : 0e8b5e0315abd672a57a60420453a1e0681c9df6
This commit is contained in:
Kris Maglione 2017-08-18 11:49:13 -07:00
parent d727073568
commit 7325526f5a

View File

@ -283,6 +283,15 @@ const CONTEXT_FOR_INJECTION = [
"shouldInject",
];
// If the message is a function, call it and return the result.
// Otherwise, assume it's a string.
function forceString(msg) {
if (typeof msg === "function") {
return msg();
}
return msg;
}
/**
* A context for schema validation and error reporting. This class is only used
* internally within Schemas.
@ -392,9 +401,11 @@ class Context {
* If the context has a `currentTarget` value, this is prepended to
* the message to indicate the location of the error.
*
* @param {string} errorMessage
* @param {string|function} errorMessage
* The error message which will be displayed when this is the
* only possible matching schema.
* only possible matching schema. If a function is passed, it
* will be evaluated when the error string is first needed, and
* must return a string.
* @param {string} choicesMessage
* The message describing the valid what constitutes a valid
* value for this schema, which will be displayed when multiple
@ -416,7 +427,8 @@ class Context {
}
if (this.currentTarget) {
return {error: `Error processing ${this.currentTarget}: ${errorMessage}`};
let {currentTarget} = this;
return {error: () => `Error processing ${currentTarget}: ${forceString(errorMessage)}`};
}
return {error: errorMessage};
}
@ -433,7 +445,7 @@ class Context {
* @returns {Error}
*/
makeError(message) {
let {error} = this.error(message);
let error = forceString(this.error(message).error);
if (this.cloneScope) {
return new this.cloneScope.Error(error);
}
@ -1177,7 +1189,7 @@ class Type extends Entry {
choice = `be a ${type} value`;
}
return context.error(`Expected ${type} instead of ${JSON.stringify(value)}`,
return context.error(() => `Expected ${type} instead of ${JSON.stringify(value)}`,
choice);
}
}
@ -1245,9 +1257,9 @@ class ChoiceType extends Type {
let message;
if (typeof value === "object") {
message = `Value must either: ${choices.join(", ")}`;
message = () => `Value must either: ${choices.join(", ")}`;
} else {
message = `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`;
message = () => `Value ${JSON.stringify(value)} must either: ${choices.join(", ")}`;
}
return context.error(message, null);
@ -1373,21 +1385,21 @@ class StringType extends Type {
let choices = this.enumeration.map(JSON.stringify).join(", ");
return context.error(`Invalid enumeration value ${JSON.stringify(value)}`,
return context.error(() => `Invalid enumeration value ${JSON.stringify(value)}`,
`be one of [${choices}]`);
}
if (value.length < this.minLength) {
return context.error(`String ${JSON.stringify(value)} is too short (must be ${this.minLength})`,
return context.error(() => `String ${JSON.stringify(value)} is too short (must be ${this.minLength})`,
`be longer than ${this.minLength}`);
}
if (value.length > this.maxLength) {
return context.error(`String ${JSON.stringify(value)} is too long (must be ${this.maxLength})`,
return context.error(() => `String ${JSON.stringify(value)} is too long (must be ${this.maxLength})`,
`be shorter than ${this.maxLength}`);
}
if (this.pattern && !this.pattern.test(value)) {
return context.error(`String ${JSON.stringify(value)} must match ${this.pattern}`,
return context.error(() => `String ${JSON.stringify(value)} must match ${this.pattern}`,
`match the pattern ${this.pattern.toSource()}`);
}
@ -1588,7 +1600,7 @@ class ObjectType extends Type {
if (error) {
if (onError == "warn") {
context.logError(error.error);
context.logError(forceString(error.error));
} else if (onError != "ignore") {
throw error;
}
@ -1813,7 +1825,7 @@ class ArrayType extends Type {
element = context.withPath(String(i), () => this.itemType.normalize(element, context));
if (element.error) {
if (this.onError == "warn") {
context.logError(element.error);
context.logError(forceString(element.error));
} else if (this.onError != "ignore") {
return element;
}
@ -1967,7 +1979,7 @@ class TypeProperty extends Entry {
let setStub = (value) => {
let normalized = this.type.normalize(value, context);
if (normalized.error) {
this.throwError(context, normalized.error);
this.throwError(context, forceString(normalized.error));
}
apiImpl.setProperty(normalized.value);
@ -2135,7 +2147,7 @@ class CallEntry extends Entry {
let parameter = this.parameters[parameterIndex];
let r = parameter.type.normalize(arg, context);
if (r.error) {
this.throwError(context, `Type error for parameter ${parameter.name} (${r.error})`);
this.throwError(context, `Type error for parameter ${parameter.name} (${forceString(r.error)})`);
}
return r.value;
});
@ -2190,7 +2202,7 @@ FunctionEntry = class FunctionEntry extends CallEntry {
}
const {error} = type.normalize(value, context);
if (error) {
this.throwError(context, `Type error for ${name} value (${error})`);
this.throwError(context, `Type error for ${name} value (${forceString(error)})`);
}
}
@ -2886,6 +2898,10 @@ this.Schemas = {
let ns = this.getNamespace(namespaceName);
let type = ns.get(prop);
return type.normalize(obj, new Context(context));
let result = type.normalize(obj, new Context(context));
if (result.error) {
return {error: forceString(result.error)};
}
return result;
},
};