Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 542cda8

Browse files
authored
perf(gatsby): support empty filters without sift (gatsbyjs#24258)
* perf(gatsby): support empty filters without sift * Add a test case for empty filters * Make return type consistent with what Sift returned In particular the fast filters were returning `undefined` where they should be returning `null`. Fairly benign internal change but will be relevant once this file gets the TS treatment. * Remove some old support checks now that all ops are supported * Only set the resolved property if something got resolved
1 parent 2e629cb commit 542cda8

File tree

3 files changed

+105
-74
lines changed

3 files changed

+105
-74
lines changed

packages/gatsby/src/redux/nodes.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,58 @@ export const ensureIndexByQuery = (
354354
postIndexingMetaSetup(filterCache, op)
355355
}
356356

357+
export function ensureEmptyFilterCache(
358+
filterCacheKey,
359+
nodeTypeNames: string[],
360+
filtersCache: FiltersCache
361+
): void {
362+
// This is called for queries without any filters
363+
// We want to cache the result since it's basically a set of nodes by type(s)
364+
// There are sites that have multiple queries which are empty
365+
366+
const state = store.getState()
367+
const resolvedNodesCache = state.resolvedNodesCache
368+
const nodesUnordered: Array<IGatsbyNode> = []
369+
370+
filtersCache.set(filterCacheKey, {
371+
op: `$eq`, // Ignore.
372+
byValue: new Map<FilterValueNullable, Set<IGatsbyNode>>(),
373+
meta: {
374+
nodesUnordered, // This is what we want
375+
},
376+
})
377+
378+
if (nodeTypeNames.length === 1) {
379+
getNodesByType(nodeTypeNames[0]).forEach(node => {
380+
if (!node.__gatsby_resolved) {
381+
const typeName = node.internal.type
382+
const resolvedNodes = resolvedNodesCache.get(typeName)
383+
const resolved = resolvedNodes?.get(node.id)
384+
if (resolved !== undefined) {
385+
node.__gatsby_resolved = resolved
386+
}
387+
}
388+
nodesUnordered.push(node)
389+
})
390+
} else {
391+
// Here we must first filter for the node type
392+
// This loop is expensive at scale (!)
393+
state.nodes.forEach(node => {
394+
if (nodeTypeNames.includes(node.internal.type)) {
395+
if (!node.__gatsby_resolved) {
396+
const typeName = node.internal.type
397+
const resolvedNodes = resolvedNodesCache.get(typeName)
398+
const resolved = resolvedNodes?.get(node.id)
399+
if (resolved !== undefined) {
400+
node.__gatsby_resolved = resolved
401+
}
402+
}
403+
nodesUnordered.push(node)
404+
}
405+
})
406+
}
407+
}
408+
357409
function addNodeToFilterCache(
358410
node: IGatsbyNode,
359411
chain: Array<string>,

packages/gatsby/src/redux/run-sift.js

Lines changed: 44 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,22 @@ const {
1212
dbQueryToSiftQuery,
1313
} = require(`../db/common/query`)
1414
const {
15+
ensureEmptyFilterCache,
1516
ensureIndexByQuery,
1617
ensureIndexByElemMatch,
1718
getNodesFromCacheByValue,
1819
addResolvedNodes,
1920
getNode: siftGetNode,
2021
} = require(`./nodes`)
2122

22-
const FAST_OPS = [
23-
`$eq`,
24-
`$ne`,
25-
`$lt`,
26-
`$lte`,
27-
`$gt`,
28-
`$gte`,
29-
`$in`,
30-
`$nin`,
31-
`$regex`, // Note: this includes $glob
32-
]
33-
3423
// More of a testing mechanic, to verify whether last runSift call used Sift
3524
let lastFilterUsedSift = false
3625

3726
/**
3827
* Creates a key for one filterCache inside FiltersCache
3928
*
4029
* @param {Array<string>} typeNames
41-
* @param {DbQuery} filter
30+
* @param {DbQuery | null} filter If null the key will have empty path/op parts
4231
* @returns {FilterCacheKey} (a string: `types.join()/path.join()/operator` )
4332
*/
4433
const createFilterCacheKey = (typeNames, filter) => {
@@ -147,10 +136,15 @@ function handleMany(siftArgs, nodes) {
147136
*
148137
* @param {Array<DbQuery>} filters Resolved. (Should be checked by caller to exist)
149138
* @param {Array<string>} nodeTypeNames
150-
* @param {FiltersCache} filtersCache
151-
* @returns {Array<IGatsbyNode> | undefined}
139+
* @param {undefined | null | FiltersCache} filtersCache
140+
* @returns {Array<IGatsbyNode> | null}
152141
*/
153-
const runFiltersWithoutSift = (filters, nodeTypeNames, filtersCache) => {
142+
const filterWithoutSift = (filters, nodeTypeNames, filtersCache) => {
143+
if (!filtersCache) {
144+
// If no filter cache is passed on, explicitly don't use one
145+
return null
146+
}
147+
154148
const nodesPerValueSets /*: Array<Set<IGatsbyNode>> */ = getBucketsForFilters(
155149
filters,
156150
nodeTypeNames,
@@ -159,7 +153,7 @@ const runFiltersWithoutSift = (filters, nodeTypeNames, filtersCache) => {
159153

160154
if (!nodesPerValueSets) {
161155
// Let Sift take over as fallback
162-
return undefined
156+
return null
163157
}
164158

165159
// Put smallest last (we'll pop it)
@@ -188,11 +182,14 @@ const runFiltersWithoutSift = (filters, nodeTypeNames, filtersCache) => {
188182
// case for all value pairs? How likely is that to ever be reused?
189183

190184
if (result.length === 0) {
191-
return undefined
185+
return null
192186
}
193187
return result
194188
}
195189

190+
// Not a public API
191+
exports.filterWithoutSift = filterWithoutSift
192+
196193
/**
197194
* @param {Array<DbQuery>} filters
198195
* @param {Array<string>} nodeTypeNames
@@ -256,11 +253,7 @@ const getBucketsForQueryFilter = (
256253
) => {
257254
let {
258255
path: filterPath,
259-
query: {
260-
// Note: comparator is verified to be a FilterOp in filterWithoutSift
261-
comparator /*: as FilterOp*/,
262-
value: filterValue,
263-
},
256+
query: { comparator /*: as FilterOp*/, value: filterValue },
264257
} = filter
265258

266259
if (!filtersCache.has(filterCacheKey)) {
@@ -323,10 +316,6 @@ const collectBucketForElemMatch = (
323316
}
324317
}
325318

326-
if (!FAST_OPS.includes(comparator)) {
327-
return false
328-
}
329-
330319
if (!filtersCache.has(filterCacheKey)) {
331320
ensureIndexByElemMatch(
332321
comparator,
@@ -411,7 +400,7 @@ exports.didLastFilterUseSift = function _didLastFilterUseSift() {
411400
* @param {Array<string>} nodeTypeNames
412401
* @param {undefined | null | FiltersCache} filtersCache
413402
* @param resolvedFields
414-
* @returns {Array<IGatsbyNode> | undefined} Collection of results. Collection
403+
* @returns {Array<IGatsbyNode> | null} Collection of results. Collection
415404
* will be limited to 1 if `firstOnly` is true
416405
*/
417406
const applyFilters = (
@@ -444,7 +433,27 @@ const applyFilters = (
444433
}
445434
}
446435

447-
const result = filterWithoutSift(filters, nodeTypeNames, filtersCache)
436+
if (filtersCache && filters.length === 0) {
437+
let filterCacheKey = createFilterCacheKey(nodeTypeNames, null)
438+
if (!filtersCache.has(filterCacheKey)) {
439+
ensureEmptyFilterCache(filterCacheKey, nodeTypeNames, filtersCache)
440+
}
441+
442+
const cache = filtersCache.get(filterCacheKey).meta.nodesUnordered
443+
444+
lastFilterUsedSift = false
445+
446+
if (firstOnly || cache.length) {
447+
return cache.slice(0)
448+
}
449+
return null
450+
}
451+
452+
const result /*: Array<IGatsbyNode> | null */ = filterWithoutSift(
453+
filters,
454+
nodeTypeNames,
455+
filtersCache
456+
)
448457

449458
lastFilterUsedSift = false
450459
if (result) {
@@ -458,7 +467,7 @@ const applyFilters = (
458467
}
459468
lastFilterUsedSift = true
460469

461-
const siftResult = filterWithSift(
470+
const siftResult /*: Array<IGatsbyNode> | null */ = filterWithSift(
462471
filters,
463472
firstOnly,
464473
nodeTypeNames,
@@ -493,54 +502,14 @@ const filterToStats = (
493502
}
494503
}
495504

496-
/**
497-
* Check if filter op is supported (not all are). If so, uses custom
498-
* fast indexes based on filter and types and returns any result it finds.
499-
* If conditions are not met or no nodes are found, returns undefined and
500-
* a slow run through Sift is executed instead.
501-
* This function is a noop if no filter cache is given to it.
502-
*
503-
* @param {Array<DbQuery>} filters Resolved. (Should be checked by caller to exist)
504-
* @param {Array<string>} nodeTypeNames
505-
* @param {undefined | null | FiltersCache} filtersCache
506-
* @returns {Array<IGatsbyNode> | undefined} Collection of results
507-
*/
508-
const filterWithoutSift = (filters, nodeTypeNames, filtersCache) => {
509-
if (!filtersCache) {
510-
// If no filter cache is passed on, explicitly don't use one
511-
return undefined
512-
}
513-
514-
if (filters.length === 0) {
515-
// If no filters are given, go through Sift. This does not appear to be
516-
// slower than shortcutting it here.
517-
return undefined
518-
}
519-
520-
if (
521-
filters.some(
522-
filter =>
523-
filter.type === `query` && !FAST_OPS.includes(filter.query.comparator)
524-
)
525-
) {
526-
// If there's a filter with non-supported op, stop now.
527-
return undefined
528-
}
529-
530-
return runFiltersWithoutSift(filters, nodeTypeNames, filtersCache)
531-
}
532-
533-
// Not a public API
534-
exports.filterWithoutSift = filterWithoutSift
535-
536505
/**
537506
* Use sift to apply filters
538507
*
539508
* @param {Array<DbQuery>} filters Resolved
540509
* @param {boolean} firstOnly
541510
* @param {Array<string>} nodeTypeNames
542511
* @param resolvedFields
543-
* @returns {Array<IGatsbyNode> | undefined | null} Collection of results.
512+
* @returns {Array<IGatsbyNode> | null} Collection of results.
544513
* Collection will be limited to 1 if `firstOnly` is true
545514
*/
546515
const filterWithSift = (filters, firstOnly, nodeTypeNames, resolvedFields) => {
@@ -566,7 +535,7 @@ const filterWithSift = (filters, firstOnly, nodeTypeNames, resolvedFields) => {
566535
* @param {Array<string>} nodeTypeNames
567536
* @param resolvedFields
568537
* @param {function(id: string): IGatsbyNode | undefined} getNode
569-
* @returns {Array<IGatsbyNode> | undefined | null} Collection of results.
538+
* @returns {Array<IGatsbyNode> | null} Collection of results.
570539
* Collection will be limited to 1 if `firstOnly` is true
571540
*/
572541
const runSiftOnNodes = (
@@ -605,13 +574,14 @@ const runSiftOnNodes = (
605574
/**
606575
* Given a list of filtered nodes and sorting parameters, sort the nodes
607576
*
608-
* @param {Array<IGatsbyNode> | undefined | null} nodes Pre-filtered list of nodes
577+
* @param {Array<IGatsbyNode> | null} nodes Pre-filtered list of nodes
609578
* @param {Object | undefined} sort Sorting arguments
610579
* @param resolvedFields
611580
* @returns {Array<IGatsbyNode> | undefined | null} Same as input, except sorted
612581
*/
613582
const sortNodes = (nodes, sort, resolvedFields, stats) => {
614-
if (!sort || nodes?.length <= 1) {
583+
// `undefined <= 1` and `undefined > 1` are both false so invert the result...
584+
if (!sort || !(nodes?.length > 1)) {
615585
return nodes
616586
}
617587

packages/gatsby/src/schema/__tests__/run-query.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,15 @@ it(`should use the cache argument`, async () => {
414414

415415
describe(desc, () => {
416416
describe(`Filter fields`, () => {
417+
describe(`none`, () => {
418+
it(`handles empty filter`, async () => {
419+
const [result, allNodes] = await runFastFilter({})
420+
421+
// Expecting all nodes
422+
expect(result?.length).toEqual(allNodes.length)
423+
})
424+
})
425+
417426
describe(`$eq`, () => {
418427
it(`handles eq operator with number value`, async () => {
419428
const needle = 2

0 commit comments

Comments
 (0)