Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: don't handle SitePage nodes when deleting stale nodes #39161

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,20 @@ exports.createResolvers = ({ createResolvers }) => {
emitter.on(`DELETE_PAGE`, action => {
const nodeId = createPageId(action.payload.path)
const node = getNode(nodeId)
store.dispatch(actions.deleteNode(node))
let deleteNodeActions = actions.deleteNode(node)
if (action.transactionId) {
function swapToStagedDelete(action) {
return {
...action,
type: `DELETE_NODE_STAGING`,
transactionId: action.transactionId,
}
}

deleteNodeActions = Array.isArray(deleteNodeActions)
? deleteNodeActions.map(swapToStagedDelete)
: swapToStagedDelete(deleteNodeActions)
}

store.dispatch(deleteNodeActions)
})
51 changes: 51 additions & 0 deletions packages/gatsby/src/redux/actions/commit-staging-nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ActionsUnion } from "../types"
import { internalCreateNodeWithoutValidation } from "./internal"
import { actions as publicActions } from "./public"
import { getNode } from "../../datastore"

import { store } from "../index"

export const commitStagingNodes = (
transactionId: string
): Array<ActionsUnion> => {
const transaction = store
.getState()
.nodesStaging.transactions.get(transactionId)
if (!transaction) {
return []
}

const actions: Array<ActionsUnion> = [
{
type: `COMMIT_STAGING_NODES`,
payload: {
transactionId,
},
},
]

const nodesState = new Map()
for (const action of transaction) {
if (action.type === `CREATE_NODE_STAGING`) {
nodesState.set(action.payload.id, action)
} else if (action.type === `DELETE_NODE_STAGING` && action.payload?.id) {
nodesState.set(action.payload.id, undefined)
}
}
for (const [id, actionOrDelete] of nodesState.entries()) {
if (actionOrDelete) {
actions.push(
...internalCreateNodeWithoutValidation(
actionOrDelete.payload,
actionOrDelete.plugin,
actionOrDelete
)
)
} else {
// delete case
actions.push(publicActions.deleteNode(getNode(id)))
}
}

return actions
}
103 changes: 103 additions & 0 deletions packages/gatsby/src/redux/actions/internal.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import reporter from "gatsby-cli/lib/reporter"
import _ from "lodash"

import {
IGatsbyConfig,
Expand Down Expand Up @@ -27,8 +28,12 @@ import {
ICreatePageDependencyActionPayloadType,
IDeleteNodeManifests,
IClearGatsbyImageSourceUrlAction,
ActionsUnion,
IGatsbyNode,
IDeleteNodeAction,
} from "../types"

import { store } from "../index"
import { gatsbyConfigSchema } from "../../joi-schemas/joi"
import { didYouMean } from "../../utils/did-you-mean"
import {
Expand All @@ -38,6 +43,8 @@ import {
getInProcessJobPromise,
} from "../../utils/jobs/manager"
import { getEngineContext } from "../../utils/engine-context"
import { getNode } from "../../datastore"
import { hasNodeChanged } from "../../utils/nodes"

/**
* Create a dependency between a page and data. Probably for
Expand Down Expand Up @@ -445,3 +452,99 @@ export const clearGatsbyImageSourceUrls =
type: `CLEAR_GATSBY_IMAGE_SOURCE_URL`,
}
}

// We add a counter to node.internal for fast comparisons/intersections
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the additions here were just moved from public.js file here to make it easier to reuse

// of various node slices. The counter must increase even across builds.
export function getNextNodeCounter(): number {
const lastNodeCounter = store.getState().status.LAST_NODE_COUNTER ?? 0
if (lastNodeCounter >= Number.MAX_SAFE_INTEGER) {
throw new Error(
`Could not create more nodes. Maximum node count is reached: ${lastNodeCounter}`
)
}
return lastNodeCounter + 1
}

export const findChildren = (initialChildren: Array<string>): Array<string> => {
const children = [...initialChildren]
const queue = [...initialChildren]
const traversedNodes = new Set()

while (queue.length > 0) {
const currentChild = getNode(queue.pop()!)
if (!currentChild || traversedNodes.has(currentChild.id)) {
continue
}
traversedNodes.add(currentChild.id)
const newChildren = currentChild.children
if (_.isArray(newChildren) && newChildren.length > 0) {
children.push(...newChildren)
queue.push(...newChildren)
}
}
return children
}

function isNode(node: any): node is IGatsbyNode {
return Boolean(node)
}

export function internalCreateNodeWithoutValidation(
node: IGatsbyNode,
plugin?: IGatsbyPlugin,
actionOptions?: any
): Array<ActionsUnion> {
let deleteActions: Array<IDeleteNodeAction> | undefined
let updateNodeAction

const oldNode = getNode(node.id)

// marking internal-data-bridge as owner of SitePage instead of plugin that calls createPage
if (oldNode && !hasNodeChanged(node.id, node.internal.contentDigest)) {
updateNodeAction = {
...actionOptions,
plugin,
type: `TOUCH_NODE`,
typeName: node.internal.type,
payload: node.id,
}
} else {
// Remove any previously created descendant nodes as they're all due
// to be recreated.
if (oldNode) {
const createDeleteAction = (node: IGatsbyNode): IDeleteNodeAction => {
return {
...actionOptions,
type: `DELETE_NODE`,
plugin,
payload: node,
isRecursiveChildrenDelete: true,
}
}
deleteActions = findChildren(oldNode.children)
.map(getNode)
.filter(isNode)
.map(createDeleteAction)
}

node.internal.counter = getNextNodeCounter()

updateNodeAction = {
...actionOptions,
type: `CREATE_NODE`,
plugin,
oldNode,
payload: node,
}
}

const actions: Array<ActionsUnion> = []

if (deleteActions && deleteActions.length) {
actions.push(...deleteActions)
}

actions.push(updateNodeAction)

return actions
}
137 changes: 45 additions & 92 deletions packages/gatsby/src/redux/actions/public.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@ const apiRunnerNode = require(`../../utils/api-runner-node`)
const { getNonGatsbyCodeFrame } = require(`../../utils/stack-trace-utils`)
const { getPageMode } = require(`../../utils/page-mode`)
const normalizePath = require(`../../utils/normalize-path`).default
import { createJobV2FromInternalJob } from "./internal"
import {
createJobV2FromInternalJob,
internalCreateNodeWithoutValidation,
getNextNodeCounter,
findChildren,
} from "./internal"
import { maybeSendJobToMainProcess } from "../../utils/jobs/worker-messaging"
import { reportOnce } from "../../utils/report-once"
import { wrapNode } from "../../utils/detect-node-mutations"
import { shouldRunOnCreatePage, shouldRunOnCreateNode } from "../plugin-runner"

const isNotTestEnv = process.env.NODE_ENV !== `test`
const isTestEnv = process.env.NODE_ENV === `test`
Expand All @@ -57,28 +63,6 @@ const ensureWindowsDriveIsUppercase = filePath => {
: filePath
}

const findChildren = initialChildren => {
const children = [...initialChildren]
const queue = [...initialChildren]
const traversedNodes = new Set()

while (queue.length > 0) {
const currentChild = getNode(queue.pop())
if (!currentChild || traversedNodes.has(currentChild.id)) {
continue
}
traversedNodes.add(currentChild.id)
const newChildren = currentChild.children
if (_.isArray(newChildren) && newChildren.length > 0) {
children.push(...newChildren)
queue.push(...newChildren)
}
}
return children
}

import type { Plugin } from "./types"

type Job = {
id: string,
}
Expand Down Expand Up @@ -137,10 +121,15 @@ type PageDataRemove = {
* @example
* deletePage(page)
*/
actions.deletePage = (page: IPageInput) => {
actions.deletePage = (
page: IPageInput,
plugin?: Plugin,
actionOptions?: ActionOptions
) => {
return {
type: `DELETE_PAGE`,
payload: page,
transactionId: actionOptions?.transactionId,
}
}

Expand Down Expand Up @@ -475,70 +464,44 @@ ${reservedFields.map(f => ` * "${f}"`).join(`\n`)}
contentDigest: createContentDigest(node),
}
node.id = `SitePage ${internalPage.path}`
const oldNode = getNode(node.id)

let deleteActions
let updateNodeAction
// marking internal-data-bridge as owner of SitePage instead of plugin that calls createPage
if (oldNode && !hasNodeChanged(node.id, node.internal.contentDigest)) {
updateNodeAction = {
...actionOptions,
plugin: { name: `internal-data-bridge` },
type: `TOUCH_NODE`,
typeName: node.internal.type,
payload: node.id,
}
} else {
// Remove any previously created descendant nodes as they're all due
// to be recreated.
if (oldNode) {
const createDeleteAction = node => {
return {
...actionOptions,
type: `DELETE_NODE`,
plugin: { name: `internal-data-bridge` },
payload: node,
isRecursiveChildrenDelete: true,
}
}
deleteActions = findChildren(oldNode.children)
.map(getNode)
.map(createDeleteAction)
}

node.internal.counter = getNextNodeCounter()

updateNodeAction = {
...actionOptions,
type: `CREATE_NODE`,
plugin: { name: `internal-data-bridge` },
oldNode,
payload: node,
}
}
const transactionId =
actionOptions?.transactionId ??
(shouldRunOnCreatePage() ? node.internal.contentDigest : undefined)

// Sanitize page object so we don't attempt to serialize user-provided objects that are not serializable later
const sanitizedPayload = sanitizeNode(internalPage)

const actions = [
{
...actionOptions,
type: `CREATE_PAGE`,
contextModified,
componentModified,
slicesModified,
plugin,
payload: sanitizedPayload,
},
]
const createPageAction = {
...actionOptions,
type: `CREATE_PAGE`,
contextModified,
componentModified,
slicesModified,
plugin,
payload: sanitizedPayload,
}
const actions = [createPageAction]

if (deleteActions && deleteActions.length) {
actions.push(...deleteActions)
if (transactionId) {
createPageAction.transactionId = transactionId
actions.push({
...actionOptions,
type: `CREATE_NODE_STAGING`,
plugin: { name: `internal-data-bridge` },
payload: node,
transactionId,
})
return actions
}

actions.push(updateNodeAction)
const upsertNodeActions = internalCreateNodeWithoutValidation(
node,
{ name: `internal-data-bridge` },
actionOptions
)

return actions
return [...actions, ...upsertNodeActions]
}

/**
Expand Down Expand Up @@ -580,18 +543,6 @@ actions.deleteNode = (node: any, plugin?: Plugin) => {
}
}

// We add a counter to node.internal for fast comparisons/intersections
// of various node slices. The counter must increase even across builds.
function getNextNodeCounter() {
const lastNodeCounter = store.getState().status.LAST_NODE_COUNTER ?? 0
if (lastNodeCounter >= Number.MAX_SAFE_INTEGER) {
throw new Error(
`Could not create more nodes. Maximum node count is reached: ${lastNodeCounter}`
)
}
return lastNodeCounter + 1
}

// memberof notation is added so this code can be referenced instead of the wrapper.
/**
* Create a new node.
Expand Down Expand Up @@ -836,7 +787,7 @@ actions.createNode =
Array.isArray(actions) ? actions : [actions]
).find(action => action.type === `CREATE_NODE`)

if (!createNodeAction) {
if (!createNodeAction || !shouldRunOnCreateNode()) {
return Promise.resolve(undefined)
}

Expand Down Expand Up @@ -1526,3 +1477,5 @@ actions.setRequestHeaders = ({ domain, headers }, plugin: Plugin) => {
}

module.exports = { actions }

// setPublicActions(actions)
Loading
Loading