perf: Optimize proxify and stringDistance (#1098)

- Fill 2D array with ints upfront to reduce property access cost
- Change from recursive to simpler iterative (DP) solution
- Add cap parameter to stringDistanceCapped to limit computation
- Make candidate generation use a simple loop to avoid allocating unnecessary arrays and to call stringDistance only once on each pair of strings instead of every time in the sort callback

This improves chai perf by about 13% on @bmeurer's https://github.com/v8/web-tooling-benchmark.
This commit is contained in:
Sophie Alpert 2018-01-07 12:55:11 -08:00 committed by Keith Cirkel
parent f54f71c234
commit c02f64b160

View file

@ -49,19 +49,31 @@ module.exports = function proxify(obj, nonChainableMethodName) {
nonChainableMethodName + '".'); nonChainableMethodName + '".');
} }
var orderedProperties = getProperties(target).filter(function(property) { // If the property is reasonably close to an existing Chai property,
return !Object.prototype.hasOwnProperty(property) && // suggest that property to the user. Only suggest properties with a
builtins.indexOf(property) === -1; // distance less than 4.
}).sort(function(a, b) { var suggestion = null;
return stringDistance(property, a) - stringDistance(property, b); var suggestionDistance = 4;
getProperties(target).forEach(function(prop) {
if (
!Object.prototype.hasOwnProperty(prop) &&
builtins.indexOf(prop) === -1
) {
var dist = stringDistanceCapped(
property,
prop,
suggestionDistance
);
if (dist < suggestionDistance) {
suggestion = prop;
suggestionDistance = dist;
}
}
}); });
if (orderedProperties.length && if (suggestion !== null) {
stringDistance(orderedProperties[0], property) < 4) {
// If the property is reasonably close to an existing Chai property,
// suggest that property to the user.
throw Error('Invalid Chai property: ' + property + throw Error('Invalid Chai property: ' + property +
'. Did you mean "' + orderedProperties[0] + '"?'); '. Did you mean "' + suggestion + '"?');
} else { } else {
throw Error('Invalid Chai property: ' + property); throw Error('Invalid Chai property: ' + property);
} }
@ -89,36 +101,44 @@ module.exports = function proxify(obj, nonChainableMethodName) {
}; };
/** /**
* # stringDistance(strA, strB) * # stringDistanceCapped(strA, strB, cap)
* Return the Levenshtein distance between two strings. * Return the Levenshtein distance between two strings, but no more than cap.
* @param {string} strA * @param {string} strA
* @param {string} strB * @param {string} strB
* @return {number} the string distance between strA and strB * @param {number} number
* @return {number} min(string distance between strA and strB, cap)
* @api private * @api private
*/ */
function stringDistance(strA, strB, memo) { function stringDistanceCapped(strA, strB, cap) {
if (!memo) { if (Math.abs(strA.length - strB.length) >= cap) {
// `memo` is a two-dimensional array containing a cache of distances return cap;
// memo[i][j] is the distance between strA.slice(0, i) and
// strB.slice(0, j).
memo = [];
for (var i = 0; i <= strA.length; i++) {
memo[i] = [];
}
} }
if (!memo[strA.length] || !memo[strA.length][strB.length]) { var memo = [];
if (strA.length === 0 || strB.length === 0) { // `memo` is a two-dimensional array containing distances.
memo[strA.length][strB.length] = Math.max(strA.length, strB.length); // memo[i][j] is the distance between strA.slice(0, i) and
} else { // strB.slice(0, j).
var sliceA = strA.slice(0, -1); for (var i = 0; i <= strA.length; i++) {
var sliceB = strB.slice(0, -1); memo[i] = Array(strB.length + 1).fill(0);
memo[strA.length][strB.length] = Math.min( memo[i][0] = i;
stringDistance(sliceA, strB, memo) + 1, }
stringDistance(strA, sliceB, memo) + 1, for (var j = 0; j < strB.length; j++) {
stringDistance(sliceA, sliceB, memo) + memo[0][j] = j;
(strA.slice(-1) === strB.slice(-1) ? 0 : 1) }
for (var i = 1; i <= strA.length; i++) {
var ch = strA.charCodeAt(i - 1);
for (var j = 1; j <= strB.length; j++) {
if (Math.abs(i - j) >= cap) {
memo[i][j] = cap;
continue;
}
memo[i][j] = Math.min(
memo[i - 1][j] + 1,
memo[i][j - 1] + 1,
memo[i - 1][j - 1] +
(ch === strB.charCodeAt(j - 1) ? 0 : 1)
); );
} }
} }