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

Add cache scope handling for dynamic IO for dev/build #70408

Merged
merged 6 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1897,6 +1897,7 @@ export default async function build(
distDir,
configFileName,
runtimeEnvConfig,
dynamicIO: Boolean(config.experimental.dynamicIO),
httpAgentOptions: config.httpAgentOptions,
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
Expand Down Expand Up @@ -2112,6 +2113,7 @@ export default async function build(
pageRuntime,
edgeInfo,
pageType,
dynamicIO: Boolean(config.experimental.dynamicIO),
cacheHandler: config.cacheHandler,
isrFlushToDisk: ciEnvironment.hasNextSupport
? false
Expand Down
6 changes: 6 additions & 0 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,7 @@ export async function buildAppStaticPaths({
dir,
page,
distDir,
dynamicIO,
configFileName,
generateParams,
isrFlushToDisk,
Expand All @@ -1369,6 +1370,7 @@ export async function buildAppStaticPaths({
}: {
dir: string
page: string
dynamicIO: boolean
configFileName: string
generateParams: GenerateParamsResults
distDir: string
Expand Down Expand Up @@ -1397,6 +1399,7 @@ export async function buildAppStaticPaths({
const incrementalCache = new IncrementalCache({
fs: nodeFs,
dev: true,
dynamicIO,
flushToDisk: isrFlushToDisk,
serverDistDir: path.join(distDir, 'server'),
fetchCacheKeyPrefix,
Expand Down Expand Up @@ -1602,6 +1605,7 @@ export async function isPageStatic({
pageRuntime,
edgeInfo,
pageType,
dynamicIO,
originalAppPath,
isrFlushToDisk,
maxMemoryCacheSize,
Expand All @@ -1613,6 +1617,7 @@ export async function isPageStatic({
dir: string
page: string
distDir: string
dynamicIO: boolean
configFileName: string
runtimeEnvConfig: any
httpAgentOptions: NextConfigComplete['httpAgentOptions']
Expand Down Expand Up @@ -1743,6 +1748,7 @@ export async function isPageStatic({
await buildAppStaticPaths({
dir,
page,
dynamicIO,
configFileName,
generateParams,
distDir,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface StaticGenerationStore {

isDraftMode?: boolean
isUnstableNoStore?: boolean
isPrefetchRequest?: boolean

requestEndedState?: { ended?: boolean }
}
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/export/helpers/create-incremental-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { formatDynamicImportPath } from '../../lib/format-dynamic-import-path'

export async function createIncrementalCache({
cacheHandler,
dynamicIO,
cacheMaxMemorySize,
fetchCacheKeyPrefix,
distDir,
dir,
flushToDisk,
}: {
dynamicIO: boolean
cacheHandler?: string
cacheMaxMemorySize?: number
fetchCacheKeyPrefix?: string
Expand All @@ -34,6 +36,7 @@ export async function createIncrementalCache({
dev: false,
requestHeaders: {},
flushToDisk,
dynamicIO,
fetchCache: true,
maxMemoryCacheSize: cacheMaxMemorySize,
fetchCacheKeyPrefix,
Expand Down
27 changes: 17 additions & 10 deletions packages/next/src/export/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
type FallbackRouteParams,
} from '../server/request/fallback-params'
import { needsExperimentalReact } from '../lib/needs-experimental-react'
import { runWithCacheScope } from '../server/async-storage/cache-scope'

const envConfig = require('../shared/lib/runtime-config.external')

Expand Down Expand Up @@ -352,6 +353,7 @@ export async function exportPages(
fetchCacheKeyPrefix,
distDir,
dir,
dynamicIO: Boolean(nextConfig.experimental.dynamicIO),
// skip writing to disk in minimal mode for now, pending some
// changes to better support it
flushToDisk: !hasNextSupport,
Expand Down Expand Up @@ -459,21 +461,26 @@ export async function exportPages(

return { result, path, pageKey }
}
// for each build worker we share one dynamic IO cache scope
// this is only leveraged if the flag is enabled
const dynamicIOCacheScope = new Map()

for (let i = 0; i < paths.length; i += maxConcurrency) {
const subset = paths.slice(i, i + maxConcurrency)
await runWithCacheScope({ cache: dynamicIOCacheScope }, async () => {
for (let i = 0; i < paths.length; i += maxConcurrency) {
const subset = paths.slice(i, i + maxConcurrency)

const subsetResults = await Promise.all(
subset.map((path) =>
exportPageWithRetry(
path,
nextConfig.experimental.staticGenerationRetryCount ?? 1
const subsetResults = await Promise.all(
subset.map((path) =>
exportPageWithRetry(
path,
nextConfig.experimental.staticGenerationRetryCount ?? 1
)
)
)
)

results.push(...subsetResults)
}
results.push(...subsetResults)
}
})

return results
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,7 @@ export const renderToHTMLOrFlight: AppPageRender = (
fallbackRouteParams,
renderOpts,
requestEndedState,
isPrefetchRequest: Boolean(req.headers[NEXT_ROUTER_PREFETCH_HEADER]),
},
(staticGenerationStore) =>
renderToHTMLOrFlightImpl(
Expand Down
27 changes: 27 additions & 0 deletions packages/next/src/server/async-storage/cache-scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AsyncLocalStorage } from 'async_hooks'

export interface CacheScopeStore {
cache?: Map<string, any>
}

export const cacheScopeAsyncLocalStorage =
new AsyncLocalStorage<CacheScopeStore>()

/**
* For dynamic IO handling we want to have a scoped memory
* cache which can live either the lifetime of a build worker,
* the lifetime of a specific request, or from a prefetch request
* to the request for non-prefetch version of a page (with
* drop-off after so long to prevent memory inflating)
*/
export function runWithCacheScope(
store: Partial<CacheScopeStore>,
fn: (...args: any[]) => Promise<any>
) {
return cacheScopeAsyncLocalStorage.run(
{
cache: store.cache || new Map(),
},
fn
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type StaticGenerationContext = {
fallbackRouteParams: FallbackRouteParams | null

requestEndedState?: { ended?: boolean }
isPrefetchRequest?: boolean
renderOpts: {
incrementalCache?: IncrementalCache
isOnDemandRevalidate?: boolean
Expand Down Expand Up @@ -69,6 +70,7 @@ export const withStaticGenerationStore: WithStore<
fallbackRouteParams,
renderOpts,
requestEndedState,
isPrefetchRequest,
}: StaticGenerationContext,
callback: (store: StaticGenerationStore) => Result
): Result => {
Expand Down Expand Up @@ -111,6 +113,7 @@ export const withStaticGenerationStore: WithStore<
isDraftMode: renderOpts.isDraftMode,

requestEndedState,
isPrefetchRequest,
}

// TODO: remove this when we resolve accessing the store outside the execution context
Expand Down
63 changes: 62 additions & 1 deletion packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ import type { RouteModule } from './route-modules/route-module'
import { FallbackMode, parseFallbackField } from '../lib/fallback'
import { toResponseCacheEntry } from './response-cache/utils'
import { scheduleOnNextTick } from '../lib/scheduler'
import { PrefetchCacheScopes } from './lib/prefetch-cache-scopes'
import {
runWithCacheScope,
type CacheScopeStore,
} from './async-storage/cache-scope'

export type FindComponentsResult = {
components: LoadComponentsReturnType
Expand Down Expand Up @@ -454,6 +459,14 @@ export default abstract class Server<

private readonly isAppPPREnabled: boolean

private readonly prefetchCacheScopesDev = new PrefetchCacheScopes()

/**
* This is used to persist cache scopes across
* prefetch -> full route requests for dynamic IO
* it's only fully used in dev
*/

public constructor(options: ServerOptions) {
const {
dir = '.',
Expand Down Expand Up @@ -2741,7 +2754,7 @@ export default abstract class Server<
}
}

const responseGenerator: ResponseGenerator = async ({
let responseGenerator: ResponseGenerator = async ({
hasResolved,
previousCacheEntry,
isRevalidating,
Expand Down Expand Up @@ -2991,6 +3004,54 @@ export default abstract class Server<
}
}

if (this.nextConfig.experimental.dynamicIO) {
const originalResponseGenerator = responseGenerator

responseGenerator = async (
...args: Parameters<typeof responseGenerator>
): ReturnType<typeof responseGenerator> => {
let cache: CacheScopeStore['cache'] | undefined

if (this.renderOpts.dev) {
cache = this.prefetchCacheScopesDev.get(urlPathname)

// we need to seed the prefetch cache scope in dev
// since we did not have a prefetch cache available
// and this is not a prefetch request
if (
!cache &&
!isPrefetchRSCRequest &&
routeModule?.definition.kind === RouteKind.APP_PAGE
) {
req.headers[RSC_HEADER] = '1'
req.headers[NEXT_ROUTER_PREFETCH_HEADER] = '1'

cache = new Map()

await runWithCacheScope({ cache }, () =>
originalResponseGenerator(...args)
)
this.prefetchCacheScopesDev.set(urlPathname, cache)

delete req.headers[RSC_HEADER]
delete req.headers[NEXT_ROUTER_PREFETCH_HEADER]
}
}

return runWithCacheScope({ cache }, () =>
originalResponseGenerator(...args)
).finally(() => {
if (this.renderOpts.dev) {
if (isPrefetchRSCRequest) {
this.prefetchCacheScopesDev.set(urlPathname, cache)
} else {
this.prefetchCacheScopesDev.del(urlPathname)
}
}
})
}
}

const cacheEntry = await this.responseCache.get(
ssgCacheKey,
responseGenerator,
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/dev/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,7 @@ export default class DevServer extends Server {
configFileName,
publicRuntimeConfig,
serverRuntimeConfig,
dynamicIO: Boolean(this.nextConfig.experimental.dynamicIO),
},
httpAgentOptions,
locales,
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/dev/static-paths-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type RuntimeConfig = {
configFileName: string
publicRuntimeConfig: { [key: string]: any }
serverRuntimeConfig: { [key: string]: any }
dynamicIO: boolean
}

// we call getStaticPaths in a separate process to ensure
Expand Down Expand Up @@ -115,6 +116,7 @@ export async function loadStaticPaths({
return await buildAppStaticPaths({
dir,
page: pathname,
dynamicIO: config.dynamicIO,
generateParams,
configFileName: config.configFileName,
distDir,
Expand Down
Loading
Loading