mirror of
https://github.com/chaijs/chai
synced 2024-11-14 15:57:10 +00:00
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:
parent
f54f71c234
commit
c02f64b160
1 changed files with 53 additions and 33 deletions
|
@ -49,19 +49,31 @@ module.exports = function proxify(obj, nonChainableMethodName) {
|
|||
nonChainableMethodName + '".');
|
||||
}
|
||||
|
||||
var orderedProperties = getProperties(target).filter(function(property) {
|
||||
return !Object.prototype.hasOwnProperty(property) &&
|
||||
builtins.indexOf(property) === -1;
|
||||
}).sort(function(a, b) {
|
||||
return stringDistance(property, a) - stringDistance(property, b);
|
||||
// If the property is reasonably close to an existing Chai property,
|
||||
// suggest that property to the user. Only suggest properties with a
|
||||
// distance less than 4.
|
||||
var suggestion = null;
|
||||
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 &&
|
||||
stringDistance(orderedProperties[0], property) < 4) {
|
||||
// If the property is reasonably close to an existing Chai property,
|
||||
// suggest that property to the user.
|
||||
if (suggestion !== null) {
|
||||
throw Error('Invalid Chai property: ' + property +
|
||||
'. Did you mean "' + orderedProperties[0] + '"?');
|
||||
'. Did you mean "' + suggestion + '"?');
|
||||
} else {
|
||||
throw Error('Invalid Chai property: ' + property);
|
||||
}
|
||||
|
@ -89,36 +101,44 @@ module.exports = function proxify(obj, nonChainableMethodName) {
|
|||
};
|
||||
|
||||
/**
|
||||
* # stringDistance(strA, strB)
|
||||
* Return the Levenshtein distance between two strings.
|
||||
* # stringDistanceCapped(strA, strB, cap)
|
||||
* Return the Levenshtein distance between two strings, but no more than cap.
|
||||
* @param {string} strA
|
||||
* @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
|
||||
*/
|
||||
|
||||
function stringDistance(strA, strB, memo) {
|
||||
if (!memo) {
|
||||
// `memo` is a two-dimensional array containing a cache of distances
|
||||
// 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] = [];
|
||||
}
|
||||
function stringDistanceCapped(strA, strB, cap) {
|
||||
if (Math.abs(strA.length - strB.length) >= cap) {
|
||||
return cap;
|
||||
}
|
||||
|
||||
if (!memo[strA.length] || !memo[strA.length][strB.length]) {
|
||||
if (strA.length === 0 || strB.length === 0) {
|
||||
memo[strA.length][strB.length] = Math.max(strA.length, strB.length);
|
||||
} else {
|
||||
var sliceA = strA.slice(0, -1);
|
||||
var sliceB = strB.slice(0, -1);
|
||||
memo[strA.length][strB.length] = Math.min(
|
||||
stringDistance(sliceA, strB, memo) + 1,
|
||||
stringDistance(strA, sliceB, memo) + 1,
|
||||
stringDistance(sliceA, sliceB, memo) +
|
||||
(strA.slice(-1) === strB.slice(-1) ? 0 : 1)
|
||||
var memo = [];
|
||||
// `memo` is a two-dimensional array containing distances.
|
||||
// memo[i][j] is the distance between strA.slice(0, i) and
|
||||
// strB.slice(0, j).
|
||||
for (var i = 0; i <= strA.length; i++) {
|
||||
memo[i] = Array(strB.length + 1).fill(0);
|
||||
memo[i][0] = i;
|
||||
}
|
||||
for (var j = 0; j < strB.length; j++) {
|
||||
memo[0][j] = j;
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue