Skip to content

Commit

Permalink
perf(gatsby): Create index on the fly for non-id index
Browse files Browse the repository at this point in the history
  • Loading branch information
pvdz committed Jan 20, 2020
1 parent 1435671 commit 97f0a80
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 17 deletions.
91 changes: 91 additions & 0 deletions packages/gatsby/src/redux/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,94 @@ const addResolvedNodes = (typeName, arr) => {
}

exports.addResolvedNodes = addResolvedNodes

// TODO: This local cache is "global" to the nodejs instance. It should be
// reset after each point where nodes are mutated. This is currently
// not the case. I don't think we want to persist this cache in redux.
// (This works currently fine for a `build`, but won't for `develop`)
let mappedByKey

const ensureIndexByTypedChain = (chain, nodeTypeNames) => {
const chained = chain.join(`+`)

if (chained === `id`) {
return
}

if (!mappedByKey) {
mappedByKey = new Map()
}

const nodeTypeNamePrefix = nodeTypeNames.join(`,`) + `/`
// The format of the typedKey is `type,type/path+to+eqobj`
const typedKey = nodeTypeNamePrefix + chained

let byKeyValue = mappedByKey.get(typedKey)
if (byKeyValue) {
return
}

const { nodes, resolvedNodesCache } = store.getState()

byKeyValue = new Map() // Map<node.value, Set<all nodes with this value for this key>>
mappedByKey.set(typedKey, byKeyValue)

nodes.forEach(node => {
if (!nodeTypeNames.includes(node.internal.type)) {
return
}

// Walk the chain in the node to find the filter target
let v = node
let i = 0
while (i < chain.length && v) {
const nextProp = chain[i++]
v = v[nextProp]
}

if (typeof v !== `string` || i !== chain.length) {
// Not sure whether this is supposed to happen, but this means that either
// - The node chain ended with `undefined`, or
// - The node chain ended in something other than a string, or
// - A part in the chain in the object was not an object
return
}

let set = byKeyValue.get(v)
if (!set) {
set = new Set()
byKeyValue.set(v, set)
}
set.add(node)

if (!node.__gatsby_resolved) {
const typeName = node.internal.type
const resolvedNodes = resolvedNodesCache.get(typeName)
node.__gatsby_resolved = resolvedNodes?.get(node.id)
}
})
}

exports.ensureIndexByTypedChain = ensureIndexByTypedChain

const getNodesByTypedChain = (chain, value, nodeTypeNames) => {
const key = chain.join(`+`)

if (key === `id`) {
const node = getNode(value)

if (nodeTypeNames.includes(node.internal.type)) {
return node
}

return undefined
}

const typedKey = nodeTypeNames.join(`,`) + `/` + key

let byTypedKey = mappedByKey?.get(typedKey)

return byTypedKey?.get(value)
}

exports.getNodesByTypedChain = getNodesByTypedChain
1 change: 1 addition & 0 deletions packages/gatsby/src/redux/reducers/resolved-nodes.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// resolvedNodesCache
module.exports = (state = new Map(), action) => {
switch (action.type) {
case `DELETE_CACHE`:
Expand Down
71 changes: 54 additions & 17 deletions packages/gatsby/src/redux/run-sift.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ const {
objectToDottedField,
liftResolvedFields,
} = require(`../db/common/query`)
const {
ensureIndexByTypedChain,
getNodesByTypedChain,
resolvedNodesCache,
addResolvedNodes,
getNode,
} = require(`./nodes`)

/////////////////////////////////////////////////////////////////////
// Parse filter
Expand Down Expand Up @@ -123,24 +130,54 @@ function handleMany(siftArgs, nodes, sort, resolvedFields) {
* if `firstOnly` is true
*/
const runSift = (args: Object) => {
const { getNode, addResolvedNodes, getResolvedNode } = require(`./nodes`)

const { nodeTypeNames } = args
if (
args.queryArgs?.filter &&
Object.getOwnPropertyNames(args.queryArgs.filter).length === 1 &&
typeof args.queryArgs.filter?.id?.eq === `string`
) {
// The args have an id.eq which subsumes all other queries
// Since the id of every node is unique there can only ever be one node found this way. Find it and return it.
let id = args.queryArgs.filter.id.eq
let node = undefined
nodeTypeNames.some(typeName => {
node = getResolvedNode(typeName, id)
return !!node
})
if (node) {
return [node]

const filter = args.queryArgs?.filter
if (filter) {
// This can be any string of {a: {b: {c: {eq: "x"}}}} and we want to confirm there is exactly one leaf in this
// structure and that this leaf is `eq`. The actual names are irrelevant, they are props on each node.

// TODO: This seems to perform okay but is it faster to just JSON.stringify the filter and use regexes to get the chain...?
let chain = []
let props = Object.getOwnPropertyNames(filter)
let obj = filter
while (props?.length === 1 && props[0] !== `eq`) {
chain.push(props[0])
obj = obj[props[0]]
props = Object.getOwnPropertyNames(obj)
}

// Now either we reached an `eq` (still need to confirm that this is a leaf node), or the current
// object has multiple props and we must bail because we currently don't support that (too complex).
let targetValue = obj?.[props[0]]
if (props.length === 1 && typeof targetValue === `string`) {
// `chain` should now be: `filter = {this: {is: {the: {chain: {eq: "foo"}}}}}` -> `['this', 'is', 'the', 'chain']`

// Extra shortcut for `id`, which we internally index by anyways, so no need to setup anything else
if (chain.join(`,`) === `id`) {
const node = getNode(targetValue)
if (nodeTypeNames.includes(node.internal.type)) {
const resolvedNodes = resolvedNodesCache.get(node.internal.type)
if (resolvedNodes) {
node.__gatsby_resolved = resolvedNodes.get(node.id)
}
return [node]
}
return undefined
}

ensureIndexByTypedChain(chain, nodeTypeNames)

const nodesByKeyValue = getNodesByTypedChain(
chain,
targetValue,
nodeTypeNames
)

if (nodesByKeyValue?.size > 0) {
return [...nodesByKeyValue]
}
// Not sure if we can just return `undefined` on a miss here
}
}

Expand Down

0 comments on commit 97f0a80

Please sign in to comment.