Optmize variable hydration

To load the values for a variable, we create a graph of variables but
only hydrate the subset of this graph containing variables in use within
a certain context (and any other variables that those might depend on).

A bug in detecting the approriate subset to hydrate meant that more
variables were being executed than necessary. This commit fixes that
issue.
pull/12758/head
Christopher Henn 2019-03-19 11:06:15 -07:00 committed by Chris Henn
parent f6b36ee4d0
commit e8bd94d7f0
1 changed files with 63 additions and 26 deletions

View File

@ -33,10 +33,7 @@ interface HydrateVarsOptions {
fetcher?: ValueFetcher fetcher?: ValueFetcher
} }
const createVariableGraph = ( const createVariableGraph = (allVariables: Variable[]): VariableNode[] => {
variables: Variable[],
allVariables: Variable[]
): VariableNode[] => {
const nodesByID: {[variableID: string]: VariableNode} = {} const nodesByID: {[variableID: string]: VariableNode} = {}
// First initialize all the nodes // First initialize all the nodes
@ -68,15 +65,64 @@ const createVariableGraph = (
} }
} }
// Only return a graph containing nodes for the variables we care about and return Object.values(nodesByID)
// their dependencies }
const relevantSubGraph = Object.values(nodesByID).filter(
node => /*
Collect all ancestors of a node.
A node `a` is an ancestor of `b` if there exists a path from `a` to `b`.
This function is safe to call on a node within a graph with cycles.
*/
const collectAncestors = (
node: VariableNode,
acc = new Set()
): VariableNode[] => {
for (const parent of node.parents) {
if (!acc.has(parent)) {
acc.add(parent)
collectAncestors(parent, acc)
}
}
return [...acc]
}
/*
Given a variable graph, return the minimal subgraph containing only the nodes
needed to hydrate the values for variables in the passed `variables` argument.
We discard all nodes in the graph unless:
- It is the node for one of the passed variables
- The node for one of the passed variables depends on this node
*/
const findSubgraph = (
graph: VariableNode[],
variables: Variable[]
): VariableNode[] => {
const subgraph = new Set()
for (const node of graph) {
const shouldKeep =
variables.includes(node.variable) || variables.includes(node.variable) ||
node.parents.some(parent => variables.includes(parent.variable)) collectAncestors(node).some(ancestor =>
variables.includes(ancestor.variable)
) )
return relevantSubGraph if (shouldKeep) {
subgraph.add(node)
}
}
for (const node of subgraph) {
node.parents = node.parents.filter(node => subgraph.has(node))
node.children = node.children.filter(node => subgraph.has(node))
}
return [...subgraph]
} }
/* /*
@ -260,21 +306,12 @@ const invalidateCycles = (graph: VariableNode[]): void => {
/* /*
Given a node, mark all ancestors of that node as `Error`. Given a node, mark all ancestors of that node as `Error`.
A node `a` is an ancestor of `b` if there exists a path from `a` to `b`.
Traversing the ancestors of a node stops at `Error` nodes. In this case it is
assumed that every ancestor of the `Error` node has also been marked as
`Error`; this is to avoid getting caught in cycles. Nodes within cycles and
nodes leading to cycles should have already been invalidated at this point by
`invalidateCycles`.
*/ */
const invalidateAncestors = (node: VariableNode): void => { const invalidateAncestors = (node: VariableNode): void => {
for (const parent of node.parents) { const ancestors = collectAncestors(node)
if (parent.status !== RemoteDataState.Error) {
parent.status = RemoteDataState.Error for (const ancestor of ancestors) {
invalidateAncestors(parent) ancestor.status = RemoteDataState.Error
}
} }
} }
@ -336,7 +373,7 @@ export const hydrateVars = (
allVariables: Variable[], allVariables: Variable[],
options: HydrateVarsOptions options: HydrateVarsOptions
): WrappedCancelablePromise<VariableValuesByID> => { ): WrappedCancelablePromise<VariableValuesByID> => {
const graph = createVariableGraph(variables, allVariables) const graph = findSubgraph(createVariableGraph(allVariables), variables)
invalidateCycles(graph) invalidateCycles(graph)