Skip to content

Commit

Permalink
perf(xod-project, xod-client): tweak memoized selectors to avoid exce…
Browse files Browse the repository at this point in the history
…ssive recalculations
  • Loading branch information
brusherru committed May 18, 2018
1 parent f5aea1c commit f996f66
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 10 deletions.
18 changes: 16 additions & 2 deletions packages/xod-client/src/project/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { Maybe } from 'ramda-fantasy';
import { createSelector } from 'reselect';

import * as XP from 'xod-project';
import { foldMaybe, foldEither, explodeMaybe } from 'xod-func-tools';
import {
foldMaybe,
foldEither,
explodeMaybe,
compareMapsBy,
maybeEqualsWith,
} from 'xod-func-tools';
import { createIndexFromPatches } from 'xod-patch-search';

import {
Expand Down Expand Up @@ -70,9 +76,17 @@ export const getCurrentPatch = createSelector(
(patchPath, project) => R.chain(XP.getPatchByPath(R.__, project), patchPath)
);

const projectHasSamePatchList = R.either(
(a, b) => a === b,
compareMapsBy(R.pipe(XP.listPatches, R.indexBy(XP.getPatchPath)))
);

export const getDeducedPinTypes = createMemoizedSelector(
[getProject, getCurrentPatch],
[R.equals, R.equals],
[
projectHasSamePatchList,
maybeEqualsWith(R.complement(XP.couldTypeDeductionChange)),
],
(project, maybeCurrentPatch) =>
foldMaybe({}, patch => XP.deducePinTypes(patch, project), maybeCurrentPatch)
);
Expand Down
23 changes: 22 additions & 1 deletion packages/xod-client/src/projectBrowser/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,30 @@ const getLocalPatchesList = createSelector(
XP.listLocalPatches
);

const patchListChangesNotAffectProjectBrowserLook = R.either(
XP.patchListEqualsBy(R.complement(XP.didUtilityOrDeprecatedMarkersChange)),
XP.patchListEqualsBy(R.complement(XP.couldValidityChange))
);

const projectChangesNotAffectProjectBrowserLook = R.either(
(prev, next) => prev === next,
(prev, next) => {
const prevLibPatches = XP.listLibraryPatches(prev);
const nextLibPatches = XP.listLibraryPatches(next);
return XP.patchListEqualsBy(
R.complement(XP.couldValidityChange),
prevLibPatches,
nextLibPatches
);
}
);

export const getLocalPatches = createMemoizedSelector(
[getLocalPatchesList, ProjectSelectors.getProject],
[R.equals],
[
patchListChangesNotAffectProjectBrowserLook,
projectChangesNotAffectProjectBrowserLook,
],
(patches, project) =>
R.compose(
R.sortBy(XP.getPatchPath),
Expand Down
17 changes: 17 additions & 0 deletions packages/xod-func-tools/src/monads.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,23 @@ export const catMaybies = R.compose(
R.filter(Maybe.isJust)
);

export const maybeEqualsWith = def(
'maybeEqualsWith :: (a -> a -> Boolean) -> Maybe a -> Maybe a -> Boolean',
(compFn, prevMaybe, nextMaybe) => {
if (prevMaybe === nextMaybe) return true;
if (Maybe.isNothing(nextMaybe) || Maybe.isNothing(prevMaybe)) return false;
const prev = explodeMaybe(
'Imposible error: we just checked for Nothings',
prevMaybe
);
const next = explodeMaybe(
'Imposible error: we just checked for Nothings',
nextMaybe
);
return compFn(prev, next);
}
);

/**
* Unwraps Either monad and returns it’s value if it is Right and throws an Error
* if it is Left.
Expand Down
19 changes: 19 additions & 0 deletions packages/xod-func-tools/src/objects.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as R from 'ramda';
import { def } from './types';
import { setOfKeys, diffSet } from './sets';

/**
* Returns an object provided with all `null` and `undefined` values omitted
Expand Down Expand Up @@ -117,3 +118,21 @@ export const invertMap = def(
'invertMap :: Map a b -> Map b a',
R.compose(R.fromPairs, R.map(R.reverse), R.toPairs)
);

export const compareMapsBy = def(
'compareMapsBy :: (b -> StrMap a) -> b -> b -> Boolean',
(mapGetter, prev, next) => {
// If same nodes maps — nothing changed
const prevMap = mapGetter(prev);
const nextMap = mapGetter(next);
if (prevMap === nextMap) return false;

const prevMapKeys = setOfKeys(prevMap);
const nextMapKeys = setOfKeys(nextMap);
const diffMapKeys = diffSet(prevMapKeys, nextMapKeys);
// If there is some nodes added/deleted
if (diffMapKeys.size > 0) return true;

return false;
}
);
10 changes: 9 additions & 1 deletion packages/xod-func-tools/src/sets.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import * as R from 'ramda';

// :: [a] -> Set a
export const toSet = a => new Set(a);
export const setOf = a => new Set(a);
// :: a -> Set [a] -> Boolean
export const inSet = R.curry((v, s) => (s.has && s.has(v)) || false);
// :: StrMap a -> Set a
export const setOfKeys = R.pipe(R.keys, x => new Set(x));
// :: Set a -> Set a -> Set a
export const diffSet = R.curry((aSet, bSet) => {
const a = Array.from(aSet).filter(x => !inSet(x, bSet));
const b = Array.from(bSet).filter(x => !inSet(x, aSet));
return new Set(a.concat(b));
});
5 changes: 5 additions & 0 deletions packages/xod-project/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export {
isDeprecatedPatch,
getDeprecationReason,
isUtilityPatch,
haveAddedNodesOrChangedTypesOrBoundValues,
patchListEqualsBy,
didUtilityOrDeprecatedMarkersChange,
couldTypeDeductionChange,
couldValidityChange,
} from './patch';
export {
getFilename as getAttachmentFilename,
Expand Down
126 changes: 124 additions & 2 deletions packages/xod-project/src/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
foldMaybe,
fail,
failOnNothing,
setOfKeys,
inSet,
diffSet,
compareMapsBy,
} from 'xod-func-tools';

import * as CONST from './constants';
Expand Down Expand Up @@ -180,6 +184,8 @@ export const removeImpl = def(
//
// =============================================================================

const getNodes = R.prop('nodes');

/**
* Checks that node id to be equal specified value
*
Expand All @@ -201,7 +207,7 @@ export const nodeIdEquals = def(
*/
export const listNodes = def(
'listNodes :: Patch -> [Node]',
R.compose(R.values, R.prop('nodes'))
R.compose(R.values, getNodes)
);

/**
Expand Down Expand Up @@ -453,14 +459,16 @@ export const isTerminalPatch = def(
//
// =============================================================================

const getLinks = R.prop('links');

/**
* @function listLinks
* @param {Patch} patch - a patch to operate on
* @returns {Link[]} list of all links not sorted in any arbitrary order
*/
export const listLinks = def(
'listLinks :: Patch -> [Link]',
R.compose(R.values, R.prop('links'))
R.compose(R.values, getLinks)
);

/**
Expand Down Expand Up @@ -1081,6 +1089,32 @@ export const isUtilityPatch = def(
)
);

/**
* Compares two lists of Patches by comparator function
*/
export const patchListEqualsBy = def(
'patchListEqualsBy :: (Patch -> Patch -> Boolean) -> [Patch] -> [Patch] -> Boolean',
(compFn, prevPatchesList, nextPatchesList) => {
if (prevPatchesList === nextPatchesList) return true;
if (prevPatchesList.length !== nextPatchesList.length) return false;
const prevPatchesMap = R.indexBy(getPatchPath, prevPatchesList);
const nextPatchesMap = R.indexBy(getPatchPath, prevPatchesList);

const prevPatchPaths = setOfKeys(prevPatchesMap);
const nextPatchPaths = setOfKeys(nextPatchesMap);
const diffPatchPaths = new Set(
[...prevPatchPaths].filter(x => !nextPatchPaths.has(x))
);
if (diffPatchPaths.size > 0) return false;

return R.all(pp => {
const patchPath = getPatchPath(pp);
const np = nextPatchesMap[patchPath];
return compFn(pp, np);
})(prevPatchesList);
}
);

// =============================================================================
//
// Variadic Utils and Getters
Expand Down Expand Up @@ -1566,3 +1600,91 @@ export const checkSpecializationMatchesAbstraction = def(
return Either.of(specializationPatch);
}
);

// =============================================================================
//
// Functions that checks whether something could change
//
// =============================================================================

export const didNodesListChange = def(
'didNodesListChange :: Patch -> Patch -> Boolean',
compareMapsBy(getNodes)
);
export const didLinksListChange = def(
'didNodesListChange :: Patch -> Patch -> Boolean',
compareMapsBy(getLinks)
);

const getSetOfNodeTypesFromPatch = R.compose(
setOfKeys,
R.indexBy(Node.getNodeType),
listNodes
);

export const didUtilityOrDeprecatedMarkersChange = def(
'didUtilityOrDeprecatedMarkersChange :: Patch -> Patch -> Boolean',
(prevPatch, nextPatch) => {
const prevNodeTypes = getSetOfNodeTypesFromPatch(prevPatch);
const nextNodeTypes = getSetOfNodeTypesFromPatch(nextPatch);
const diff = diffSet(prevNodeTypes, nextNodeTypes);
return R.either(
inSet(CONST.DEPRECATED_MARKER_PATH),
inSet(CONST.UTILITY_MARKER_PATH)
)(diff);
}
);

export const didAnyNodeChangeType = def(
'didAnyNodeChangeType :: Patch -> Patch -> Boolean',
(prevPatch, nextPatch) => {
const prevNodes = listNodes(prevPatch);
const nextNodes = getNodes(nextPatch);

return R.any(prevNode => {
const nodeId = Node.getNodeId(prevNode);
return (
nextNodes[nodeId] &&
Node.getNodeType(nextNodes[nodeId]) !== Node.getNodeType(prevNode)
);
})(prevNodes);
}
);

export const didAnyNodeChangeBoundValue = def(
'didAnyNodeChangeBoundValue :: Patch -> Patch -> Boolean',
(prevPatch, nextPatch) => {
const prevNodes = listNodes(prevPatch);
const nextNodes = getNodes(nextPatch);
return R.any(prevNode => {
const nodeId = Node.getNodeId(prevNode);
return (
nextNodes[nodeId] &&
Node.getAllBoundValues(prevNode) !==
Node.getAllBoundValues(nextNodes[nodeId])
);
})(prevNodes);
}
);

export const couldTypeDeductionChange = def(
'couldTypeDeductionChange :: Patch -> Patch -> Boolean',
R.anyPass([
didLinksListChange,
didAnyNodeChangeType,
didAnyNodeChangeBoundValue,
])
);

/**
* Checks Patch for changes that could affect its validity.
*/
export const couldValidityChange = def(
'couldValidityChange :: Patch -> Patch -> Boolean',
R.anyPass([
didNodesListChange,
didLinksListChange,
didAnyNodeChangeType,
didAnyNodeChangeBoundValue,
])
);
8 changes: 4 additions & 4 deletions packages/xod-project/src/typeDeduction.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
fail,
failOnNothing,
prependTraceToError,
toSet,
setOf,
inSet,
} from 'xod-func-tools';

Expand Down Expand Up @@ -226,11 +226,11 @@ export const deducePinTypes = def(
R.map(Link.getLinkNodeIds),
R.filter(
R.both(
// We use `toSet` and `inSet` cause it is faster than `isAmong` function.
R.pipe(Link.getLinkInputNodeId, inSet(R.__, toSet(abstractNodeIds))),
// We use `setOf` and `inSet` cause it is faster than `isAmong` function.
R.pipe(Link.getLinkInputNodeId, inSet(R.__, setOf(abstractNodeIds))),
R.pipe(
Link.getLinkOutputNodeId,
inSet(R.__, toSet(withoutDeferNodeIds))
inSet(R.__, setOf(withoutDeferNodeIds))
)
)
),
Expand Down
Loading

0 comments on commit f996f66

Please sign in to comment.