Optimization pass

This commit is contained in:
Wyatt Johnson
2017-02-15 17:12:17 -07:00
parent 6c141cbce0
commit 9eefc0299f
2 changed files with 133 additions and 105 deletions
+123 -105
View File
@@ -1,13 +1,104 @@
const _ = require('lodash');
const DataLoader = require('dataloader');
const util = require('./util');
const objectCacheKeyFn = util.objectCacheKeyFn;
const arrayCacheKeyFn = util.arrayCacheKeyFn;
const CommentModel = require('../../models/comment');
const AssetModel = require('../../models/asset');
const ActionModel = require('../../models/action');
const getMetrics = (context, {from, to, sort, limit}) => {
const getMetrics = ({loaders: {Metrics, Assets}}, {from, to, sort, limit}) => {
let commentMetrics = {};
let assetMetrics = [];
return Metrics.getRecentActions.load({from, to})
.then((actionSummaries) => {
commentMetrics = actionSummaries.reduce((acc, {item_id, action_type, count}) => {
if (!(item_id in acc)) {
acc[item_id] = [];
}
acc[item_id].push({action_type, count});
return acc;
}, {});
// Collect just the comment id's.
let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id));
// Find those comments.
return Metrics.getSpecificComments.load(commentIDs);
})
.then((commentResults) => {
assetMetrics = commentResults.map(({ids, asset_id}) => {
let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type');
let action_summaries = Object.keys(summaries).map((action_type) => ({
action_type,
actionCount: summaries[action_type].reduce((acc, {count}) => acc + count, 0),
actionableItemCount: summaries[action_type].length
}));
return {action_summaries, id: asset_id};
});
// Sort these metrics by the predefined sort order. This will ensure that
// if the action summary does not exist on the object, that it is less
// prefered over the one that does have it.
assetMetrics.sort((a, b) => {
let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort));
let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort));
// If either a or b don't have this action type, then one of them will
// automatically win.
if (aActionSummary == null || bActionSummary == null) {
if (bActionSummary != null) {
return 1;
}
if (aActionSummary != null) {
return -1;
}
return 0;
}
// Both of them had an actionCount, hence we can determine that we could
// compare the actual values directly.
return bActionSummary.actionCount - aActionSummary.actionCount;
});
// Only keep the top `limit`.
assetMetrics = assetMetrics.slice(0, limit);
// Determine the assets that we need to return.
return Assets.getByID.loadMany(assetMetrics.map((asset) => asset.id));
})
.then((assets) => {
// Join up the assets that are returned by their id.
let groupedAssets = _.groupBy(assets, 'id');
// Return from the sorted asset metrics and return their assetes.
return assetMetrics.map(({id, action_summaries}) => {
if (id in groupedAssets) {
let asset = groupedAssets[id][0];
// Add the action summaries to the asset.
asset.action_summaries = action_summaries;
return asset;
}
return null;
}).filter((asset) => asset != null);
});
};
const getRecentActions = (context, {from, to}) => {
return ActionModel.aggregate([
// Find all actions that were created in the time range.
@@ -36,120 +127,47 @@ const getMetrics = (context, {from, to, sort, limit}) => {
action_type: '$_id.action_type',
count: '$count'
}}
]).then((actionSummaries) => {
]);
};
// Collect all the action summaries into a dictionary.
actionSummaries.forEach(({item_id, action_type, count}) => {
if (!(item_id in commentMetrics)) {
commentMetrics[item_id] = [];
}
const getSpecificComments = (context, ids) => {
return CommentModel.aggregate([
commentMetrics[item_id].push({action_type, count});
});
// Collect just the comment id's.
let commentIDs = _.uniq(actionSummaries.map((as) => as.item_id));
// Find those comments.
return CommentModel.aggregate([
// Get only those comments.
{$match: {
id: {
$in: commentIDs
}
}},
// Group by their asset id and push in the comment id.
{$group: {
_id: {
asset_id: '$asset_id'
},
ids: {
$addToSet: '$id'
}
}},
// Project that data only as better fields.
{$project: {
asset_id: '$_id.asset_id',
ids: '$ids'
}}
]);
})
.then((commentResults) => {
assetMetrics = commentResults.map(({ids, asset_id}) => {
let summaries = _.groupBy(_.flatten(ids.map((id) => commentMetrics[id])), 'action_type');
let action_summaries = Object.keys(summaries).map((action_type) => ({
action_type,
actionCount: summaries[action_type].reduce((acc, {count}) => acc + count, 0),
actionableItemCount: summaries[action_type].length
}));
return {action_summaries, id: asset_id};
});
// Sort these metrics by the predefined sort order. This will ensure that
// if the action summary does not exist on the object, that it is less
// prefered over the one that does have it.
assetMetrics.sort((a, b) => {
let aActionSummary = a.action_summaries.find((({action_type}) => action_type === sort));
let bActionSummary = b.action_summaries.find((({action_type}) => action_type === sort));
// If either a or b don't have this action type, then one of them will
// automatically win.
if (aActionSummary == null || bActionSummary == null) {
if (bActionSummary != null) {
return 1;
}
if (aActionSummary != null) {
return -1;
}
return 0;
}
// Both of them had an actionCount, hence we can determine that we could
// compare the actual values directly.
return bActionSummary.actionCount - aActionSummary.actionCount;
});
// Only keep the top `limit`.
assetMetrics = assetMetrics.slice(0, limit);
// Determine the assets that we need to return.
return AssetModel.find({
// Get only those comments.
{$match: {
id: {
$in: assetMetrics.map((asset) => asset.id)
$in: ids
}
});
})
.then((assets) => {
}},
// Join up the assets that are returned by their id.
let groupedAssets = _.groupBy(assets, 'id');
// Return from the sorted asset metrics and return their assetes.
return assetMetrics.map(({id, action_summaries}) => {
if (id in groupedAssets) {
let asset = groupedAssets[id][0];
// Add the action summaries to the asset.
asset.action_summaries = action_summaries;
return asset;
// Group by their asset id and push in the comment id.
{$group: {
_id: {
asset_id: '$asset_id'
},
ids: {
$addToSet: '$id'
}
}},
return null;
}).filter((asset) => asset != null);
});
// Project that data only as better fields.
{$project: {
asset_id: '$_id.asset_id',
ids: '$ids'
}}
]);
};
module.exports = (context) => ({
Metrics: {
getSpecificComments: new DataLoader(([ids]) => getSpecificComments(context, ids).then((c) => [c]), {
batch: false,
cacheKeyFn: arrayCacheKeyFn
}),
getRecentActions: new DataLoader(([{from, to}]) => getRecentActions(context, {from, to}).then((as) => [as]), {
batch: false,
cacheKeyFn: objectCacheKeyFn('from', 'to')
}),
get: ({from, to, sort, limit}) => getMetrics(context, {from, to, sort, limit})
}
});
+10
View File
@@ -130,10 +130,20 @@ const objectCacheKeyFn = (...paths) => (obj) => {
return paths.map((path) => obj[path]).join(':');
};
/**
* Maps an object's paths to a string that can be used as a cache key.
* @param {Array} paths paths on the object to be used to generate the cache
* key
*/
const arrayCacheKeyFn = (arr) => {
return arr.sort().join(':');
};
module.exports = {
singleJoinBy,
arrayJoinBy,
objectCacheKeyFn,
arrayCacheKeyFn,
SingletonResolver,
SharedCacheDataLoader
};