Skip to content

Commit 006ecd8

Browse files
vladarGatsbyJS Bot
authored andcommitted
perf(gatsby): Avoid unnecessary type inference during bootstrap (#19781)
* perf(gatsby): Avoid unnecessary type inference during bootstrap * Add tests for inference states * Perf: disable incremental inference for SitePage
1 parent e0c1618 commit 006ecd8

File tree

12 files changed

+292
-28
lines changed

12 files changed

+292
-28
lines changed

packages/gatsby/src/redux/__tests__/__snapshots__/index.js.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ Object {
1616
"query": "",
1717
},
1818
},
19-
"inferenceMetadata": Object {},
2019
"staticQueryComponents": Map {},
2120
"status": Object {
2221
"plugins": Object {},

packages/gatsby/src/redux/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ const saveState = () => {
6060
`components`,
6161
`staticQueryComponents`,
6262
`webpackCompilationHash`,
63-
`inferenceMetadata`,
6463
])
6564

6665
return writeToCache(pickedState)

packages/gatsby/src/redux/reducers/inference-metadata.js

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,98 @@
33
const { omit } = require(`lodash`)
44
const {
55
addNode,
6+
addNodes,
67
deleteNode,
78
ignore,
9+
disable,
810
} = require(`../../schema/infer/inference-metadata`)
911
const { NodeInterfaceFields } = require(`../../schema/types/node-interface`)
1012
const { typesWithoutInference } = require(`../../schema/types/type-defs`)
1113

14+
const StepsEnum = {
15+
initialBuild: `initialBuild`,
16+
incrementalBuild: `incrementalBuild`,
17+
}
18+
19+
const initialState = () => {
20+
return {
21+
step: StepsEnum.initialBuild, // `initialBuild` | `incrementalBuild`
22+
typeMap: {},
23+
}
24+
}
25+
26+
module.exports = (state = initialState(), action) => {
27+
switch (action.type) {
28+
case `CREATE_NODE`:
29+
case `DELETE_NODE`:
30+
case `DELETE_NODES`:
31+
case `ADD_CHILD_NODE_TO_PARENT_NODE`:
32+
case `ADD_FIELD_TO_NODE`: {
33+
// Perf: disable incremental inference until the first schema build.
34+
// There are plugins which create and delete lots of nodes during bootstrap,
35+
// which makes this reducer to do a lot of unnecessary work.
36+
// Instead we defer the initial metadata creation until the first schema build
37+
// and then enable incremental updates explicitly
38+
if (state.step === StepsEnum.initialBuild) {
39+
return state
40+
}
41+
state.typeMap = incrementalReducer(state.typeMap, action)
42+
return state
43+
}
44+
45+
case `START_INCREMENTAL_INFERENCE`: {
46+
return {
47+
...state,
48+
step: StepsEnum.incrementalBuild,
49+
}
50+
}
51+
52+
case `DELETE_CACHE`: {
53+
return initialState()
54+
}
55+
56+
default: {
57+
state.typeMap = incrementalReducer(state.typeMap, action)
58+
return state
59+
}
60+
}
61+
}
62+
1263
const ignoredFields = new Set([
1364
...NodeInterfaceFields,
1465
`$loki`,
1566
`__gatsby_resolved`,
1667
])
1768

18-
module.exports = (state = {}, action) => {
19-
switch (action.type) {
20-
case `DELETE_CACHE`:
21-
return {}
69+
const initialTypeMetadata = () => {
70+
return { ignoredFields }
71+
}
2272

73+
const incrementalReducer = (state = {}, action) => {
74+
switch (action.type) {
2375
case `CREATE_TYPES`: {
2476
const typeDefs = Array.isArray(action.payload)
2577
? action.payload
2678
: [action.payload]
2779
const ignoredTypes = typeDefs.reduce(typesWithoutInference, [])
2880
ignoredTypes.forEach(type => {
29-
state[type] = ignore(state[type])
81+
state[type] = ignore(state[type] || initialTypeMetadata())
82+
})
83+
return state
84+
}
85+
86+
case `BUILD_TYPE_METADATA`: {
87+
// Overwrites existing metadata
88+
const { nodes, typeName } = action.payload
89+
state[typeName] = addNodes(initialTypeMetadata(), nodes)
90+
return state
91+
}
92+
93+
case `DISABLE_TYPE_INFERENCE`: {
94+
// Note: types disabled here will be re-enabled after BUILD_TYPE_METADATA
95+
const types = action.payload
96+
types.forEach(type => {
97+
state[type] = disable(state[type] || initialTypeMetadata())
3098
})
3199
return state
32100
}
@@ -35,17 +103,17 @@ module.exports = (state = {}, action) => {
35103
const { payload: node, oldNode } = action
36104
const { type } = node.internal
37105
if (oldNode) {
38-
state[type] = deleteNode(state[type] || { ignoredFields }, oldNode)
106+
state[type] = deleteNode(state[type] || initialTypeMetadata(), oldNode)
39107
}
40-
state[type] = addNode(state[type] || { ignoredFields }, node)
108+
state[type] = addNode(state[type] || initialTypeMetadata(), node)
41109
return state
42110
}
43111

44112
case `DELETE_NODE`: {
45113
const node = action.payload
46114
if (!node) return state
47115
const { type } = node.internal
48-
state[type] = deleteNode(state[type] || { ignoredFields }, node)
116+
state[type] = deleteNode(state[type] || initialTypeMetadata(), node)
49117
return state
50118
}
51119

@@ -79,7 +147,7 @@ module.exports = (state = {}, action) => {
79147
const { fullNodes } = action
80148
fullNodes.forEach(node => {
81149
const { type } = node.internal
82-
state[type] = deleteNode(state[type] || { ignoredFields }, node)
150+
state[type] = deleteNode(state[type] || initialTypeMetadata(), node)
83151
})
84152
return state
85153
}

packages/gatsby/src/schema/__tests__/build-node-connections.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const makeNodes = () => [
6666
describe(`build-node-connections`, () => {
6767
async function runQuery(query, nodes = makeNodes()) {
6868
store.dispatch({ type: `DELETE_CACHE` })
69+
store.dispatch({ type: `START_INCREMENTAL_INFERENCE` })
6970
nodes.forEach(node =>
7071
actions.createNode(node, { name: `test` })(store.dispatch)
7172
)

packages/gatsby/src/schema/__tests__/build-node-types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const makeNodes = () => [
7575
describe(`build-node-types`, () => {
7676
async function runQuery(query, nodes = makeNodes()) {
7777
store.dispatch({ type: `DELETE_CACHE` })
78+
store.dispatch({ type: `START_INCREMENTAL_INFERENCE` })
7879
nodes.forEach(node =>
7980
actions.createNode(node, { name: `test` })(store.dispatch)
8081
)

packages/gatsby/src/schema/__tests__/connection-input-fields.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ function makeNodes() {
134134

135135
async function queryResult(nodes, query) {
136136
store.dispatch({ type: `DELETE_CACHE` })
137+
store.dispatch({ type: `START_INCREMENTAL_INFERENCE` })
137138
nodes.forEach(node =>
138139
actions.createNode(node, { name: `test` })(store.dispatch)
139140
)

packages/gatsby/src/schema/__tests__/rebuild-schema.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,8 +1148,8 @@ describe(`compatibility with createTypes`, () => {
11481148

11491149
it(`should not collect inference metadata for types with inference disabled`, async () => {
11501150
const { inferenceMetadata } = store.getState()
1151-
const typesToIgnore = Object.keys(inferenceMetadata).filter(
1152-
type => inferenceMetadata[type].ignored
1151+
const typesToIgnore = Object.keys(inferenceMetadata.typeMap).filter(
1152+
type => inferenceMetadata.typeMap[type].ignored
11531153
)
11541154
expect(typesToIgnore).toEqual([`FooFieldsBaz`, `Bar`, `BarBaz`])
11551155
})

packages/gatsby/src/schema/index.js

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,54 @@ const getAllFieldExtensions = () => {
1919
}
2020
}
2121

22-
const build = async ({ parentSpan }) => {
22+
// Schema building requires metadata for type inference.
23+
// Technically it means looping through all type nodes, analyzing node structure
24+
// and then using this aggregated node structure in related GraphQL type.
25+
// Actual logic for inference located in inferenceMetadata reducer and ./infer
26+
// Here we just orchestrate the process via redux actions
27+
const buildInferenceMetadata = ({ types }) =>
28+
new Promise(resolve => {
29+
if (!types || !types.length) {
30+
resolve()
31+
return
32+
}
33+
const typeNames = [...types]
34+
// TODO: use async iterators when we switch to node>=10
35+
// or better investigate if we can offload metadata building to worker/Jobs API
36+
// and then feed the result into redux?
37+
const processNextType = () => {
38+
const typeName = typeNames.pop()
39+
store.dispatch({
40+
type: `BUILD_TYPE_METADATA`,
41+
payload: {
42+
typeName,
43+
nodes: nodeStore.getNodesByType(typeName),
44+
},
45+
})
46+
if (typeNames.length > 0) {
47+
// Give event-loop a break
48+
setTimeout(processNextType, 0)
49+
} else {
50+
resolve()
51+
}
52+
}
53+
processNextType()
54+
})
55+
56+
const build = async ({ parentSpan, fullMetadataBuild = true }) => {
2357
const spanArgs = parentSpan ? { childOf: parentSpan } : {}
2458
const span = tracer.startSpan(`build schema`, spanArgs)
2559

60+
if (fullMetadataBuild) {
61+
// Build metadata for type inference and start updating it incrementally
62+
// except for SitePage type: we rebuild it in rebuildWithSitePage anyway
63+
// so it makes little sense to update it incrementally
64+
// (and those updates may have significant performance overhead)
65+
await buildInferenceMetadata({ types: nodeStore.getTypes() })
66+
store.dispatch({ type: `START_INCREMENTAL_INFERENCE` })
67+
store.dispatch({ type: `DISABLE_TYPE_INFERENCE`, payload: [`SitePage`] })
68+
}
69+
2670
const {
2771
schemaCustomization: { thirdPartySchemas, types, printConfig },
2872
inferenceMetadata,
@@ -70,12 +114,22 @@ const build = async ({ parentSpan }) => {
70114
span.finish()
71115
}
72116

117+
const rebuild = async ({ parentSpan }) =>
118+
await build({ parentSpan, fullMetadataBuild: false })
119+
73120
const rebuildWithSitePage = async ({ parentSpan }) => {
74121
const spanArgs = parentSpan ? { childOf: parentSpan } : {}
75122
const span = tracer.startSpan(
76123
`rebuild schema with SitePage context`,
77124
spanArgs
78125
)
126+
await buildInferenceMetadata({ types: [`SitePage`] })
127+
128+
// Disabling incremental inference for SitePage after the initial build
129+
// as it has a significant performance cost for zero benefits.
130+
// The only benefit is that schema rebuilds when SitePage.context structure changes.
131+
// (one can just restart `develop` in this case)
132+
store.dispatch({ type: `DISABLE_TYPE_INFERENCE`, payload: [`SitePage`] })
79133

80134
const {
81135
schemaCustomization: { composer: schemaComposer },
@@ -111,6 +165,6 @@ const rebuildWithSitePage = async ({ parentSpan }) => {
111165

112166
module.exports = {
113167
build,
114-
rebuild: build,
168+
rebuild,
115169
rebuildWithSitePage,
116170
}

packages/gatsby/src/schema/infer/__tests__/infer-input.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jest.mock(`gatsby-cli/lib/reporter`, () => {
3434

3535
const buildTestSchema = async nodes => {
3636
store.dispatch({ type: `DELETE_CACHE` })
37+
store.dispatch({ type: `START_INCREMENTAL_INFERENCE` })
3738
nodes.forEach(node =>
3839
actions.createNode(node, { name: `test` })(store.dispatch)
3940
)

0 commit comments

Comments
 (0)