不要怂,就是干,撸起袖子干!

Commit 7e2c06eb by Mick Hansen

perf(query): optimize groupJoinData to use a single loop

1 parent 1c43cceb
Showing with 282 additions and 61 deletions
...@@ -338,92 +338,307 @@ module.exports = (function() { ...@@ -338,92 +338,307 @@ module.exports = (function() {
] ]
*/ */
// includeOptions are 'level'-specific where options is a general directive /*
var i = 0; * Assumptions
var groupJoinData = function(data, includeOptions, options) { * ID is not necessarily the first field
var results = [] * All fields for a level is grouped in the same set (i.e. Panel.id, Task.id, Panel.title is not possible)
, existingResult * Parent keys will be seen before any include/child keys
, calleeData * Previous set won't necessarily be parent set (one parent could have two children, one child would then be previous set for the other)
, child */
, calleeDataIgnore = ['__children']
, parseChildren = function(result) { var groupJoinData = function(rows, includeOptions, options) {
_.each(result.__children, function (children, key) { if (!rows.length) {
result[key] = groupJoinData(children, (includeOptions.includeMap && includeOptions.includeMap[key]), options) return [];
}) }
delete result.__children
}, var
primaryKeyAttribute, // Generic looping
primaryKeyMap = {} i
, length
, $i
, $length
// Row specific looping
, rowsI
, rowsLength = rows.length
, row
// Key specific looping
, keys
, key
, keyI
, keyLength
, prevKey
, values
, topValues
, topExists
, previous
, checkExisting = options.checkExisting
// If we don't have to deduplicate we can pre-allocate the resulting array
, results = checkExisting ? [] : new Array(rowsLength)
, resultMap = {}
, includeMap = {}
, itemHash
, parentHash
, topHash
// Result variables for the respective functions
, $keyPrefix
, $keyPrefixString
, $prevKeyPrefixString
, $prevKeyPrefix
, $lastKeyPrefix
, $current
, $parent
// Map each key to an include option
, previousPiece
, buildIncludeMap = function (piece) {
if ($current.includeMap[piece]) {
includeMap[key] = $current = $current.includeMap[piece];
if (previousPiece) {
previousPiece = previousPiece+'.'+piece;
} else {
previousPiece = piece;
}
includeMap[previousPiece] = $current;
}
}
// Calcuate the last item in the array prefix ('Results' for 'User.Results.id')
, lastKeyPrefixMemo = {}
, lastKeyPrefix = function (key) {
if (!lastKeyPrefixMemo[key]) {
var prefix = keyPrefix(key)
, length = prefix.length;
// Ignore all include keys on main data lastKeyPrefixMemo[key] = !length ? '' : prefix[length - 1];
if (includeOptions.includeNames) { }
calleeDataIgnore = calleeDataIgnore.concat(includeOptions.includeNames) return lastKeyPrefixMemo[key];
}
// Calculate the string prefix of a key ('User.Results' for 'User.Results.id')
, keyPrefixStringMemo = {}
, keyPrefixString = function (key, memo) {
if (!memo[key]) {
memo[key] = key.substr(0, key.lastIndexOf('.'));
}
return memo[key];
}
// Removes the prefix from a key ('id' for 'User.Results.id')
, removeKeyPrefixMemo = {}
, removeKeyPrefix = function (key) {
if (!removeKeyPrefixMemo[key]) {
var index = key.lastIndexOf('.');
removeKeyPrefixMemo[key] = key.substr(index === -1 ? 0 : index + 1);
}
return removeKeyPrefixMemo[key];
}
// Calculates the array prefix of a key (['User', 'Results'] for 'User.Results.id')
, keyPrefixMemo = {}
, keyPrefix = function (key) {
// We use a double memo and keyPrefixString so that different keys with the same prefix will receive the same array instead of differnet arrays with equal values
if (!keyPrefixMemo[key]) {
var prefixString = keyPrefixString(key, keyPrefixStringMemo);
if (!keyPrefixMemo[prefixString]) {
keyPrefixMemo[prefixString] = prefixString ? prefixString.split(".") : [];
}
keyPrefixMemo[key] = keyPrefixMemo[prefixString];
}
return keyPrefixMemo[key];
} }
if (includeOptions.model.primaryKeyAttributes.length === 1) { , primaryKeyAttributes
primaryKeyAttribute = includeOptions.model.primaryKeyAttribute , prefix;
for (rowsI = 0; rowsI < rowsLength; rowsI++) {
row = rows[rowsI]
// Keys are the same for all rows, so only need to compute them on the first row
if (rowsI === 0) {
keys = Object.keys(row)
keyLength = keys.length
} }
data.forEach(function parseRow(row) { if (checkExisting) {
row = Dot.transform(row) topExists = false;
calleeData = row
// If there are :M associations included we need to see if the main result of the row has already been identified // Compute top level hash key (this is usually just the primary key values)
if (options.checkExisting) { $length = includeOptions.model.primaryKeyAttributes.length;
if (primaryKeyAttribute) { if ($length === 1) {
// If we can, detect equality on the singular primary key topHash = row[includeOptions.model.primaryKeyAttributes[0]];
existingResult = primaryKeyMap[calleeData[primaryKeyAttribute]]
} else { } else {
// If we can't identify on a singular primary key, do a full row equality check topHash = '';
existingResult = _.find(results, function (result) { for ($i = 0; $i < $length; $i++) {
return Utils._.isEqual(_.omit(result, calleeDataIgnore), calleeData) topHash += row[includeOptions.model.primaryKeyAttributes[$i]];
}) }
} }
}
topValues = values = {}
$prevKeyPrefix = undefined
for (keyI = 0; keyI < keyLength; keyI++) {
key = keys[keyI]
// The string prefix isn't actualy needed
// We use it so keyPrefix for different keys will resolve to the same array if they have the same prefix
// TODO: Find a better way?
$keyPrefixString = keyPrefixString(key, keyPrefixStringMemo);
$keyPrefix = keyPrefix(key);
// On the first row we compute the includeMap
if (rowsI === 0 && includeMap[key] === undefined) {
if (!$keyPrefix.length) {
includeMap[key] = includeMap[''] = includeOptions;
} else { } else {
existingResult = null $current = includeOptions;
previousPiece = undefined
$keyPrefix.forEach(buildIncludeMap);
}
}
// End of key set
if ($prevKeyPrefix !== undefined && $prevKeyPrefix !== $keyPrefix) {
if (checkExisting) {
// Compute hash key for this set instance
// TODO: Optimize
length = $prevKeyPrefix.length;
$parent = null;
parentHash = null;
if (length) {
for (i = 0; i < length; i++) {
prefix = $parent ? $parent+'.'+$prevKeyPrefix[i] : $prevKeyPrefix[i];
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
$length = primaryKeyAttributes.length;
if ($length === 1) {
itemHash = prefix+row[prefix+'.'+primaryKeyAttributes[0]];
} else {
itemHash = prefix;
for ($i = 0; $i < $length; $i++) {
itemHash += row[prefix+'.'+primaryKeyAttributes[$i]];
}
}
if (!parentHash) {
parentHash = topHash;
} }
if (!existingResult) { itemHash = parentHash + itemHash;
results.push(existingResult = calleeData) $parent = prefix;
if (options.checkExisting && primaryKeyAttribute) { if (i < length - 1) {
primaryKeyMap[existingResult[primaryKeyAttribute]] = existingResult parentHash = itemHash;
} }
} }
} else {
itemHash = topHash;
}
for (var attrName in row) { if (itemHash === topHash) {
if (row.hasOwnProperty(attrName)) { if (!resultMap[itemHash]) {
// Child if object, and is an child include resultMap[itemHash] = values;
child = Object(row[attrName]) === row[attrName] && includeOptions.includeMap && includeOptions.includeMap[attrName] } else {
topExists = true;
}
} else {
if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
//console.log($parent, prevKey, $lastKeyPrefix);
if (includeMap[prevKey].association.isSingleAssociation) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
} else {
if (!$parent[$lastKeyPrefix]) {
$parent[$lastKeyPrefix] = [];
}
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
}
}
}
if (child) { // Reset values
// Make sure nested object is available values = {};
if (!existingResult.__children) { } else {
existingResult.__children = {} // If checkExisting is false it's because there's only 1:1 associations in this query
// However we still need to map onto the appropriate parent
// For 1:1 we map forward, initializing the value object on the parent to be filled in the next iterations of the loop
$current = topValues;
length = $keyPrefix.length;
if (length) {
for (i = 0; i < length; i++) {
if (i === length -1) {
values = $current[$keyPrefix[i]] = {};
}
$current = $current[$keyPrefix[i]];
} }
if (!existingResult.__children[attrName]) { }
existingResult.__children[attrName] = [] }
}
// End of iteration, set value and set prev values (for next iteration)
values[removeKeyPrefix(key)] = row[key];
prevKey = key
$prevKeyPrefix = $keyPrefix
$prevKeyPrefixString = $keyPrefixString;
} }
existingResult.__children[attrName].push(row[attrName]) if (checkExisting) {
length = $prevKeyPrefix.length;
$parent = null;
parentHash = null;
// Remove from main if (length) {
delete existingResult[attrName] for (i = 0; i < length; i++) {
prefix = $parent ? $parent+'.'+$prevKeyPrefix[i] : $prevKeyPrefix[i];
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
$length = primaryKeyAttributes.length;
if ($length === 1) {
itemHash = prefix+row[prefix+'.'+primaryKeyAttributes[0]];
} else {
itemHash = prefix;
for ($i = 0; $i < $length; $i++) {
itemHash += row[prefix+'.'+primaryKeyAttributes[$i]];
} }
} }
if (!parentHash) {
parentHash = topHash;
} }
// parseChildren in same loop if no duplicate values are possible itemHash = parentHash + itemHash;
if (!options.checkExisting) { $parent = prefix;
parseChildren(existingResult) if (i < length - 1) {
parentHash = itemHash;
}
}
} else {
itemHash = topHash;
} }
})
// parseChildren after row parsing if duplicate values are possible if (itemHash === topHash) {
if (options.checkExisting) { if (!resultMap[itemHash]) {
results.forEach(parseChildren) resultMap[itemHash] = values;
} else {
topExists = true;
}
} else {
if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
//console.log($parent, prevKey, $lastKeyPrefix);
if (includeMap[prevKey].association.isSingleAssociation) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
} else {
if (!$parent[$lastKeyPrefix]) {
$parent[$lastKeyPrefix] = [];
}
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
}
}
} }
return results if (!topExists) {
results.push(topValues);
}
} else {
results[rowsI] = topValues;
} }
//break;
}
return results
};
AbstractQuery.$groupJoinData = groupJoinData; AbstractQuery.$groupJoinData = groupJoinData;
......
...@@ -361,7 +361,7 @@ module.exports = (function() { ...@@ -361,7 +361,7 @@ module.exports = (function() {
, accessor = Utils._.camelize(key) , accessor = Utils._.camelize(key)
, childOptions , childOptions
, primaryKeyAttribute = include.model.primaryKeyAttribute , primaryKeyAttribute = include.model.primaryKeyAttribute
, isEmpty = value[0] && value[0][primaryKeyAttribute] === null , isEmpty
if (!isEmpty) { if (!isEmpty) {
childOptions = { childOptions = {
...@@ -380,9 +380,15 @@ module.exports = (function() { ...@@ -380,9 +380,15 @@ module.exports = (function() {
accessor = accessor.slice(0,1).toLowerCase() + accessor.slice(1) accessor = accessor.slice(0,1).toLowerCase() + accessor.slice(1)
if (association.isSingleAssociation) { if (association.isSingleAssociation) {
if (Array.isArray(value)) {
value = value[0];
}
isEmpty = value && value[primaryKeyAttribute] === null
accessor = Utils.singularize(accessor, self.Model.options.language) accessor = Utils.singularize(accessor, self.Model.options.language)
self[accessor] = self.dataValues[accessor] = isEmpty ? null : include.model.build(value[0], childOptions) self[accessor] = self.dataValues[accessor] = isEmpty ? null : include.model.build(value, childOptions)
} else { } else {
isEmpty = value[0] && value[0][primaryKeyAttribute] === null
self[accessor] = self.dataValues[accessor] = isEmpty ? [] : include.model.bulkBuild(value, childOptions) self[accessor] = self.dataValues[accessor] = isEmpty ? [] : include.model.bulkBuild(value, childOptions)
} }
} }
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!