first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,9 @@
import { Suspense } from 'react'
export default async function Layout({
children,
}: {
children: React.ReactNode
}) {
return <Suspense fallback="Loading...">{children}</Suspense>
}

View File

@@ -0,0 +1,35 @@
import { Suspense } from 'react'
type Params = { step: string }
async function Content({ params }: { params: Promise<Params> }) {
const { step } = await params
return (
<div id="memory-pressure-step-page">
<h1>{`Page ${step}.`}</h1>
{/* Render a large string such that a prefetch of this segment is roughly
1MB over the network */}
<p>{'a'.repeat(1024 * 1024)}</p>
</div>
)
}
export default async function Page({ params }: { params: Promise<Params> }) {
return (
<Suspense fallback="Loading...">
<Content params={params} />
</Suspense>
)
}
export async function generateStaticParams(): Promise<Array<Params>> {
// Generate some number of steps. This should be just enough to trigger the
// default LRU limit used by the client prefetch cache.
// TODO: Once we add a config option to set the prefetch limit, we can set a
// smaller number, to speed up testing iteration (and CI).
const result: Array<Params> = []
for (let i = 0; i < 60; i++) {
result.push({ step: i.toString() })
}
return result
}

View File

@@ -0,0 +1,3 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return children
}

View File

@@ -0,0 +1,117 @@
'use client'
import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
function Tab0() {
return <p id="tab-0-content">Intentionally empty</p>
}
function Tab1() {
return <Link href="/memory-pressure/0">Link 0</Link>
}
function Tab2() {
const links = []
for (let i = 1; i < 60; i++) {
links.push(<Link href={'/memory-pressure/' + i}>Link {i}</Link>)
}
return links
}
export default function MemoryPressure() {
const router = useRouter()
const [selectedTab, setSelectedTab] = useState('0')
const handlePageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedTab(event.target.value)
}
let tab
switch (selectedTab) {
case '0':
tab = <Tab0 />
break
case '1':
tab = <Tab1 />
break
case '2':
tab = <Tab2 />
break
default:
tab = null
break
}
return (
<form>
<h1>Memory pressure</h1>
<p>
Tests that prefetch data is evicted once the cache size grows too large,
using an LRU.
</p>
<p>
On page load, the first link is preloaded. When you switch to the second
tab, the first link is replaced by a large number of a new links.
</p>
<p>The payload for each link's prefetch is about 1MB.</p>
<p>
Switching tabs causes the cache size to exceed the limit{' '}
<em>
(currently hardcoded to 50MB, but we will make this configurable)
</em>
, and the prefetch for the first link will be evicted.
</p>
<div>
<button
id="navigate-to-link-0"
formAction={() => {
// Intentionally using the imperative API instead of a Link so that
// the presence of the button does not affect the
// prefetching behavior.
router.push('/memory-pressure/0')
}}
>
Navigate to link 0
</button>
</div>
<div>
<label>
<input
type="radio"
value="0"
checked={selectedTab === '0'}
onChange={handlePageChange}
name="tabSelection"
/>
Tab 0
</label>
</div>
<div>
<label>
<input
type="radio"
value="1"
checked={selectedTab === '1'}
onChange={handlePageChange}
name="tabSelection"
/>
Tab 1
</label>
</div>
<div>
<label>
<input
type="radio"
value="2"
checked={selectedTab === '2'}
onChange={handlePageChange}
name="tabSelection"
/>
Tab 2
</label>
</div>
<div id="tab-content">{tab}</div>
</form>
)
}

View File

@@ -0,0 +1,8 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
cacheComponents: true,
}
module.exports = nextConfig

View File

@@ -0,0 +1,189 @@
import { nextTestSetup } from 'e2e-utils'
import { createRouterAct } from 'router-act'
import type { CDPSession, Page, Request as PlaywrightRequest } from 'playwright'
describe('segment cache memory pressure', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
if (isNextDev) {
test('disabled in development', () => {})
return
}
it('evicts least recently used prefetch data once cache size exceeds limit', async () => {
let act: ReturnType<typeof createRouterAct>
const browser = await next.browser('/memory-pressure', {
beforePageLoad(page) {
act = createRouterAct(page)
},
})
const switchToTab1 = await browser.elementByCss(
'input[type="radio"][value="1"]'
)
const switchToTab2 = await browser.elementByCss(
'input[type="radio"][value="2"]'
)
// Switch to tab 1 to kick off a prefetch for a link to Page 0.
await act(
async () => {
await switchToTab1.click()
},
{ includes: 'Page 0.' }
)
// Switching to tab 2 causes the cache to overflow, evicting the prefetch
// for the Page 0 link.
await act(
async () => {
await switchToTab2.click()
},
{ includes: 'Page 1.' }
)
// Switching back to tab 1 initiates a new prefetch for Page 0. If
// there are no requests, that means the prefetch was not evicted correctly.
await act(
async () => {
await switchToTab1.click()
},
{
includes: 'Page 0.',
}
)
// Switching back to tab 2 should not evict and re-fetch the prefetches for
// Page 0 and Page 1, since they were recently accessed.
await act(async () => {
await switchToTab2.click()
}, [
{ includes: 'Page 0.', block: 'reject' },
{ includes: 'Page 1.', block: 'reject' },
])
})
it('does not leak memory when repeatedly triggering prefetches', async () => {
let cdpSession: CDPSession
let page: Page
const browser = await next.browser('/memory-pressure', {
async beforePageLoad(p: Page) {
page = p
cdpSession = await page.context().newCDPSession(page)
await cdpSession.send('HeapProfiler.enable')
},
})
const tab0Radio = await browser.elementByCss(
'input[type="radio"][value="0"]'
)
const tab2Radio = await browser.elementByCss(
'input[type="radio"][value="2"]'
)
const totalCycles = 10
const measurements: number[] = []
for (let i = 0; i < totalCycles; i++) {
// Switch to Tab 2 (links mount, prefetches are triggered)
const prefetchesSettled = waitForPrefetchesToSettle(page)
await tab2Radio.click()
await browser.waitForElementByCss('#tab-content a')
await prefetchesSettled
// Switch back to Tab 0 (links unmount)
await tab0Radio.click()
await browser.waitForElementByCss('#tab-0-content')
measurements.push(await getHeapMB(cdpSession))
}
// Use linear regression on measurements (skipping the first as warmup)
// to compute the slope (MB/cycle). If evicted prefetch entries are not
// properly garbage-collected, the heap will grow steadily across cycles
// instead of plateauing.
const afterWarmup = measurements.slice(1)
const n = afterWarmup.length
let sumX = 0
let sumY = 0
let sumXY = 0
let sumXX = 0
for (let j = 0; j < n; j++) {
sumX += j
sumY += afterWarmup[j]
sumXY += j * afterWarmup[j]
sumXX += j * j
}
const growthPerCycle = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX)
try {
expect(growthPerCycle).toBeLessThan(0.1)
} catch (e) {
throw new Error(
`Heap grew ${growthPerCycle.toFixed(3)} MB/cycle.\n` +
`Measurements (MB): ${measurements.map((m) => m.toFixed(2)).join(', ')}`
)
}
}, 120_000)
})
async function getHeapMB(cdpSession: CDPSession): Promise<number> {
await cdpSession.send('HeapProfiler.collectGarbage')
await cdpSession.send('HeapProfiler.collectGarbage')
const { usedSize } = await cdpSession.send('Runtime.getHeapUsage')
return usedSize / 1024 / 1024
}
/**
* Returns a promise that resolves once all in-flight prefetch requests have
* received responses and no new ones have been initiated for `quietMs`. Must be
* called *before* the action that triggers prefetches so that no requests are
* missed.
*/
function waitForPrefetchesToSettle(page: Page, quietMs = 200): Promise<void> {
let inFlight = 0
let resolve: () => void
let timer: ReturnType<typeof setTimeout> | null = null
const promise = new Promise<void>((r) => {
resolve = r
})
function done() {
page.off('request', onRequest)
page.off('requestfinished', onRequestDone)
page.off('requestfailed', onRequestDone)
resolve()
}
function check() {
if (inFlight === 0) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(done, quietMs)
}
}
function onRequest(req: PlaywrightRequest) {
if (req.headers()['next-router-segment-prefetch']) {
inFlight++
if (timer) {
clearTimeout(timer)
}
}
}
function onRequestDone(req: PlaywrightRequest) {
if (req.headers()['next-router-segment-prefetch']) {
inFlight--
check()
}
}
page.on('request', onRequest)
page.on('requestfinished', onRequestDone)
page.on('requestfailed', onRequestDone)
return promise
}