diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml index cd47ca3ba1..eae3cd7448 100644 --- a/.github/workflows/client-nav-benchmarks.yml +++ b/.github/workflows/client-nav-benchmarks.yml @@ -36,6 +36,17 @@ jobs: benchmark: - client-nav - ssr + - memory-server + - memory-client + include: + - benchmark: client-nav + mode: simulation + - benchmark: ssr + mode: simulation + - benchmark: memory-server + mode: memory + - benchmark: memory-client + mode: memory runs-on: ubuntu-latest steps: - name: Checkout @@ -46,8 +57,19 @@ jobs: - name: Setup Tools uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + # Run the build outside of the CodSpeed action to avoid being slowed by intrumentation overhead. + # (and then run the task itself with --excludeTaskDependencies) + - name: Prepare ${{ matrix.benchmark }}:${{ matrix.framework }} benchmark + run: >- + pnpm nx run + @benchmarks/${{ matrix.benchmark }}:build:${{ matrix.framework }} + - name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0 with: - mode: simulation - run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }} + mode: ${{ matrix.mode }} + run: >- + WITH_INSTRUMENTATION=1 + pnpm nx run + @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }} + --excludeTaskDependencies diff --git a/.gitignore b/.gitignore index 4c5184f4d8..1699592784 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ vite.config.ts.timestamp_* # eslint-plugin-start perf fixtures /e2e/eslint-plugin-start/src/perf/generated + +# local memory flame profiles +benchmarks/memory/**/.profiles/ diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md new file mode 100644 index 0000000000..d9fb24f752 --- /dev/null +++ b/benchmarks/memory/README.md @@ -0,0 +1,186 @@ +# Memory Benchmarks + +Dedicated memory benchmarks for TanStack Router / Start, measured with the +CodSpeed **memory instrument** (`mode: memory` in +`.github/workflows/client-nav-benchmarks.yml`). Two separate benchmarks: + +- `server/` (`@benchmarks/memory-server`) — React/Solid/Vue Start apps, requests against + the built server handler (`handler.fetch`), Node environment. +- `client/` (`@benchmarks/memory-client`) — router-only React/Solid/Vue apps in jsdom. + +These deliberately do **not** reuse the CPU scenarios in `benchmarks/ssr` and +`benchmarks/client-nav`: memory benches need their own iteration counts, +payload sizes, and route shapes, and tuning those must never shift the CPU +baselines. Each scenario keeps a framework level (`react/`, `solid/`, `vue/`) +so framework ports can be added without renames. + +## Layout + +```text +benchmarks/memory// + package.json Nx targets: build:, test:perf:, test:flame:, test:types + bench-utils.ts memoryBenchOptions, seeded LCG (+ sequential request loop on the server side) + vitest..config.ts aggregates scenarios/*//vite.config.ts + scenarios/// + one isolated app per scenario + setup.ts + memory.bench.ts + memory.flame.ts +``` + +One app per scenario; apps and bench names are stable once landed (CodSpeed +continuity). Never grow an existing scenario for a new case — add a scenario. +`setup.ts` imports the built app and exports the concrete workload; +`memory.bench.ts` registers `bench(...)` directly, and `memory.flame.ts` runs the +same workload through the Flame profiler. + +## How the memory instrument executes a bench + +- The bench function is warmed up, then **measured exactly once**, starting + after a forced GC. Under plain `vitest bench` the suites only smoke-test: + timing output is meaningless; real numbers come from CodSpeed. +- Under CodSpeed the bench fn runs several warmup invocations plus the + measured one **on the same mount**, so bench fns must be idempotent and + module-level counters/LCGs are used where ids must never repeat across + invocations. +- Plain `vitest bench` never runs suite hooks (`beforeAll`/`afterAll`) and + only honors tinybench's `setup`/`teardown` options; the CodSpeed runner + does the exact opposite. Client benches therefore register **both** — in + any given mode exactly one pair runs. +- The process runs with V8 determinism flags (predictable GC schedule, + `--no-opt`). Never call `global.gc()` manually. Because of `--no-opt`, + allocation counts overstate production; numbers are for regression + tracking, not absolute claims. +- Keep each bench under **~1.5M allocations** (instrument overhead grows past + 2M); this is the main constraint when tuning iteration counts. + +## Bench shapes and signals + +- **Churn (leak detector):** N sequential iterations at steady state. If one + iteration leaks L bytes, peak grows by ~N·L; healthy builds show a flat + timeline floor independent of N. Tuning check: doubling N must leave peak + roughly unchanged. +- **Peak (footprint):** one (or very few) large operations; peak memory + scaling with the workload is the signal. + +## Scenarios + +### Server + +| Scenario | Shape | Guards against | +| ----------------------- | ----- | --------------------------------------------------------- | +| `request-churn` | churn | cross-request retention in document SSR (unique URLs) | +| `server-fn-churn` | churn | retention in the server-function RPC path | +| `error-paths` | churn | redirect/notFound/error/unmatched paths pinning contexts | +| `aborted-requests` | churn | dangling streams/listeners after mid-stream client aborts | +| `peak-large-page` | peak | per-request peak scaling with page size | +| `streaming-peak` | peak | streaming buffering O(document) instead of O(chunk) | +| `serialization-payload` | peak | double-buffering / string-copy blowups in dehydration | + +### Client + +| Scenario | Shape | Guards against | +| ------------------------- | ----- | -------------------------------------------------------- | +| `navigation-churn` | churn | per-navigation retention at steady state | +| `unique-location-churn` | churn | unbounded href/search-keyed caches (never-repeated URLs) | +| `preload-churn` | churn | preload-cache eviction not releasing memory | +| `loader-data-retention` | churn | departed routes' loader data staying pinned (gcTime 0) | +| `mount-unmount` | churn | router instances not collectable after dispose | +| `interrupted-navigations` | churn | superseded navigations retaining closures/contexts | + +## Conventions + +- Strictly sequential work: at most one request/navigation in flight; each + server response is fully consumed before the next request. Pairing a single + navigation with its render signal via `Promise.all([navigate, rendered])` + is fine — never overlap distinct work items. +- Randomness only via the seeded LCG in `bench-utils.ts`; no `Math.random`, + `Date.now`, or timers — with one documented exception: `streaming-peak`'s + deferred sections use small `setTimeout` delays so deferred stream chunks are + observable across framework renderers. +- Sanity assertions run once at module load and throw on wrong + status/markers, so a bench can never silently measure the wrong thing. +- Server requests follow `benchmarks/ssr` conventions: document GETs send + `accept: text/html`, server-fn requests send `sec-fetch-site: same-origin` + with bodies precomputed at module level. +- Client apps export `mountTestApp` from `app.tsx`; benches import the built + `dist/app.js`; navigations use `replace: true`; unmount does full teardown + (framework root, `__TSR_ROUTER__`, `history.destroy()`); large loader payloads + are never rendered into the DOM. +- `NODE_ENV=production` everywhere (the Nx targets set it). + +## Run + +Smoke-test the CodSpeed/Vitest benchmark entrypoints and typecheck the +scenarios: + +```bash +pnpm nx run @benchmarks/memory-server:test:perf:react --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-server:test:perf:solid --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-server:test:perf:vue --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client:test:perf:react --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client:test:perf:solid --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client:test:perf:vue --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-server:test:types --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client:test:types --outputStyle=stream --skipRemoteCache +``` + +Local attribution profiling, without CodSpeed CLI/login/sudo/upload, uses +`@datadog/pprof` heap sampling and `@platformatic/flame` only to render the +captured pprof files as HTML/Markdown. These targets rebuild the scenarios with +`--sourcemap true` so the generated profile reports can point back to source; +the normal CodSpeed benchmark builds are unchanged. Local aggregate scripts run +with `--parallel=1`, and scenario `test:flame` targets opt out of Nx parallelism +so profiling workloads do not overlap and bias each other. The Vitest aggregate +configs also set `fileParallelism: false` so benchmark files run sequentially +inside `test:perf:react`. + +```bash +pnpm benchmark:memory:server:flame +pnpm benchmark:memory:client:flame +pnpm benchmark:memory:server:flame:solid +pnpm benchmark:memory:client:flame:solid +pnpm benchmark:memory:server:flame:vue +pnpm benchmark:memory:client:flame:vue +``` + +To profile one scenario, run its `test:flame` target directly: + +```bash +pnpm nx run @benchmarks/memory-server-request-churn-react:test:flame --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client-navigation-churn-react:test:flame --outputStyle=stream --skipRemoteCache +``` + +Flame writes reports under the scenario's ignored `.profiles//` +directory, including `heap-profile-*.html` and `heap-profile-*.md`. The +`memory.flame.ts` entrypoints run the same workload shape as `memory.bench.ts` +but manually start profiling after sanity/setup work and stop it after the +measured workload. Treat these profiles as diagnostic heap-sampling attribution; +they are not CodSpeed memory metrics such as peak memory, allocated bytes, or +allocation counts. The heap sampler is stopped before profile conversion and +Flame report generation, so Flame/pprof report-generation work should not appear +as part of the captured workload. Flame runs do not force GC before profiling; +doing so would perturb the workload and still would not make heap sampling +equivalent to CodSpeed memory metrics. + +Clean local Flame profile output with: + +```bash +pnpm --filter @benchmarks/memory-server clean:profiles +pnpm --filter @benchmarks/memory-client clean:profiles +``` + +Client memory benches are useful for regression tracking of router/React/jsdom +integration behavior, especially retained route/cache data. They are not pure +browser-memory measurements, and local Flame attribution can include jsdom, +React DOM, and profiler shutdown frames. + +Real memory measurement, locally (requires the CodSpeed CLI, `codspeed setup` +once to install the memory executor, and sudo; **uploads results to the +CodSpeed dashboard** — local runs do not affect PR baselines): + +```bash +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-server:test:perf:react +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-server:test:perf:solid +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-server:test:perf:vue +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-client:test:perf:react +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-client:test:perf:solid +WITH_INSTRUMENTATION=1 codspeed run --mode memory -- pnpm nx run @benchmarks/memory-client:test:perf:vue +``` diff --git a/benchmarks/memory/client/bench-utils.ts b/benchmarks/memory/client/bench-utils.ts new file mode 100644 index 0000000000..0cbaedbd71 --- /dev/null +++ b/benchmarks/memory/client/bench-utils.ts @@ -0,0 +1,20 @@ +export const memoryBenchOptions = { + iterations: 1, + warmupIterations: 1, + time: 0, + warmupTime: 0, + throws: true, +} + +export function createDeterministicRandom(seed: number) { + let state = seed >>> 0 + + return () => { + state = (state * 1664525 + 1013904223) >>> 0 + return state / 0x100000000 + } +} + +export function randomSegment(random: () => number) { + return Math.floor(random() * 1_000_000_000).toString(36) +} diff --git a/benchmarks/memory/client/benchmark.ts b/benchmarks/memory/client/benchmark.ts new file mode 100644 index 0000000000..8b877b0740 --- /dev/null +++ b/benchmarks/memory/client/benchmark.ts @@ -0,0 +1,7 @@ +export interface ClientMemoryWorkload { + name: string + before?: () => Promise | void + run: () => Promise | void + sanity: () => Promise | void + after?: () => Promise | void +} diff --git a/benchmarks/memory/client/flame-runner.ts b/benchmarks/memory/client/flame-runner.ts new file mode 100644 index 0000000000..04941fda67 --- /dev/null +++ b/benchmarks/memory/client/flame-runner.ts @@ -0,0 +1,14 @@ +import { profileFlameWorkload } from '../flame-control.ts' +import { window } from './jsdom.ts' +import type { ClientMemoryWorkload } from './benchmark.ts' + +export async function runClientFlameBenchmark(workload: ClientMemoryWorkload) { + try { + await workload.sanity() + await workload.before?.() + await profileFlameWorkload(workload.run, workload.name) + } finally { + await workload.after?.() + window.close() + } +} diff --git a/benchmarks/memory/client/jsdom.ts b/benchmarks/memory/client/jsdom.ts new file mode 100644 index 0000000000..4df480df68 --- /dev/null +++ b/benchmarks/memory/client/jsdom.ts @@ -0,0 +1,51 @@ +import { JSDOM } from 'jsdom' + +const dom = new JSDOM('', { + url: 'http://localhost/', +}) + +const { window } = dom + +function setGlobal(name: string, value: unknown) { + Object.defineProperty(globalThis, name, { + value, + configurable: true, + writable: true, + }) +} + +setGlobal('window', window) +setGlobal('document', window.document) +setGlobal('self', window) +setGlobal('navigator', window.navigator) +setGlobal('location', window.location) +setGlobal('history', window.history) +setGlobal('HTMLElement', window.HTMLElement) +setGlobal('Element', window.Element) +setGlobal('SVGElement', window.SVGElement) +setGlobal('DocumentFragment', window.DocumentFragment) +setGlobal('Node', window.Node) +setGlobal('MouseEvent', window.MouseEvent) +setGlobal('MutationObserver', window.MutationObserver) +setGlobal('sessionStorage', window.sessionStorage) +setGlobal('localStorage', window.localStorage) +setGlobal('getComputedStyle', window.getComputedStyle.bind(window)) + +setGlobal( + 'requestAnimationFrame', + window.requestAnimationFrame?.bind(window) ?? + ((callback: (time: number) => void) => + setTimeout(() => callback(performance.now()), 16)), +) + +setGlobal( + 'cancelAnimationFrame', + window.cancelAnimationFrame?.bind(window) ?? + ((handle: number) => clearTimeout(handle)), +) + +const scrollTo = () => {} +window.scrollTo = scrollTo +setGlobal('scrollTo', scrollTo) + +export { window } diff --git a/benchmarks/memory/client/lifecycle.ts b/benchmarks/memory/client/lifecycle.ts new file mode 100644 index 0000000000..c4a62f0f96 --- /dev/null +++ b/benchmarks/memory/client/lifecycle.ts @@ -0,0 +1,46 @@ +export type Framework = 'react' | 'solid' | 'vue' + +export type MountedApp = { + router: unknown + unmount: () => void +} + +export type MountTestApp = (container: HTMLDivElement) => MountedApp + +const frameworkNames = { + react: 'React', + solid: 'Solid', + vue: 'Vue', +} satisfies Record + +export function noop() {} + +export function warnClientMemoryDevMode(framework: Framework) { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `memory client benchmark is running without NODE_ENV=production; ${frameworkNames[framework]} dev overhead will dominate results.`, + ) + } +} + +export function createBenchContainer() { + const container = document.createElement('div') + document.body.append(container) + + return container +} + +export function removeBenchContainer(container: HTMLDivElement | undefined) { + container?.remove() +} + +export function nextAnimationFrame() { + return new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) +} + +export async function drainMicrotasks() { + await Promise.resolve() + await Promise.resolve() +} diff --git a/benchmarks/memory/client/package.json b/benchmarks/memory/client/package.json new file mode 100644 index 0000000000..2624b9e4e5 --- /dev/null +++ b/benchmarks/memory/client/package.json @@ -0,0 +1,260 @@ +{ + "name": "@benchmarks/memory-client", + "private": true, + "type": "module", + "scripts": { + "clean:profiles": "rm -rf scenarios/*/*/.profiles" + }, + "imports": { + "#memory-client/benchmark": "./benchmark.ts", + "#memory-client/bench-utils": "./bench-utils.ts", + "#memory-client/flame-runner": "./flame-runner.ts", + "#memory-client/lifecycle": "./lifecycle.ts" + }, + "dependencies": { + "@tanstack/react-router": "workspace:*", + "@tanstack/router-core": "workspace:*", + "@tanstack/solid-router": "workspace:*", + "@tanstack/vue-router": "workspace:*", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "solid-js": "^1.9.10", + "vue": "^3.5.16" + }, + "devDependencies": { + "@codspeed/vitest-plugin": "^5.5.0", + "@datadog/pprof": "^5.13.2", + "@platformatic/flame": "^1.6.0", + "@testing-library/react": "^16.2.0", + "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-vue": "^6.0.5", + "@vitejs/plugin-vue-jsx": "^5.1.5", + "@types/jsdom": "28.0.0", + "jsdom": "29.1.1", + "typescript": "^6.0.2", + "vite": "^8.0.14", + "vite-plugin-solid": "^2.11.11", + "vitest": "^4.1.4" + }, + "nx": { + "targets": { + "build:react": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-react", + "@benchmarks/memory-client-unique-location-churn-react", + "@benchmarks/memory-client-preload-churn-react", + "@benchmarks/memory-client-loader-data-retention-react", + "@benchmarks/memory-client-mount-unmount-react", + "@benchmarks/memory-client-interrupted-navigations-react" + ], + "target": "build:client" + } + ] + }, + "build:solid": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-solid", + "@benchmarks/memory-client-unique-location-churn-solid", + "@benchmarks/memory-client-preload-churn-solid", + "@benchmarks/memory-client-loader-data-retention-solid", + "@benchmarks/memory-client-mount-unmount-solid", + "@benchmarks/memory-client-interrupted-navigations-solid" + ], + "target": "build:client" + } + ] + }, + "build:vue": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-vue", + "@benchmarks/memory-client-unique-location-churn-vue", + "@benchmarks/memory-client-preload-churn-vue", + "@benchmarks/memory-client-loader-data-retention-vue", + "@benchmarks/memory-client-mount-unmount-vue", + "@benchmarks/memory-client-interrupted-navigations-vue" + ], + "target": "build:client" + } + ] + }, + "build:react:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-react", + "@benchmarks/memory-client-unique-location-churn-react", + "@benchmarks/memory-client-preload-churn-react", + "@benchmarks/memory-client-loader-data-retention-react", + "@benchmarks/memory-client-mount-unmount-react", + "@benchmarks/memory-client-interrupted-navigations-react" + ], + "target": "build:client:flame" + } + ] + }, + "build:solid:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-solid", + "@benchmarks/memory-client-unique-location-churn-solid", + "@benchmarks/memory-client-preload-churn-solid", + "@benchmarks/memory-client-loader-data-retention-solid", + "@benchmarks/memory-client-mount-unmount-solid", + "@benchmarks/memory-client-interrupted-navigations-solid" + ], + "target": "build:client:flame" + } + ] + }, + "build:vue:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-vue", + "@benchmarks/memory-client-unique-location-churn-vue", + "@benchmarks/memory-client-preload-churn-vue", + "@benchmarks/memory-client-loader-data-retention-vue", + "@benchmarks/memory-client-mount-unmount-vue", + "@benchmarks/memory-client-interrupted-navigations-vue" + ], + "target": "build:client:flame" + } + ] + }, + "test:flame:react": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-react", + "@benchmarks/memory-client-unique-location-churn-react", + "@benchmarks/memory-client-preload-churn-react", + "@benchmarks/memory-client-loader-data-retention-react", + "@benchmarks/memory-client-mount-unmount-react", + "@benchmarks/memory-client-interrupted-navigations-react" + ], + "target": "test:flame" + } + ] + }, + "test:flame:solid": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-solid", + "@benchmarks/memory-client-unique-location-churn-solid", + "@benchmarks/memory-client-preload-churn-solid", + "@benchmarks/memory-client-loader-data-retention-solid", + "@benchmarks/memory-client-mount-unmount-solid", + "@benchmarks/memory-client-interrupted-navigations-solid" + ], + "target": "test:flame" + } + ] + }, + "test:flame:vue": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-vue", + "@benchmarks/memory-client-unique-location-churn-vue", + "@benchmarks/memory-client-preload-churn-vue", + "@benchmarks/memory-client-loader-data-retention-vue", + "@benchmarks/memory-client-mount-unmount-vue", + "@benchmarks/memory-client-interrupted-navigations-vue" + ], + "target": "test:flame" + } + ] + }, + "test:perf:react": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:react" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.react.config.ts", + "cwd": "benchmarks/memory/client" + } + }, + "test:perf:solid": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:solid" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.solid.config.ts", + "cwd": "benchmarks/memory/client" + } + }, + "test:perf:vue": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:vue" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.vue.config.ts", + "cwd": "benchmarks/memory/client" + } + }, + "test:types": { + "executor": "nx:noop", + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-client-navigation-churn-react", + "@benchmarks/memory-client-unique-location-churn-react", + "@benchmarks/memory-client-preload-churn-react", + "@benchmarks/memory-client-loader-data-retention-react", + "@benchmarks/memory-client-mount-unmount-react", + "@benchmarks/memory-client-interrupted-navigations-react", + "@benchmarks/memory-client-navigation-churn-solid", + "@benchmarks/memory-client-unique-location-churn-solid", + "@benchmarks/memory-client-preload-churn-solid", + "@benchmarks/memory-client-loader-data-retention-solid", + "@benchmarks/memory-client-mount-unmount-solid", + "@benchmarks/memory-client-interrupted-navigations-solid", + "@benchmarks/memory-client-navigation-churn-vue", + "@benchmarks/memory-client-unique-location-churn-vue", + "@benchmarks/memory-client-preload-churn-vue", + "@benchmarks/memory-client-loader-data-retention-vue", + "@benchmarks/memory-client-mount-unmount-vue", + "@benchmarks/memory-client-interrupted-navigations-vue" + ], + "target": "test:types:client" + } + ] + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json new file mode 100644 index 0000000000..0ed437c9ca --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-interrupted-navigations-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts new file mode 100644 index 0000000000..6e072ad5f3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts @@ -0,0 +1,19 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} = (await import(/* @vite-ignore */ appModulePath)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx new file mode 100644 index 0000000000..bd605a78ee --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx @@ -0,0 +1,37 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export { + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} from '../../slow-loaders' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..475085a433 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as FastIdRouteImport } from './routes/fast.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FastIdRoute = FastIdRouteImport.update({ + id: '/fast/$id', + path: '/fast/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/fast/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/fast/$id' | '/slow/$id' + id: '__root__' | '/' | '/fast/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + FastIdRoute: typeof FastIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/fast/$id': { + id: '/fast/$id' + path: '/fast/$id' + fullPath: '/fast/$id' + preLoaderRoute: typeof FastIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + FastIdRoute: FastIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx new file mode 100644 index 0000000000..ac27b20f79 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx new file mode 100644 index 0000000000..d5fd7ade46 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/react-router' +import { fixedTimestamp } from '../../../slow-loaders' + +export const Route = createFileRoute('/fast/$id')({ + loader: ({ params }) => ({ + id: params.id, + kind: 'fast' as const, + ts: fixedTimestamp, + }), + component: FastComponent, +}) + +function FastComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.kind}:${data.id}:${data.ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx new file mode 100644 index 0000000000..a425ac79c4 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
shell
+} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..61bbf328b7 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/react-router' +import { getSlowLoaderDeferred } from '../../../slow-loaders' + +export const Route = createFileRoute('/slow/$id')({ + loader: async ({ params }) => { + const deferred = getSlowLoaderDeferred(params.id) + + return await deferred.promise + }, + component: SlowComponent, +}) + +function SlowComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.kind}:${data.id}:${data.ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts new file mode 100644 index 0000000000..048a9cc833 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client interrupted-navigations (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts new file mode 100644 index 0000000000..0e01e92e08 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts @@ -0,0 +1,320 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' +import { + createBenchContainer, + drainMicrotasks, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type NavigationSettlement = + | { + status: 'fulfilled' + value: void + } + | { + status: 'rejected' + reason: unknown + } + +type ResolveAllSlowLoaders = () => void +type ResolveSlowLoader = (id: string) => void +type SlowLoaderRegistry = { + has: (id: string) => boolean +} + +type RenderedEvent = { + toLocation: { + pathname: string + } +} + +type InterruptedNavigationRouter = { + latestLoadPromise: Promise | undefined + load: () => Promise + navigate: (options: { + to: '/fast/$id' | '/slow/$id' + params: { id: string } + replace: true + }) => Promise + subscribe: ( + event: 'onRendered', + listener: (event: RenderedEvent) => void, + ) => () => void +} + +const interruptedNavigationIterations = 150 +const interruptedNavigationPairs = createInterruptedNavigationPairs( + interruptedNavigationIterations, +) + +const uninitialized = () => + Promise.reject( + new Error('interrupted-navigations benchmark is not initialized'), + ) + +const uninitializedSettlement = () => + Promise.resolve({ + status: 'rejected', + reason: new Error('interrupted-navigations benchmark is not initialized'), + }) + +function createInterruptedNavigationPairs(iterations: number) { + const random = createDeterministicRandom(13) + + return Array.from({ length: iterations }, (_, index) => ({ + slowId: `slow-${index}-${randomSegment(random)}`, + fastId: `fast-${index}-${randomSegment(random)}`, + })) +} + +function formatReason(reason: unknown) { + if (reason instanceof Error) { + return `${reason.name}: ${reason.message}` + } + + return String(reason) +} + +function assertSlowNavigationSettlement(settlement: NavigationSettlement) { + if (settlement.status === 'fulfilled') { + if (settlement.value !== undefined) { + throw new Error('Expected slow navigation to fulfill with undefined') + } + + return + } + + if ( + reasonHasAbortShape(settlement.reason) || + reasonHasCancellationShape(settlement.reason) + ) { + return + } + + throw new Error( + `Expected slow navigation to settle as void or cancellation, got ${formatReason( + settlement.reason, + )}`, + ) +} + +async function awaitExpectedLoadSettlement(loadPromise: Promise) { + try { + await loadPromise + } catch (reason) { + if (reasonHasAbortShape(reason) || reasonHasCancellationShape(reason)) { + return + } + + throw reason + } +} + +function reasonHasAbortShape(reason: unknown) { + return reason instanceof DOMException && reason.name === 'AbortError' +} + +function reasonHasCancellationShape(reason: unknown) { + return ( + reason instanceof Error && + (reason.name === 'AbortError' || reason.name === 'CancelledError') + ) +} + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, + resolveAllSlowLoaders: ResolveAllSlowLoaders, + resolveSlowLoader: ResolveSlowLoader, + slowLoaderRegistry: SlowLoaderRegistry, +) { + warnClientMemoryDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop + let expectedRenderedPath: string | undefined = undefined + let navigateFast: (id: string) => Promise = uninitialized + let startSlowNavigation: (id: string) => Promise = + uninitializedSettlement + let getLatestLoadPromise: () => Promise | undefined = () => undefined + + function assertRenderedPage(page: 'shell' | 'fast', id?: string) { + const element = container?.querySelector('[data-bench-page]') + const actualPage = element?.dataset.benchPage + const actualId = element?.dataset.benchId + + if (actualPage !== page) { + throw new Error(`Expected rendered page ${page}, got ${actualPage}`) + } + + if (id !== undefined && actualId !== id) { + throw new Error(`Expected rendered id ${id}, got ${actualId}`) + } + } + + async function waitForRenderedPage(page: 'shell' | 'fast', id?: string) { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedPage(page, id) + return + } catch { + await nextAnimationFrame() + } + } + + assertRenderedPage(page, id) + } + + async function waitForSlowLoader(id: string) { + for (let attempt = 0; attempt < 20; attempt++) { + if (slowLoaderRegistry.has(id)) { + return + } + + await drainMicrotasks() + } + + throw new Error(`Slow loader was not registered for id: ${id}`) + } + + function waitForNextRender(pathname: string) { + expectedRenderedPath = pathname + + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = createBenchContainer() + + const mounted = mountTestApp(container) + const router = mounted.router as InterruptedNavigationRouter + unmount = mounted.unmount + getLatestLoadPromise = () => router.latestLoadPromise + + unsub = router.subscribe('onRendered', (event) => { + if ( + expectedRenderedPath && + event.toLocation.pathname !== expectedRenderedPath + ) { + return + } + + const resolve = resolveRendered + resolveRendered = noop + expectedRenderedPath = undefined + resolve() + }) + + navigateFast = async (id) => { + const rendered = waitForNextRender(`/fast/${id}`) + await Promise.all([ + router.navigate({ + to: '/fast/$id', + params: { id }, + replace: true, + }), + rendered, + ]) + } + + startSlowNavigation = (id) => { + const navigation = router.navigate({ + to: '/slow/$id', + params: { id }, + replace: true, + }) + + return navigation + .then((value): NavigationSettlement => ({ status: 'fulfilled', value })) + .catch( + (reason: unknown): NavigationSettlement => ({ + status: 'rejected', + reason, + }), + ) + } + + await router.load() + await waitForRenderedPage('shell') + } + + function after() { + resolveAllSlowLoaders() + unmount() + removeBenchContainer(container) + unsub() + + container = undefined + unmount = noop + unsub = noop + resolveRendered = noop + expectedRenderedPath = undefined + navigateFast = uninitialized + startSlowNavigation = uninitializedSettlement + getLatestLoadPromise = () => undefined + } + + async function interrupt( + slowId: string, + fastId: string, + assertSettlement = true, + ) { + const slowNavigation = startSlowNavigation(slowId) + + await waitForSlowLoader(slowId) + const slowLoadPromise = getLatestLoadPromise() + + if (!slowLoadPromise) { + throw new Error(`Slow navigation did not start a load for id: ${slowId}`) + } + + await navigateFast(fastId) + resolveSlowLoader(slowId) + + const settlement = await slowNavigation + + if (assertSettlement) { + assertSlowNavigationSettlement(settlement) + } + + await awaitExpectedLoadSettlement(slowLoadPromise) + await drainMicrotasks() + } + + return { + name: `mem interrupted-navigations (${framework})`, + before, + interrupt, + async run() { + for (const pair of interruptedNavigationPairs) { + await interrupt(pair.slowId, pair.fastId) + } + }, + async sanity() { + await before() + + try { + assertRenderedPage('shell') + await interrupt('sanity-slow', 'sanity-fast', false) + assertRenderedPage('fast', 'sanity-fast') + } finally { + after() + } + }, + after, + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/slow-loaders.ts new file mode 100644 index 0000000000..c70ccac3da --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/slow-loaders.ts @@ -0,0 +1,54 @@ +export const fixedTimestamp = 1_700_000_000_000 + +type SlowLoaderPayload = { + id: string + kind: 'slow' + ts: number +} + +type SlowLoaderDeferred = { + promise: Promise + resolve: () => void +} + +export const slowLoaderRegistry = new Map() + +export function getSlowLoaderDeferred(id: string) { + const existing = slowLoaderRegistry.get(id) + + if (existing) { + return existing + } + + let resolvePromise!: (payload: SlowLoaderPayload) => void + const promise = new Promise((resolve) => { + resolvePromise = resolve + }) + + const deferred: SlowLoaderDeferred = { + promise, + resolve() { + slowLoaderRegistry.delete(id) + resolvePromise({ id, kind: 'slow', ts: fixedTimestamp }) + }, + } + + slowLoaderRegistry.set(id, deferred) + return deferred +} + +export function resolveSlowLoader(id: string) { + const deferred = slowLoaderRegistry.get(id) + + if (!deferred) { + throw new Error(`No pending slow loader for id: ${id}`) + } + + deferred.resolve() +} + +export function resolveAllSlowLoaders() { + for (const id of Array.from(slowLoaderRegistry.keys())) { + resolveSlowLoader(id) + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json new file mode 100644 index 0000000000..4aff39500e --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-interrupted-navigations-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts new file mode 100644 index 0000000000..e736111663 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts @@ -0,0 +1,19 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} = (await import(/* @vite-ignore */ appModulePath)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx new file mode 100644 index 0000000000..6288b5fab1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx @@ -0,0 +1,35 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from 'solid-js/web' +import { getRouter } from './router' + +export { + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} from '../../slow-loaders' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..f42255963c --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as FastIdRouteImport } from './routes/fast.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FastIdRoute = FastIdRouteImport.update({ + id: '/fast/$id', + path: '/fast/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/fast/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/fast/$id' | '/slow/$id' + id: '__root__' | '/' | '/fast/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + FastIdRoute: typeof FastIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/fast/$id': { + id: '/fast/$id' + path: '/fast/$id' + fullPath: '/fast/$id' + preLoaderRoute: typeof FastIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + FastIdRoute: FastIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx new file mode 100644 index 0000000000..884aa3438f --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx new file mode 100644 index 0000000000..511e4712b7 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { fixedTimestamp } from '../../../slow-loaders' + +export const Route = createFileRoute('/fast/$id')({ + loader: ({ params }) => ({ + id: params.id, + kind: 'fast' as const, + ts: fixedTimestamp, + }), + component: FastComponent, +}) + +function FastComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data().kind}:${data().id}:${data().ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx new file mode 100644 index 0000000000..592b2d666e --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
shell
+} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..9aaaef5568 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { getSlowLoaderDeferred } from '../../../slow-loaders' + +export const Route = createFileRoute('/slow/$id')({ + loader: async ({ params }) => { + const deferred = getSlowLoaderDeferred(params.id) + + return await deferred.promise + }, + component: SlowComponent, +}) + +function SlowComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data().kind}:${data().id}:${data().ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json new file mode 100644 index 0000000000..b12dcb7ade --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts new file mode 100644 index 0000000000..e710e0fd12 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client interrupted-navigations (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json new file mode 100644 index 0000000000..01288cb235 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-interrupted-navigations-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts new file mode 100644 index 0000000000..260e45b64b --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts @@ -0,0 +1,19 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} = (await import(/* @vite-ignore */ appModulePath)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx new file mode 100644 index 0000000000..1abb4410dc --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx @@ -0,0 +1,42 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export { + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} from '../../slow-loaders' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..5ea61a89a3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as SlowIdRouteImport } from './routes/slow.$id' +import { Route as FastIdRouteImport } from './routes/fast.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const SlowIdRoute = SlowIdRouteImport.update({ + id: '/slow/$id', + path: '/slow/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FastIdRoute = FastIdRouteImport.update({ + id: '/fast/$id', + path: '/fast/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/fast/$id': typeof FastIdRoute + '/slow/$id': typeof SlowIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/fast/$id' | '/slow/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/fast/$id' | '/slow/$id' + id: '__root__' | '/' | '/fast/$id' | '/slow/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + FastIdRoute: typeof FastIdRoute + SlowIdRoute: typeof SlowIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/slow/$id': { + id: '/slow/$id' + path: '/slow/$id' + fullPath: '/slow/$id' + preLoaderRoute: typeof SlowIdRouteImport + parentRoute: typeof rootRouteImport + } + '/fast/$id': { + id: '/fast/$id' + path: '/fast/$id' + fullPath: '/fast/$id' + preLoaderRoute: typeof FastIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + FastIdRoute: FastIdRoute, + SlowIdRoute: SlowIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx new file mode 100644 index 0000000000..4655fe8424 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx new file mode 100644 index 0000000000..3bd820bd07 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { fixedTimestamp } from '../../../slow-loaders' + +export const Route = createFileRoute('/fast/$id')({ + loader: ({ params }: { params: { id: string } }) => ({ + id: params.id, + kind: 'fast' as const, + ts: fixedTimestamp, + }), + component: FastComponent, +}) + +function FastComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.value.kind}:${data.value.id}:${data.value.ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx new file mode 100644 index 0000000000..c30a51e1e5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
shell
+} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx new file mode 100644 index 0000000000..68885fb918 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { getSlowLoaderDeferred } from '../../../slow-loaders' + +export const Route = createFileRoute('/slow/$id')({ + loader: async ({ params }: { params: { id: string } }) => { + const deferred = getSlowLoaderDeferred(params.id) + + return await deferred.promise + }, + component: SlowComponent, +}) + +function SlowComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.value.kind}:${data.value.id}:${data.value.ts}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts new file mode 100644 index 0000000000..b2912ce9d1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client interrupted-navigations (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/loader-data.ts new file mode 100644 index 0000000000..3bd59c82ef --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/loader-data.ts @@ -0,0 +1,48 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' + +export const loaderPayloadRecordCount = 512 + +const loaderPayloadCharsPerRecord = 1024 + +type LoaderRecord = { + key: string + value: string +} + +export function createLoaderData(id: string) { + const random = createDeterministicRandom(hashId(id)) + const records: Array = [] + + for (let index = 0; index < loaderPayloadRecordCount; index++) { + records.push({ + key: `${id}:${index}:${randomSegment(random)}`, + value: createRecordValue(id, index, random), + }) + } + + return { id, records } +} + +function createRecordValue(id: string, index: number, random: () => number) { + const firstSegment = randomSegment(random) + const secondSegment = randomSegment(random) + const segment = `${id}:${index}:${firstSegment}:${secondSegment}:` + + return segment + .repeat(Math.ceil(loaderPayloadCharsPerRecord / segment.length)) + .slice(0, loaderPayloadCharsPerRecord) +} + +function hashId(id: string) { + let hash = 2166136261 + + for (let index = 0; index < id.length; index++) { + hash ^= id.charCodeAt(index) + hash = Math.imul(hash, 16777619) + } + + return hash >>> 0 +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json new file mode 100644 index 0000000000..713038e20f --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-loader-data-retention-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts new file mode 100644 index 0000000000..fafaf13e6f --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { loaderPayloadRecordCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, + loaderPayloadRecordCount, +) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx new file mode 100644 index 0000000000..5eef150c2c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx @@ -0,0 +1,33 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export { loaderPayloadRecordCount } from '../../loader-data' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..ea96d809c8 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts @@ -0,0 +1,102 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ShellRouteImport } from './routes/shell' +import { Route as PageIdRouteImport } from './routes/page.$id' +import { Route as ShellIndexRouteImport } from './routes/shell.index' + +const ShellRoute = ShellRouteImport.update({ + id: '/shell', + path: '/shell', + getParentRoute: () => rootRouteImport, +} as any) +const PageIdRoute = PageIdRouteImport.update({ + id: '/page/$id', + path: '/page/$id', + getParentRoute: () => rootRouteImport, +} as any) +const ShellIndexRoute = ShellIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShellRoute, +} as any) + +export interface FileRoutesByFullPath { + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRoutesByTo { + '/page/$id': typeof PageIdRoute + '/shell': typeof ShellIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/shell' | '/page/$id' | '/shell/' + fileRoutesByTo: FileRoutesByTo + to: '/page/$id' | '/shell' + id: '__root__' | '/shell' | '/page/$id' | '/shell/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ShellRoute: typeof ShellRouteWithChildren + PageIdRoute: typeof PageIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/shell': { + id: '/shell' + path: '/shell' + fullPath: '/shell' + preLoaderRoute: typeof ShellRouteImport + parentRoute: typeof rootRouteImport + } + '/page/$id': { + id: '/page/$id' + path: '/page/$id' + fullPath: '/page/$id' + preLoaderRoute: typeof PageIdRouteImport + parentRoute: typeof rootRouteImport + } + '/shell/': { + id: '/shell/' + path: '/' + fullPath: '/shell/' + preLoaderRoute: typeof ShellIndexRouteImport + parentRoute: typeof ShellRoute + } + } +} + +interface ShellRouteChildren { + ShellIndexRoute: typeof ShellIndexRoute +} + +const ShellRouteChildren: ShellRouteChildren = { + ShellIndexRoute: ShellIndexRoute, +} + +const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ShellRoute: ShellRouteWithChildren, + PageIdRoute: PageIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx new file mode 100644 index 0000000000..5a5c8210d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx @@ -0,0 +1,19 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/shell'], + }), + routeTree, + defaultGcTime: 0, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx new file mode 100644 index 0000000000..63af9ed429 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createLoaderData } from '../../../loader-data' + +export const Route = createFileRoute('/page/$id')({ + loader: ({ params }) => createLoaderData(params.id), + component: PageComponent, +}) + +function PageComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.id}:${data.records.length}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx new file mode 100644 index 0000000000..d78bb8bfb9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/shell/')({ + component: ShellIndexComponent, +}) + +function ShellIndexComponent() { + return
ready
+} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx new file mode 100644 index 0000000000..024a58c243 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/shell')({ + component: ShellComponent, +}) + +function ShellComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts new file mode 100644 index 0000000000..555e5f5612 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client loader-data-retention (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts new file mode 100644 index 0000000000..a5c496cb81 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts @@ -0,0 +1,187 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' +import { + createBenchContainer, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type RenderEvent = { + toLocation: { + pathname: string + } +} + +type LoaderDataRouter = { + load: () => Promise + navigate: (options: { + to: '/page/$id' + params: { id: string } + replace: true + }) => Promise + subscribe: ( + event: 'onRendered', + listener: (event: RenderEvent) => void, + ) => () => void +} + +const loaderDataRetentionNavigationCount = 20 +const pageIds = createPageIds() + +const uninitialized = () => + Promise.reject( + new Error('loader-data-retention benchmark is not initialized'), + ) + +function createPageIds() { + const random = createDeterministicRandom(11) + + return Array.from( + { length: loaderDataRetentionNavigationCount }, + (_, index) => `${index}-${randomSegment(random)}`, + ) +} + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, + loaderPayloadRecordCount: number, +) { + warnClientMemoryDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop + let expectedRenderedPath: string | undefined = undefined + let navigateTo: (id: string) => Promise = uninitialized + + function assertRenderedShell() { + const actual = + container?.querySelector('[data-bench-page]')?.dataset + .benchPage + + if (actual !== 'shell') { + throw new Error(`Expected rendered shell page, got ${actual}`) + } + } + + function assertRenderedPage(id: string) { + const page = container?.querySelector( + '[data-bench-page="page"]', + ) + const actualId = page?.dataset.benchId + const actualCount = page?.dataset.benchCount + const expectedCount = String(loaderPayloadRecordCount) + + if (actualId !== id || actualCount !== expectedCount) { + throw new Error( + `Expected rendered page ${id}:${expectedCount}, got ${actualId}:${actualCount}`, + ) + } + } + + async function waitForRenderedShell() { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedShell() + return + } catch { + await nextAnimationFrame() + } + } + + assertRenderedShell() + } + + function waitForNextRender(pathname: string) { + expectedRenderedPath = pathname + + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = createBenchContainer() + + const mounted = mountTestApp(container) + const router = mounted.router as LoaderDataRouter + unmount = mounted.unmount + + unsub = router.subscribe('onRendered', (event) => { + if ( + expectedRenderedPath && + event.toLocation.pathname !== expectedRenderedPath + ) { + return + } + + const resolve = resolveRendered + resolveRendered = noop + expectedRenderedPath = undefined + resolve() + }) + + navigateTo = async (id) => { + const pathname = `/page/${id}` + const rendered = waitForNextRender(pathname) + + await router.navigate({ + to: '/page/$id', + params: { id }, + replace: true, + }) + await rendered + assertRenderedPage(id) + } + + await router.load() + await waitForRenderedShell() + } + + function after() { + unmount() + removeBenchContainer(container) + unsub() + + container = undefined + unmount = noop + unsub = noop + resolveRendered = noop + expectedRenderedPath = undefined + navigateTo = uninitialized + } + + return { + name: `mem loader-data-retention (${framework})`, + before, + navigate: (id: string) => navigateTo(id), + async run() { + for (const id of pageIds) { + await navigateTo(id) + } + }, + async sanity() { + await before() + + try { + assertRenderedShell() + await navigateTo('sanity-a') + assertRenderedPage('sanity-a') + } finally { + after() + } + }, + after, + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json new file mode 100644 index 0000000000..6c85f51896 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-loader-data-retention-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts new file mode 100644 index 0000000000..b7c80a3e49 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { loaderPayloadRecordCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, + loaderPayloadRecordCount, +) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx new file mode 100644 index 0000000000..4315a4b15c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx @@ -0,0 +1,31 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from 'solid-js/web' +import { getRouter } from './router' + +export { loaderPayloadRecordCount } from '../../loader-data' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..a1946d187d --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts @@ -0,0 +1,102 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ShellRouteImport } from './routes/shell' +import { Route as ShellIndexRouteImport } from './routes/shell.index' +import { Route as PageIdRouteImport } from './routes/page.$id' + +const ShellRoute = ShellRouteImport.update({ + id: '/shell', + path: '/shell', + getParentRoute: () => rootRouteImport, +} as any) +const ShellIndexRoute = ShellIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShellRoute, +} as any) +const PageIdRoute = PageIdRouteImport.update({ + id: '/page/$id', + path: '/page/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRoutesByTo { + '/page/$id': typeof PageIdRoute + '/shell': typeof ShellIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/shell' | '/page/$id' | '/shell/' + fileRoutesByTo: FileRoutesByTo + to: '/page/$id' | '/shell' + id: '__root__' | '/shell' | '/page/$id' | '/shell/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ShellRoute: typeof ShellRouteWithChildren + PageIdRoute: typeof PageIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/shell': { + id: '/shell' + path: '/shell' + fullPath: '/shell' + preLoaderRoute: typeof ShellRouteImport + parentRoute: typeof rootRouteImport + } + '/shell/': { + id: '/shell/' + path: '/' + fullPath: '/shell/' + preLoaderRoute: typeof ShellIndexRouteImport + parentRoute: typeof ShellRoute + } + '/page/$id': { + id: '/page/$id' + path: '/page/$id' + fullPath: '/page/$id' + preLoaderRoute: typeof PageIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +interface ShellRouteChildren { + ShellIndexRoute: typeof ShellIndexRoute +} + +const ShellRouteChildren: ShellRouteChildren = { + ShellIndexRoute: ShellIndexRoute, +} + +const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ShellRoute: ShellRouteWithChildren, + PageIdRoute: PageIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx new file mode 100644 index 0000000000..06afdb10d3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx @@ -0,0 +1,19 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/shell'], + }), + routeTree, + defaultGcTime: 0, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx new file mode 100644 index 0000000000..fb6c438fa5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx @@ -0,0 +1,21 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createLoaderData } from '../../../loader-data' + +export const Route = createFileRoute('/page/$id')({ + loader: ({ params }) => createLoaderData(params.id), + component: PageComponent, +}) + +function PageComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data().id}:${data().records.length}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx new file mode 100644 index 0000000000..2f4f5daee2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/shell/')({ + component: ShellIndexComponent, +}) + +function ShellIndexComponent() { + return
ready
+} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx new file mode 100644 index 0000000000..c1e38a1aaf --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/shell')({ + component: ShellComponent, +}) + +function ShellComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json new file mode 100644 index 0000000000..b12dcb7ade --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts new file mode 100644 index 0000000000..58d6b77acc --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client loader-data-retention (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json new file mode 100644 index 0000000000..22504c906f --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-loader-data-retention-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts new file mode 100644 index 0000000000..a83a1f70d1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { loaderPayloadRecordCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, + loaderPayloadRecordCount, +) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx new file mode 100644 index 0000000000..c843b4feb5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx @@ -0,0 +1,38 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export { loaderPayloadRecordCount } from '../../loader-data' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..5b09d365e7 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts @@ -0,0 +1,102 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ShellRouteImport } from './routes/shell' +import { Route as PageIdRouteImport } from './routes/page.$id' +import { Route as ShellIndexRouteImport } from './routes/shell.index' + +const ShellRoute = ShellRouteImport.update({ + id: '/shell', + path: '/shell', + getParentRoute: () => rootRouteImport, +} as any) +const PageIdRoute = PageIdRouteImport.update({ + id: '/page/$id', + path: '/page/$id', + getParentRoute: () => rootRouteImport, +} as any) +const ShellIndexRoute = ShellIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => ShellRoute, +} as any) + +export interface FileRoutesByFullPath { + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRoutesByTo { + '/page/$id': typeof PageIdRoute + '/shell': typeof ShellIndexRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/shell': typeof ShellRouteWithChildren + '/page/$id': typeof PageIdRoute + '/shell/': typeof ShellIndexRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/shell' | '/page/$id' | '/shell/' + fileRoutesByTo: FileRoutesByTo + to: '/page/$id' | '/shell' + id: '__root__' | '/shell' | '/page/$id' | '/shell/' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ShellRoute: typeof ShellRouteWithChildren + PageIdRoute: typeof PageIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/shell': { + id: '/shell' + path: '/shell' + fullPath: '/shell' + preLoaderRoute: typeof ShellRouteImport + parentRoute: typeof rootRouteImport + } + '/page/$id': { + id: '/page/$id' + path: '/page/$id' + fullPath: '/page/$id' + preLoaderRoute: typeof PageIdRouteImport + parentRoute: typeof rootRouteImport + } + '/shell/': { + id: '/shell/' + path: '/' + fullPath: '/shell/' + preLoaderRoute: typeof ShellIndexRouteImport + parentRoute: typeof ShellRoute + } + } +} + +interface ShellRouteChildren { + ShellIndexRoute: typeof ShellIndexRoute +} + +const ShellRouteChildren: ShellRouteChildren = { + ShellIndexRoute: ShellIndexRoute, +} + +const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + ShellRoute: ShellRouteWithChildren, + PageIdRoute: PageIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx new file mode 100644 index 0000000000..84286e1dae --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx @@ -0,0 +1,19 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/shell'], + }), + routeTree, + defaultGcTime: 0, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx new file mode 100644 index 0000000000..9f4b3c4470 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createLoaderData } from '../../../loader-data' + +export const Route = createFileRoute('/page/$id')({ + loader: ({ params }: { params: { id: string } }) => + createLoaderData(params.id), + component: PageComponent, +}) + +function PageComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.value.id}:${data.value.records.length}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx new file mode 100644 index 0000000000..640ffeeb5c --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/shell/')({ + component: ShellIndexComponent, +}) + +function ShellIndexComponent() { + return
ready
+} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx new file mode 100644 index 0000000000..f8a598bc34 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/shell')({ + component: ShellComponent, +}) + +function ShellComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts new file mode 100644 index 0000000000..bbf2fd02d8 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client loader-data-retention (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/project.json b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json new file mode 100644 index 0000000000..05d8aaa4bf --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-mount-unmount-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts new file mode 100644 index 0000000000..70603b5c34 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx new file mode 100644 index 0000000000..bdf76eaa24 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx @@ -0,0 +1,29 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..5efcb2cf72 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ARouteImport } from './routes/a' + +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' + fileRoutesByTo: FileRoutesByTo + to: '/a' + id: '__root__' | '/a' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx new file mode 100644 index 0000000000..9cb85c5a1b --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx new file mode 100644 index 0000000000..1e268d3c42 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx @@ -0,0 +1,12 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/a')({ + loader: () => ({ id: 'a' }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return
{data.id}
+} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts new file mode 100644 index 0000000000..bb0f3c2e0a --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client mount-unmount (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/shared.ts b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts new file mode 100644 index 0000000000..4f9b9f430f --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts @@ -0,0 +1,74 @@ +import { + createBenchContainer, + drainMicrotasks, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type RenderRouter = { + load: () => Promise + subscribe: (event: 'onRendered', listener: () => void) => () => void +} + +const mountUnmountIterations = 100 + +function assertEmptyBody() { + if (document.body.childNodes.length !== 0) { + throw new Error( + `Expected document.body to be empty, found ${document.body.childNodes.length} child node(s)`, + ) + } +} + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, +) { + warnClientMemoryDevMode(framework) + + async function cycle() { + const container = createBenchContainer() + + let unmount = noop + let unsubscribe = noop + + try { + const mounted = mountTestApp(container) + const router = mounted.router as RenderRouter + unmount = mounted.unmount + + const rendered = new Promise((resolve) => { + unsubscribe = router.subscribe('onRendered', () => { + resolve() + }) + }) + + await router.load() + await rendered + unsubscribe() + unsubscribe = noop + } finally { + unmount() + removeBenchContainer(container) + unsubscribe() + await drainMicrotasks() + } + } + + return { + name: `mem mount-unmount (${framework})`, + cycle, + async run() { + for (let index = 0; index < mountUnmountIterations; index++) { + await cycle() + } + }, + async sanity() { + assertEmptyBody() + await cycle() + assertEmptyBody() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/project.json b/benchmarks/memory/client/scenarios/mount-unmount/solid/project.json new file mode 100644 index 0000000000..63e8e950db --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-mount-unmount-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts new file mode 100644 index 0000000000..493425c6a2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx new file mode 100644 index 0000000000..b23bc126d5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx @@ -0,0 +1,27 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from 'solid-js/web' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..a3f9739290 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ARouteImport } from './routes/a' + +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' + fileRoutesByTo: FileRoutesByTo + to: '/a' + id: '__root__' | '/a' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx new file mode 100644 index 0000000000..e8c14f12f6 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx new file mode 100644 index 0000000000..36c4be538c --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx @@ -0,0 +1,12 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/a')({ + loader: () => ({ id: 'a' }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return
{data().id}
+} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json new file mode 100644 index 0000000000..b12dcb7ade --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts new file mode 100644 index 0000000000..5f0ab26252 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client mount-unmount (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/project.json b/benchmarks/memory/client/scenarios/mount-unmount/vue/project.json new file mode 100644 index 0000000000..157673cf16 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-mount-unmount-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts new file mode 100644 index 0000000000..a38df04788 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx new file mode 100644 index 0000000000..27b82fcbe1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx @@ -0,0 +1,34 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..1a54b81033 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ARouteImport } from './routes/a' + +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' + fileRoutesByTo: FileRoutesByTo + to: '/a' + id: '__root__' | '/a' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx new file mode 100644 index 0000000000..e083495cd2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx new file mode 100644 index 0000000000..cbcd543ce5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx @@ -0,0 +1,12 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/a')({ + loader: () => ({ id: 'a' }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return
{data.value.id}
+} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts new file mode 100644 index 0000000000..0f7573a408 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client mount-unmount (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/project.json b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json new file mode 100644 index 0000000000..b424277bc6 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-navigation-churn-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts new file mode 100644 index 0000000000..70603b5c34 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx new file mode 100644 index 0000000000..d714e7bfdd --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx @@ -0,0 +1,31 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..3cca81cabb --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as BRouteImport } from './routes/b' +import { Route as ARouteImport } from './routes/a' + +const BRoute = BRouteImport.update({ + id: '/b', + path: '/b', + getParentRoute: () => rootRouteImport, +} as any) +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' | '/b' + fileRoutesByTo: FileRoutesByTo + to: '/a' | '/b' + id: '__root__' | '/a' | '/b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute + BRoute: typeof BRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/b': { + id: '/b' + path: '/b' + fullPath: '/b' + preLoaderRoute: typeof BRouteImport + parentRoute: typeof rootRouteImport + } + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, + BRoute: BRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx new file mode 100644 index 0000000000..9cb85c5a1b --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx new file mode 100644 index 0000000000..bdd32d0b81 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/a')({ + loader: () => ({ name: 'a', ts: fixedTimestamp }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return
{`${data.name}:${data.ts}`}
+} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx new file mode 100644 index 0000000000..05deef729d --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/b')({ + loader: () => ({ name: 'b', ts: fixedTimestamp }), + component: BComponent, +}) + +function BComponent() { + const data = Route.useLoaderData() + + return
{`${data.name}:${data.ts}`}
+} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts new file mode 100644 index 0000000000..b05b999297 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client navigation-churn (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/shared.ts b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts new file mode 100644 index 0000000000..0745de2116 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts @@ -0,0 +1,129 @@ +import { + createBenchContainer, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type Target = '/a' | '/b' + +type NavigationRouter = { + load: () => Promise + navigate: (options: { to: Target; replace: true }) => Promise + subscribe: (event: 'onRendered', listener: () => void) => () => void +} + +const navigationChurnIterations = 300 + +const uninitialized = () => + Promise.reject(new Error('navigation-churn benchmark is not initialized')) + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, +) { + warnClientMemoryDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop + let navigateTo: (target: Target) => Promise = uninitialized + + function assertRenderedPage(target: Target) { + const expected = target.slice(1) + const actual = + container?.querySelector('[data-bench-page]')?.dataset + .benchPage + + if (actual !== expected) { + throw new Error(`Expected rendered page ${expected}, got ${actual}`) + } + } + + async function waitForRenderedPage(target: Target) { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedPage(target) + return + } catch { + await nextAnimationFrame() + } + } + + assertRenderedPage(target) + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = createBenchContainer() + + const mounted = mountTestApp(container) + const router = mounted.router as NavigationRouter + unmount = mounted.unmount + + unsub = router.subscribe('onRendered', () => { + resolveRendered() + }) + + navigateTo = async (target) => { + const rendered = waitForNextRender() + await Promise.all([ + router.navigate({ + to: target, + replace: true, + }), + rendered, + ]) + } + + await router.load() + await waitForRenderedPage('/a') + } + + function after() { + unmount() + removeBenchContainer(container) + unsub() + + container = undefined + unmount = noop + unsub = noop + resolveRendered = noop + navigateTo = uninitialized + } + + return { + name: `mem navigation-churn (${framework})`, + before, + navigate: (target: Target) => navigateTo(target), + async run() { + for (let index = 0; index < navigationChurnIterations; index++) { + await navigateTo(index % 2 === 0 ? '/b' : '/a') + } + }, + async sanity() { + await before() + + try { + assertRenderedPage('/a') + await navigateTo('/b') + assertRenderedPage('/b') + } finally { + after() + } + }, + after, + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/project.json b/benchmarks/memory/client/scenarios/navigation-churn/solid/project.json new file mode 100644 index 0000000000..2c581e6188 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-navigation-churn-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts new file mode 100644 index 0000000000..493425c6a2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx new file mode 100644 index 0000000000..16000c90a1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx @@ -0,0 +1,29 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from 'solid-js/web' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..59d7fad058 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as BRouteImport } from './routes/b' +import { Route as ARouteImport } from './routes/a' + +const BRoute = BRouteImport.update({ + id: '/b', + path: '/b', + getParentRoute: () => rootRouteImport, +} as any) +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' | '/b' + fileRoutesByTo: FileRoutesByTo + to: '/a' | '/b' + id: '__root__' | '/a' | '/b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute + BRoute: typeof BRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/b': { + id: '/b' + path: '/b' + fullPath: '/b' + preLoaderRoute: typeof BRouteImport + parentRoute: typeof rootRouteImport + } + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, + BRoute: BRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx new file mode 100644 index 0000000000..e8c14f12f6 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx new file mode 100644 index 0000000000..0b7838fdbe --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/a')({ + loader: () => ({ name: 'a', ts: fixedTimestamp }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return
{`${data().name}:${data().ts}`}
+} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx new file mode 100644 index 0000000000..ec9b513ba9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/b')({ + loader: () => ({ name: 'b', ts: fixedTimestamp }), + component: BComponent, +}) + +function BComponent() { + const data = Route.useLoaderData() + + return
{`${data().name}:${data().ts}`}
+} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json new file mode 100644 index 0000000000..b12dcb7ade --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts new file mode 100644 index 0000000000..6b6735891a --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client navigation-churn (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/project.json b/benchmarks/memory/client/scenarios/navigation-churn/vue/project.json new file mode 100644 index 0000000000..f96d03d7d3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-navigation-churn-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts new file mode 100644 index 0000000000..a38df04788 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx new file mode 100644 index 0000000000..fad2251cc0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx @@ -0,0 +1,36 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..12c9640358 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as BRouteImport } from './routes/b' +import { Route as ARouteImport } from './routes/a' + +const BRoute = BRouteImport.update({ + id: '/b', + path: '/b', + getParentRoute: () => rootRouteImport, +} as any) +const ARoute = ARouteImport.update({ + id: '/a', + path: '/a', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesByTo { + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/a': typeof ARoute + '/b': typeof BRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/a' | '/b' + fileRoutesByTo: FileRoutesByTo + to: '/a' | '/b' + id: '__root__' | '/a' | '/b' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ARoute: typeof ARoute + BRoute: typeof BRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/b': { + id: '/b' + path: '/b' + fullPath: '/b' + preLoaderRoute: typeof BRouteImport + parentRoute: typeof rootRouteImport + } + '/a': { + id: '/a' + path: '/a' + fullPath: '/a' + preLoaderRoute: typeof ARouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ARoute: ARoute, + BRoute: BRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx new file mode 100644 index 0000000000..e083495cd2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/a'], + }), + routeTree, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx new file mode 100644 index 0000000000..183228e748 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/vue-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/a')({ + loader: () => ({ name: 'a', ts: fixedTimestamp }), + component: AComponent, +}) + +function AComponent() { + const data = Route.useLoaderData() + + return ( +
{`${data.value.name}:${data.value.ts}`}
+ ) +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx new file mode 100644 index 0000000000..3a18ec97a9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/vue-router' + +const fixedTimestamp = 1_700_000_000_000 + +export const Route = createFileRoute('/b')({ + loader: () => ({ name: 'b', ts: fixedTimestamp }), + component: BComponent, +}) + +function BComponent() { + const data = Route.useLoaderData() + + return ( +
{`${data.value.name}:${data.value.ts}`}
+ ) +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts new file mode 100644 index 0000000000..d26ce23d07 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client navigation-churn (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/item-payload.ts new file mode 100644 index 0000000000..4a86ea7244 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/item-payload.ts @@ -0,0 +1,51 @@ +const payloadByteLength = 20 * 1024 +const payloadChunkCount = 32 +const payloadChunkSize = payloadByteLength / payloadChunkCount +const trackedLoaderIdPrefix = 'sanity-' +const trackedItemLoaderCalls = new Map() + +export function createItemPayload(id: string) { + let seed = hashId(id) + const chunks = new Array(payloadChunkCount) + + for (let index = 0; index < payloadChunkCount; index++) { + seed = (seed * 1664525 + 1013904223) >>> 0 + chunks[index] = createPayloadChunk(id, index, seed) + } + + return { + id, + chunks, + byteLength: payloadByteLength, + } +} + +export function trackItemLoaderCall(id: string) { + if (!id.startsWith(trackedLoaderIdPrefix)) { + return + } + + trackedItemLoaderCalls.set(id, (trackedItemLoaderCalls.get(id) ?? 0) + 1) +} + +export function getTrackedItemLoaderCount(id: string) { + return trackedItemLoaderCalls.get(id) ?? 0 +} + +function createPayloadChunk(id: string, index: number, seed: number) { + const token = `${id}:${index.toString(36)}:${seed.toString(36)}:` + const repeatCount = Math.ceil(payloadChunkSize / token.length) + + return token.repeat(repeatCount).slice(0, payloadChunkSize) +} + +function hashId(id: string) { + let hash = 2166136261 + + for (let index = 0; index < id.length; index++) { + hash ^= id.charCodeAt(index) + hash = Math.imul(hash, 16777619) + } + + return hash >>> 0 +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts b/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/project.json b/benchmarks/memory/client/scenarios/preload-churn/react/project.json new file mode 100644 index 0000000000..238d98c816 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-preload-churn-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts new file mode 100644 index 0000000000..31cd97d135 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { getTrackedItemLoaderCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, + getTrackedItemLoaderCount, +) diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx new file mode 100644 index 0000000000..b75d1fe34d --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx @@ -0,0 +1,33 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export { getTrackedItemLoaderCount } from '../../item-payload' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..84aaebb9c9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx new file mode 100644 index 0000000000..69e3180173 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx @@ -0,0 +1,18 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx new file mode 100644 index 0000000000..af735d705b --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
index
+} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx new file mode 100644 index 0000000000..2f52acd528 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createItemPayload, trackItemLoaderCall } from '../../../item-payload' + +export const Route = createFileRoute('/items/$id')({ + loader: ({ params }) => { + trackItemLoaderCall(params.id) + return createItemPayload(params.id) + }, + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.id}:${data.byteLength}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts b/benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts new file mode 100644 index 0000000000..f9c426656b --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client preload-churn (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/shared.ts b/benchmarks/memory/client/scenarios/preload-churn/shared.ts new file mode 100644 index 0000000000..78e35c42ad --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/shared.ts @@ -0,0 +1,220 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' +import { + createBenchContainer, + drainMicrotasks, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type GetTrackedItemLoaderCount = (id: string) => number + +type PreloadRouter = { + load: () => Promise + preloadRoute: (options: { + to: '/items/$id' + params: { id: string } + }) => Promise + navigate: ( + options: + | { + to: '/items/$id' + params: { id: string } + replace: true + } + | { + to: '/' + replace: true + }, + ) => Promise + subscribe: (event: 'onRendered', listener: () => void) => () => void +} + +// Fixed id for the eviction navigations interleaved into the bench loop; its +// payload is a constant-size steady-state resident, never part of the signal. +const evictionItemId = 'nav-evict' +const preloadChurnIterations = 200 +// A navigation commit is what triggers the router's clearExpiredCache -- +// preloaded matches (defaultPreloadGcTime: 0) are only evicted then, never +// during a preload-only loop. Interleaving a navigation every few preloads is +// what makes the flat floor assert "eviction releases preloaded payloads". +const preloadsPerEvictionNavigation = 10 +// Module-level so ids stay unique across runner invocations on one mount; a +// per-invocation LCG would replay identical ids, and every preload after the +// first invocation would dedupe against cachedMatches instead of doing work. +const benchmarkRandom = createDeterministicRandom(0x706c6f61) +let preloadCounter = 0 + +const uninitialized = async (_id: string) => { + throw new Error('preload-churn benchmark is not initialized') +} + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, + getTrackedItemLoaderCount: GetTrackedItemLoaderCount, +) { + warnClientMemoryDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let router: PreloadRouter | undefined = undefined + let unmount = noop + let unsub = noop + let resolveRendered = noop + let evictionParity = 0 + let preloadItem: (id: string) => Promise = uninitialized + let navigateToItem: (id: string) => Promise = uninitialized + let navigateToIndex: () => Promise = () => + uninitialized('navigate-to-index') + + function assertRenderedIndex() { + const actual = + container?.querySelector('[data-bench-page]')?.dataset + .benchPage + + if (actual !== 'index') { + throw new Error(`Expected rendered index page, got ${actual}`) + } + } + + async function waitForRenderedIndex() { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedIndex() + return + } catch { + await nextAnimationFrame() + } + } + + assertRenderedIndex() + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = createBenchContainer() + + const mounted = mountTestApp(container) + router = mounted.router as PreloadRouter + unmount = mounted.unmount + + unsub = router.subscribe('onRendered', () => { + resolveRendered() + }) + + preloadItem = async (id) => { + await router!.preloadRoute({ + to: '/items/$id', + params: { id }, + }) + await drainMicrotasks() + } + + navigateToItem = async (id) => { + const rendered = waitForNextRender() + await Promise.all([ + router!.navigate({ + to: '/items/$id', + params: { id }, + replace: true, + }), + rendered, + ]) + } + + navigateToIndex = async () => { + const rendered = waitForNextRender() + await Promise.all([ + router!.navigate({ + to: '/', + replace: true, + }), + rendered, + ]) + } + + await router.load() + await waitForRenderedIndex() + } + + function after() { + unmount() + removeBenchContainer(container) + unsub() + + container = undefined + router = undefined + unmount = noop + unsub = noop + resolveRendered = noop + evictionParity = 0 + preloadItem = uninitialized + navigateToItem = uninitialized + navigateToIndex = () => uninitialized('navigate-to-index') + } + + // Alternate between two distinct locations so every eviction navigation + // changes the href (a same-href navigate would never commit or render). + async function evictPreloads() { + evictionParity = (evictionParity + 1) % 2 + + if (evictionParity === 1) { + await navigateToItem(evictionItemId) + } else { + await navigateToIndex() + } + } + + return { + name: `mem preload-churn (${framework})`, + before, + preload: (id: string) => preloadItem(id), + evictPreloads, + async run() { + for (let index = 0; index < preloadChurnIterations; index++) { + await preloadItem( + `${(preloadCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`, + ) + + if ((index + 1) % preloadsPerEvictionNavigation === 0) { + await evictPreloads() + } + } + }, + async sanity() { + await before() + + try { + assertRenderedIndex() + + const id = 'sanity-preloaded-item' + const initialLoaderCount = getTrackedItemLoaderCount(id) + await preloadItem(id) + + const preloadedLoaderCount = getTrackedItemLoaderCount(id) + if (preloadedLoaderCount !== initialLoaderCount + 1) { + throw new Error( + `Expected preload to run item loader once, got ${preloadedLoaderCount - initialLoaderCount}`, + ) + } + } finally { + after() + } + }, + after, + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/project.json b/benchmarks/memory/client/scenarios/preload-churn/solid/project.json new file mode 100644 index 0000000000..16ef8af968 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-preload-churn-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts new file mode 100644 index 0000000000..4db457f561 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { getTrackedItemLoaderCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, + getTrackedItemLoaderCount, +) diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx new file mode 100644 index 0000000000..18ad5b97e4 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx @@ -0,0 +1,31 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from 'solid-js/web' +import { getRouter } from './router' + +export { getTrackedItemLoaderCount } from '../../item-payload' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..27749ab2af --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx new file mode 100644 index 0000000000..faed87f1f4 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx @@ -0,0 +1,18 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx new file mode 100644 index 0000000000..b870b8bfd5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
index
+} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx new file mode 100644 index 0000000000..15fa2a34ab --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { createItemPayload, trackItemLoaderCall } from '../../../item-payload' + +export const Route = createFileRoute('/items/$id')({ + loader: ({ params }) => { + trackItemLoaderCall(params.id) + return createItemPayload(params.id) + }, + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data().id}:${data().byteLength}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json new file mode 100644 index 0000000000..b12dcb7ade --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts new file mode 100644 index 0000000000..c86a6fcbb3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client preload-churn (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/project.json b/benchmarks/memory/client/scenarios/preload-churn/vue/project.json new file mode 100644 index 0000000000..e13388890f --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-preload-churn-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts new file mode 100644 index 0000000000..4aad4af31e --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { getTrackedItemLoaderCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, + getTrackedItemLoaderCount, +) diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx new file mode 100644 index 0000000000..890447d88b --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx @@ -0,0 +1,38 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export { getTrackedItemLoaderCount } from '../../item-payload' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..a2216f2b67 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx new file mode 100644 index 0000000000..86534112d7 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx @@ -0,0 +1,18 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/'], + }), + routeTree, + defaultPreloadGcTime: 0, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx new file mode 100644 index 0000000000..b4f871306c --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
index
+} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx new file mode 100644 index 0000000000..2a29245926 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createItemPayload, trackItemLoaderCall } from '../../../item-payload' + +export const Route = createFileRoute('/items/$id')({ + loader: ({ params }: { params: { id: string } }) => { + trackItemLoaderCall(params.id) + return createItemPayload(params.id) + }, + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+ {`${data.value.id}:${data.value.byteLength}`} +
+ ) +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts new file mode 100644 index 0000000000..ea7ed7b695 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client preload-churn (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json new file mode 100644 index 0000000000..1572552650 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-unique-location-churn-react", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts new file mode 100644 index 0000000000..70603b5c34 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx new file mode 100644 index 0000000000..d714e7bfdd --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx @@ -0,0 +1,31 @@ +import { RouterProvider } from '@tanstack/react-router' +import { createRoot } from 'react-dom/client' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const reactRoot = createRoot(container) + let didUnmount = false + + reactRoot.render() + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + reactRoot.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..1165f57523 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/items/$id' + id: '__root__' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx new file mode 100644 index 0000000000..50ad269060 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/items/initial?q=q-initial'], + }), + routeTree, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx new file mode 100644 index 0000000000..889395056b --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx new file mode 100644 index 0000000000..c9e823f6c2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/react-router' + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }) => ({ q: search.q }), + loader: ({ params, deps }) => ({ + id: params.id, + q: deps.q, + checksum: params.id.length + deps.q.length, + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
{`${data.id}:${data.q}:${data.checksum}`}
+ ) +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json new file mode 100644 index 0000000000..ea566061d9 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowImportingTsExtensions": true, + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts new file mode 100644 index 0000000000..f424c57cef --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client unique-location-churn (react)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts new file mode 100644 index 0000000000..95b382269d --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts @@ -0,0 +1,149 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' +import { + createBenchContainer, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' + +type ItemLocation = { + id: string + q: string +} + +type NavigationRouter = { + load: () => Promise + navigate: (options: { + to: '/items/$id' + params: { id: string } + search: { q: string } + replace: true + }) => Promise + subscribe: (event: 'onRendered', listener: () => void) => () => void +} + +const uniqueLocationChurnIterations = 300 +// Module-level so ids stay unique across runner invocations on one mount; the +// counter prefix removes any residual LCG birthday-collision risk. +const benchmarkRandom = createDeterministicRandom(0xdecafbad) +let locationCounter = 0 + +const uninitialized = () => + Promise.reject( + new Error('unique-location-churn benchmark is not initialized'), + ) + +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, +) { + warnClientMemoryDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop + let navigateTo: (location: ItemLocation) => Promise = uninitialized + + function assertRenderedId(expected: string) { + const actual = + container?.querySelector('[data-bench-id]')?.dataset.benchId + + if (actual !== expected) { + throw new Error(`Expected rendered item id ${expected}, got ${actual}`) + } + } + + async function waitForRenderedId(expected: string) { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedId(expected) + return + } catch { + await nextAnimationFrame() + } + } + + assertRenderedId(expected) + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = createBenchContainer() + + const mounted = mountTestApp(container) + const router = mounted.router as NavigationRouter + unmount = mounted.unmount + + unsub = router.subscribe('onRendered', () => { + resolveRendered() + }) + + navigateTo = async (location) => { + const rendered = waitForNextRender() + await Promise.all([ + router.navigate({ + to: '/items/$id', + params: { id: location.id }, + search: { q: location.q }, + replace: true, + }), + rendered, + ]) + } + + await router.load() + await waitForRenderedId('initial') + } + + function after() { + unmount() + removeBenchContainer(container) + unsub() + + container = undefined + unmount = noop + unsub = noop + resolveRendered = noop + navigateTo = uninitialized + } + + return { + name: `mem unique-location-churn (${framework})`, + before, + navigate: (location: ItemLocation) => navigateTo(location), + async run() { + for (let index = 0; index < uniqueLocationChurnIterations; index++) { + const id = `${(locationCounter++).toString(36)}-${randomSegment(benchmarkRandom)}` + const q = `q-${randomSegment(benchmarkRandom)}` + + await navigateTo({ id, q }) + } + }, + async sanity() { + await before() + + try { + await navigateTo({ id: 'sanity-one', q: 'q-sanity-one' }) + assertRenderedId('sanity-one') + } finally { + after() + } + }, + after, + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json new file mode 100644 index 0000000000..5be22de7a4 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-unique-location-churn-solid", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts new file mode 100644 index 0000000000..493425c6a2 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx new file mode 100644 index 0000000000..16000c90a1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx @@ -0,0 +1,29 @@ +import { RouterProvider } from '@tanstack/solid-router' +import { render } from 'solid-js/web' +import { getRouter } from './router' + +export function mountTestApp(container: Element) { + const router = getRouter() + const dispose = render(() => , container) + let didUnmount = false + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + dispose() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..4a451735ff --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/items/$id' + id: '__root__' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx new file mode 100644 index 0000000000..2969b06914 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/items/initial?q=q-initial'], + }), + routeTree, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..cb8d5a688d --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/solid-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx new file mode 100644 index 0000000000..9724922b8d --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/solid-router' + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }) => ({ q: search.q }), + loader: ({ params, deps }) => ({ + id: params.id, + q: deps.q, + checksum: params.id.length + deps.q.length, + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
{`${data().id}:${data().q}:${data().checksum}`}
+ ) +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json new file mode 100644 index 0000000000..b12dcb7ade --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts new file mode 100644 index 0000000000..d4acf0019a --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + solid({ hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client unique-location-churn (solid)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts new file mode 100644 index 0000000000..e645ab38f1 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts @@ -0,0 +1,21 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' + +await workload.sanity() + +describe('memory', () => { + if (workload.before && workload.after) { + beforeAll(workload.before) + afterAll(workload.after) + + bench(workload.name, workload.run, { + ...memoryBenchOptions, + setup: workload.before, + teardown: workload.after, + }) + return + } + + bench(workload.name, workload.run, memoryBenchOptions) +}) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts new file mode 100644 index 0000000000..952fd9a62c --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runClientFlameBenchmark } from '#memory-client/flame-runner' +import { workload } from './setup.ts' + +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json new file mode 100644 index 0000000000..6c05cf80b0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-client-unique-location-churn-vue", + "projectType": "application", + "targets": { + "build:client": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:client:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:client:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:client": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-router"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts new file mode 100644 index 0000000000..a38df04788 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts @@ -0,0 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' +import type * as App from './src/app' +import { createWorkload } from '../shared.ts' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx new file mode 100644 index 0000000000..fad2251cc0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx @@ -0,0 +1,36 @@ +import { RouterProvider } from '@tanstack/vue-router' +import { createApp } from 'vue' +import { getRouter } from './router' +import type {} from '@tanstack/router-core' + +export function mountTestApp(container: Element) { + const router = getRouter() + const vueApp = createApp({ + setup() { + return () => + }, + }) + let didUnmount = false + + vueApp.mount(container) + + // Full teardown mirrors the mount-unmount scenario: guard double-unmounts, + // release the devtools global, and detach history listeners. + return { + router, + unmount() { + if (didUnmount) { + return + } + + didUnmount = true + vueApp.unmount() + + if (typeof self !== 'undefined' && self.__TSR_ROUTER__ === router) { + self.__TSR_ROUTER__ = undefined + } + + router.history.destroy() + }, + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..d3e9db0345 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts @@ -0,0 +1,59 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/items/$id' + id: '__root__' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx new file mode 100644 index 0000000000..72f9ab565b --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx @@ -0,0 +1,17 @@ +import { createMemoryHistory, createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + history: createMemoryHistory({ + initialEntries: ['/items/initial?q=q-initial'], + }), + routeTree, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..91296e6f84 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx @@ -0,0 +1,9 @@ +import { Outlet, createRootRoute } from '@tanstack/vue-router' + +export const Route = createRootRoute({ + component: RootComponent, +}) + +function RootComponent() { + return +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx new file mode 100644 index 0000000000..1854367142 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx @@ -0,0 +1,34 @@ +import { createFileRoute } from '@tanstack/vue-router' + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }: { search: ItemSearch }) => ({ q: search.q }), + loader: ({ + params, + deps, + }: { + params: { id: string } + deps: { q: string } + }) => ({ + id: params.id, + q: deps.q, + checksum: params.id.length + deps.q.length, + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
{`${data.value.id}:${data.value.q}:${data.value.checksum}`}
+ ) +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json new file mode 100644 index 0000000000..9a5872a4c0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "preserve", + "allowImportingTsExtensions": true, + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "../../../vitest.setup.ts" + ] +} diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts new file mode 100644 index 0000000000..423126019b --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts @@ -0,0 +1,36 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + vue(), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + lib: { + entry: './src/app.tsx', + formats: ['es'], + fileName: 'app', + }, + }, + test: { + name: '@benchmarks/memory-client unique-location-churn (vue)', + watch: false, + environment: 'jsdom', + setupFiles: ['../../../vitest.setup.ts'], + }, +}) diff --git a/benchmarks/memory/client/tsconfig.json b/benchmarks/memory/client/tsconfig.json new file mode 100644 index 0000000000..9280d5c42f --- /dev/null +++ b/benchmarks/memory/client/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": ["bench-utils.ts", "lifecycle.ts", "vitest.setup.ts"] +} diff --git a/benchmarks/memory/client/vitest.react.config.ts b/benchmarks/memory/client/vitest.react.config.ts new file mode 100644 index 0000000000..44643b4d60 --- /dev/null +++ b/benchmarks/memory/client/vitest.react.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/react/vite.config.ts'], + }, +}) diff --git a/benchmarks/memory/client/vitest.setup.ts b/benchmarks/memory/client/vitest.setup.ts new file mode 100644 index 0000000000..ce3127275a --- /dev/null +++ b/benchmarks/memory/client/vitest.setup.ts @@ -0,0 +1,9 @@ +// @ts-expect-error +global.IS_REACT_ACT_ENVIRONMENT = true + +const scrollTo = () => {} + +window.scrollTo = scrollTo +globalThis.scrollTo = scrollTo + +export {} diff --git a/benchmarks/memory/client/vitest.solid.config.ts b/benchmarks/memory/client/vitest.solid.config.ts new file mode 100644 index 0000000000..5c8185cdd9 --- /dev/null +++ b/benchmarks/memory/client/vitest.solid.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/solid/vite.config.ts'], + }, +}) diff --git a/benchmarks/memory/client/vitest.vue.config.ts b/benchmarks/memory/client/vitest.vue.config.ts new file mode 100644 index 0000000000..01768185ee --- /dev/null +++ b/benchmarks/memory/client/vitest.vue.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/vue/vite.config.ts'], + }, +}) diff --git a/benchmarks/memory/flame-control.ts b/benchmarks/memory/flame-control.ts new file mode 100644 index 0000000000..0d6f42df33 --- /dev/null +++ b/benchmarks/memory/flame-control.ts @@ -0,0 +1,123 @@ +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' + +const flameEnabled = process.env.TSR_MEMORY_FLAME === '1' +const heapIntervalBytes = 512 * 1024 +const heapStackDepth = 64 + +interface HeapProfile { + encode: () => Uint8Array +} + +interface HeapProfiler { + start: (intervalBytes: number, stackDepth: number) => void + stop: () => void + v8Profile: () => unknown + convertProfile: ( + v8Profile: unknown, + ignoreSamplePath?: string, + sourceMapper?: unknown, + ) => HeapProfile +} + +interface SourceMapperConstructor { + create: (searchDirs: Array) => Promise +} + +interface PprofModule { + heap: HeapProfiler + SourceMapper: SourceMapperConstructor +} + +function loadPprof() { + const requireFrom = process.env.TSR_MEMORY_REQUIRE_FROM + ? path.resolve(process.env.TSR_MEMORY_REQUIRE_FROM) + : import.meta.url + const require = createRequire(requireFrom) + + return require('@datadog/pprof') as PprofModule +} + +function getSourcemapDirs() { + return (process.env.TSR_MEMORY_SOURCEMAP_DIRS ?? '') + .split(path.delimiter) + .filter(Boolean) +} + +async function createSourceMapper(pprof: PprofModule) { + const sourcemapDirs = getSourcemapDirs() + + if (sourcemapDirs.length === 0) { + return undefined + } + + return pprof.SourceMapper.create(sourcemapDirs) +} + +function formatTimestamp() { + return new Date().toISOString().replace(/[:.]/g, '-') +} + +function formatProfileFileName(profileName?: string) { + const profileNameSlug = profileName + ?.trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + const profileNamePart = profileNameSlug ? `${profileNameSlug}-` : '' + + return `heap-profile-${profileNamePart}${formatTimestamp()}.pb` +} + +export async function profileFlameWorkload( + workload: () => Promise | void, + profileName?: string, +) { + if (!flameEnabled) { + await workload() + return + } + + const profileDir = process.env.TSR_MEMORY_PROFILE_DIR + + if (!profileDir) { + throw new Error('TSR_MEMORY_PROFILE_DIR must be set for flame profiling') + } + + fs.mkdirSync(profileDir, { recursive: true }) + + const pprof = loadPprof() + const sourceMapper = await createSourceMapper(pprof) + + pprof.heap.start(heapIntervalBytes, heapStackDepth) + + let workloadError: unknown = undefined + let v8Profile: unknown = undefined + + try { + await workload() + } catch (error) { + workloadError = error + } finally { + try { + v8Profile = pprof.heap.v8Profile() + } finally { + pprof.heap.stop() + } + } + + const heapProfile = pprof.heap.convertProfile( + v8Profile, + undefined, + sourceMapper, + ) + const profilePath = path.join(profileDir, formatProfileFileName(profileName)) + + fs.writeFileSync(profilePath, heapProfile.encode()) + console.log(`Heap profile written to: ${profilePath}`) + + if (workloadError) { + throw workloadError + } +} diff --git a/benchmarks/memory/run-flame.mjs b/benchmarks/memory/run-flame.mjs new file mode 100644 index 0000000000..cc568f3f77 --- /dev/null +++ b/benchmarks/memory/run-flame.mjs @@ -0,0 +1,117 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process' +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' +import process from 'node:process' + +const [, , entrypointArg, sourcemapDirArg] = process.argv + +if (!entrypointArg || !sourcemapDirArg) { + console.error( + 'Usage: node benchmarks/memory/run-flame.mjs ', + ) + process.exit(1) +} + +const entrypointPath = path.resolve(entrypointArg) +const sourcemapDirPath = path.resolve(sourcemapDirArg) + +if (!fs.existsSync(entrypointPath)) { + console.error(`Flame entrypoint not found: ${entrypointPath}`) + process.exit(1) +} + +if (!fs.existsSync(sourcemapDirPath)) { + console.error(`Flame sourcemap directory not found: ${sourcemapDirPath}`) + process.exit(1) +} + +const profileDir = path.join( + path.dirname(entrypointPath), + '.profiles', + new Date().toISOString().replace(/[:.]/g, '-'), +) + +fs.mkdirSync(profileDir, { recursive: true }) + +const entrypointRequire = createRequire(entrypointPath) +const { generateFlamegraph, generateMarkdown } = entrypointRequire( + '@platformatic/flame', +) + +process.env.NODE_ENV = 'production' + +console.log(`Flame profile directory: ${profileDir}`) + +const childProcess = spawn(process.execPath, [entrypointPath], { + cwd: profileDir, + env: { + ...process.env, + NODE_ENV: 'production', + TSR_MEMORY_FLAME: '1', + TSR_MEMORY_PROFILE_DIR: profileDir, + TSR_MEMORY_REQUIRE_FROM: entrypointPath, + TSR_MEMORY_SOURCEMAP_DIRS: sourcemapDirPath, + }, + stdio: 'inherit', +}) + +console.log(`Flame profiling process: ${childProcess.pid}`) + +const childCode = await new Promise((resolve) => { + childProcess.on('error', (error) => { + console.error(`Failed to start Flame profiled process: ${error.message}`) + resolve(1) + }) + + childProcess.on('close', (code, signal) => { + if (signal) { + console.error(`Flame profiled process exited via signal ${signal}`) + resolve(1) + return + } + + resolve(code ?? 0) + }) +}) + +const heapProfilePaths = fs + .readdirSync(profileDir) + .filter((fileName) => /^heap-profile-.*\.pb$/.test(fileName)) + .sort() + .map((fileName) => path.join(profileDir, fileName)) + +for (const heapProfilePath of heapProfilePaths) { + const htmlPath = heapProfilePath.replace(/\.pb$/, '.html') + const mdPath = heapProfilePath.replace(/\.pb$/, '.md') + + console.log(`Generating heap flamegraph: ${htmlPath}`) + try { + await generateFlamegraph(heapProfilePath, htmlPath) + } catch (error) { + console.warn(`Failed to generate heap flamegraph: ${error.message}`) + } + + console.log(`Generating heap markdown: ${mdPath}`) + try { + await generateMarkdown(heapProfilePath, mdPath, { format: 'detailed' }) + } catch (error) { + console.warn(`Failed to generate heap markdown: ${error.message}`) + } +} + +const heapReportPaths = fs + .readdirSync(profileDir) + .filter((fileName) => /^heap-profile-.*\.(?:html|md)$/.test(fileName)) + .sort() + .map((fileName) => path.join(profileDir, fileName)) + +if (heapReportPaths.length > 0) { + console.log('Generated heap profile reports:') + for (const heapReportPath of heapReportPaths) { + console.log(` ${heapReportPath}`) + } +} + +process.exit(childCode) diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts new file mode 100644 index 0000000000..f1f5dadea0 --- /dev/null +++ b/benchmarks/memory/server/bench-utils.ts @@ -0,0 +1,90 @@ +export interface StartRequestHandler { + fetch: (request: Request) => Promise | Response +} + +type RunSequentialRequestLoopRandomOptions = + | { + seed: number + random?: never + } + | { + random: () => number + seed?: never + } + +export type RunSequentialRequestLoopOptions = + RunSequentialRequestLoopRandomOptions & { + iterations?: number + buildRequest: (random: () => number, index: number) => Request + validateResponse?: (response: Response, request: Request) => void + } + +export const memoryBenchOptions = { + iterations: 1, + warmupIterations: 1, + time: 0, + warmupTime: 0, + throws: true, +} + +export function createDeterministicRandom(seed: number) { + let state = seed >>> 0 + + return () => { + state = (state * 1664525 + 1013904223) >>> 0 + return state / 0x100000000 + } +} + +export function randomSegment(random: () => number) { + return Math.floor(random() * 1_000_000_000).toString(36) +} + +export async function drainResponse(response: Response) { + const reader = response.body?.getReader() + + if (!reader) { + return + } + + try { + while (true) { + const result = await reader.read() + + if (result.done) { + break + } + } + } finally { + reader.releaseLock() + } +} + +export async function runSequentialRequestLoop( + handler: StartRequestHandler, + options: RunSequentialRequestLoopOptions, +) { + const { iterations = 10, buildRequest, validateResponse } = options + const random = + options.seed !== undefined + ? createDeterministicRandom(options.seed) + : options.random + const validate = + validateResponse ?? + ((response: Response, request: Request) => { + if (response.status !== 200) { + throw new Error( + `Request failed with non-200 status ${response.status} (${request.url})`, + ) + } + }) + + for (let index = 0; index < iterations; index++) { + const request = buildRequest(random, index) + const response = await handler.fetch(request) + + validate(response, request) + + await drainResponse(response) + } +} diff --git a/benchmarks/memory/server/benchmark.ts b/benchmarks/memory/server/benchmark.ts new file mode 100644 index 0000000000..301a9f956d --- /dev/null +++ b/benchmarks/memory/server/benchmark.ts @@ -0,0 +1,9 @@ +export interface ServerMemoryWorkload { + name: string + run: () => Promise | void +} + +export interface ServerMemoryWorkloadGroup { + sanity: () => Promise | void + workloads: Array +} diff --git a/benchmarks/memory/server/flame-runner.ts b/benchmarks/memory/server/flame-runner.ts new file mode 100644 index 0000000000..1930085aad --- /dev/null +++ b/benchmarks/memory/server/flame-runner.ts @@ -0,0 +1,12 @@ +import { profileFlameWorkload } from '../flame-control.ts' +import type { ServerMemoryWorkloadGroup } from './benchmark.ts' + +export async function runServerFlameBenchmark( + workloadGroup: ServerMemoryWorkloadGroup, +) { + await workloadGroup.sanity() + + for (const workload of workloadGroup.workloads) { + await profileFlameWorkload(workload.run, workload.name) + } +} diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json new file mode 100644 index 0000000000..68ba6b3c32 --- /dev/null +++ b/benchmarks/memory/server/package.json @@ -0,0 +1,269 @@ +{ + "name": "@benchmarks/memory-server", + "private": true, + "type": "module", + "scripts": { + "clean:profiles": "rm -rf scenarios/*/*/.profiles" + }, + "imports": { + "#memory-server/benchmark": "./benchmark.ts", + "#memory-server/bench-utils": "./bench-utils.ts", + "#memory-server/flame-runner": "./flame-runner.ts" + }, + "dependencies": { + "@tanstack/react-router": "workspace:*", + "@tanstack/react-start": "workspace:*", + "@tanstack/solid-router": "workspace:*", + "@tanstack/solid-start": "workspace:*", + "@tanstack/vue-router": "workspace:*", + "@tanstack/vue-start": "workspace:*", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "solid-js": "^1.9.10", + "vue": "^3.5.16" + }, + "devDependencies": { + "@codspeed/vitest-plugin": "^5.5.0", + "@datadog/pprof": "^5.13.2", + "@platformatic/flame": "^1.6.0", + "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-vue-jsx": "^5.1.5", + "typescript": "^6.0.2", + "vite": "^8.0.14", + "vite-plugin-solid": "^2.11.11", + "vitest": "^4.1.4" + }, + "nx": { + "targets": { + "build:react": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-react", + "@benchmarks/memory-server-server-fn-churn-react", + "@benchmarks/memory-server-error-paths-react", + "@benchmarks/memory-server-aborted-requests-react", + "@benchmarks/memory-server-peak-large-page-react", + "@benchmarks/memory-server-streaming-peak-react", + "@benchmarks/memory-server-serialization-payload-react" + ], + "target": "build:ssr" + } + ] + }, + "build:solid": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-solid", + "@benchmarks/memory-server-server-fn-churn-solid", + "@benchmarks/memory-server-error-paths-solid", + "@benchmarks/memory-server-aborted-requests-solid", + "@benchmarks/memory-server-peak-large-page-solid", + "@benchmarks/memory-server-streaming-peak-solid", + "@benchmarks/memory-server-serialization-payload-solid" + ], + "target": "build:ssr" + } + ] + }, + "build:vue": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-vue", + "@benchmarks/memory-server-server-fn-churn-vue", + "@benchmarks/memory-server-error-paths-vue", + "@benchmarks/memory-server-aborted-requests-vue", + "@benchmarks/memory-server-peak-large-page-vue", + "@benchmarks/memory-server-streaming-peak-vue", + "@benchmarks/memory-server-serialization-payload-vue" + ], + "target": "build:ssr" + } + ] + }, + "build:react:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-react", + "@benchmarks/memory-server-server-fn-churn-react", + "@benchmarks/memory-server-error-paths-react", + "@benchmarks/memory-server-aborted-requests-react", + "@benchmarks/memory-server-peak-large-page-react", + "@benchmarks/memory-server-streaming-peak-react", + "@benchmarks/memory-server-serialization-payload-react" + ], + "target": "build:ssr:flame" + } + ] + }, + "build:solid:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-solid", + "@benchmarks/memory-server-server-fn-churn-solid", + "@benchmarks/memory-server-error-paths-solid", + "@benchmarks/memory-server-aborted-requests-solid", + "@benchmarks/memory-server-peak-large-page-solid", + "@benchmarks/memory-server-streaming-peak-solid", + "@benchmarks/memory-server-serialization-payload-solid" + ], + "target": "build:ssr:flame" + } + ] + }, + "build:vue:flame": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-vue", + "@benchmarks/memory-server-server-fn-churn-vue", + "@benchmarks/memory-server-error-paths-vue", + "@benchmarks/memory-server-aborted-requests-vue", + "@benchmarks/memory-server-peak-large-page-vue", + "@benchmarks/memory-server-streaming-peak-vue", + "@benchmarks/memory-server-serialization-payload-vue" + ], + "target": "build:ssr:flame" + } + ] + }, + "test:flame:react": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-react", + "@benchmarks/memory-server-server-fn-churn-react", + "@benchmarks/memory-server-error-paths-react", + "@benchmarks/memory-server-aborted-requests-react", + "@benchmarks/memory-server-peak-large-page-react", + "@benchmarks/memory-server-streaming-peak-react", + "@benchmarks/memory-server-serialization-payload-react" + ], + "target": "test:flame" + } + ] + }, + "test:flame:solid": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-solid", + "@benchmarks/memory-server-server-fn-churn-solid", + "@benchmarks/memory-server-error-paths-solid", + "@benchmarks/memory-server-aborted-requests-solid", + "@benchmarks/memory-server-peak-large-page-solid", + "@benchmarks/memory-server-streaming-peak-solid", + "@benchmarks/memory-server-serialization-payload-solid" + ], + "target": "test:flame" + } + ] + }, + "test:flame:vue": { + "executor": "nx:noop", + "cache": false, + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-vue", + "@benchmarks/memory-server-server-fn-churn-vue", + "@benchmarks/memory-server-error-paths-vue", + "@benchmarks/memory-server-aborted-requests-vue", + "@benchmarks/memory-server-peak-large-page-vue", + "@benchmarks/memory-server-streaming-peak-vue", + "@benchmarks/memory-server-serialization-payload-vue" + ], + "target": "test:flame" + } + ] + }, + "test:perf:react": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:react" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.react.config.ts", + "cwd": "benchmarks/memory/server" + } + }, + "test:perf:solid": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:solid" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.solid.config.ts", + "cwd": "benchmarks/memory/server" + } + }, + "test:perf:vue": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": [ + "build:vue" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.vue.config.ts", + "cwd": "benchmarks/memory/server" + } + }, + "test:types": { + "executor": "nx:noop", + "dependsOn": [ + { + "projects": [ + "@benchmarks/memory-server-request-churn-react", + "@benchmarks/memory-server-server-fn-churn-react", + "@benchmarks/memory-server-error-paths-react", + "@benchmarks/memory-server-aborted-requests-react", + "@benchmarks/memory-server-peak-large-page-react", + "@benchmarks/memory-server-streaming-peak-react", + "@benchmarks/memory-server-serialization-payload-react", + "@benchmarks/memory-server-request-churn-solid", + "@benchmarks/memory-server-server-fn-churn-solid", + "@benchmarks/memory-server-error-paths-solid", + "@benchmarks/memory-server-aborted-requests-solid", + "@benchmarks/memory-server-peak-large-page-solid", + "@benchmarks/memory-server-streaming-peak-solid", + "@benchmarks/memory-server-serialization-payload-solid", + "@benchmarks/memory-server-request-churn-vue", + "@benchmarks/memory-server-server-fn-churn-vue", + "@benchmarks/memory-server-error-paths-vue", + "@benchmarks/memory-server-aborted-requests-vue", + "@benchmarks/memory-server-peak-large-page-vue", + "@benchmarks/memory-server-streaming-peak-vue", + "@benchmarks/memory-server-serialization-payload-vue" + ], + "target": "test:types:ssr" + } + ] + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/deferred-records.ts b/benchmarks/memory/server/scenarios/aborted-requests/deferred-records.ts new file mode 100644 index 0000000000..4886e3ee80 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/deferred-records.ts @@ -0,0 +1,18 @@ +const recordCount = 20 + +export type RecordGroup = 'alpha' | 'beta' + +export interface DeferredRecord { + id: string + label: string +} + +export function makeAbortedRequestRecords( + id: string, + group: RecordGroup, +): Array { + return Array.from({ length: recordCount }, (_, index) => ({ + id: `${group}-${id}-${index}`, + label: `deferred-${group}-${id}-${index}`, + })) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/project.json b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json new file mode 100644 index 0000000000..bea90c8766 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-aborted-requests-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..fbbc25ecfb --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx new file mode 100644 index 0000000000..08d534a779 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
aborted-requests-index
+} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..ecd9ef524d --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx @@ -0,0 +1,66 @@ +import { Await, createFileRoute } from '@tanstack/react-router' +import { Suspense } from 'react' +import { makeAbortedRequestRecords } from '../../../deferred-records' + +function resolveAfterMicrotasks(microtasks: number, value: () => T) { + let promise = Promise.resolve() + + for (let index = 0; index < microtasks; index++) { + promise = promise.then(() => undefined) + } + + return promise.then(value) +} + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + eager: `eager-${params.id}`, + alpha: resolveAfterMicrotasks(32, () => + makeAbortedRequestRecords(params.id, 'alpha'), + ), + beta: resolveAfterMicrotasks(64, () => + makeAbortedRequestRecords(params.id, 'beta'), + ), + }), + component: StreamComponent, +}) + +function StreamComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.eager}

+ loading-alpha

+ } + > + + {(records) => ( +
    + {records.map((record) => ( +
  • {record.label}
  • + ))} +
+ )} +
+
+ loading-beta

+ } + > + + {(records) => ( +
    + {records.map((record) => ( +
  • {record.label}
  • + ))} +
+ )} +
+
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts new file mode 100644 index 0000000000..a3f4836091 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server aborted-requests (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/shared.ts b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts new file mode 100644 index 0000000000..9b71c37617 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts @@ -0,0 +1,278 @@ +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +type AbortedRequestReadMode = 'first-chunk' | 'shell-before-deferred' +type AbortedRequestCancelMode = 'plain' | 'swallow-abort-error' +type AbortedRequestDrainMode = 'microtasks' | 'tasks' + +type AbortedRequestMode = { + readMode: AbortedRequestReadMode + cancelMode: AbortedRequestCancelMode + drainMode: AbortedRequestDrainMode +} + +const abortedRequestIterations = 100 +let abortedRequestCounter = 0 +const eagerMarker = 'data-bench="aborted-requests-eager"' +const alphaFallbackMarker = 'data-bench="aborted-requests-alpha-fallback"' +const betaFallbackMarker = 'data-bench="aborted-requests-beta-fallback"' +const alphaFirstRecord = (id: string) => `deferred-alpha-${id}-0` +const alphaLastRecord = (id: string) => `deferred-alpha-${id}-19` +const betaFirstRecord = (id: string) => `deferred-beta-${id}-0` +const betaLastRecord = (id: string) => `deferred-beta-${id}-19` + +const textDecoder = new TextDecoder() +const abortedRequestModes: Record = { + react: { + readMode: 'first-chunk', + cancelMode: 'plain', + drainMode: 'microtasks', + }, + solid: { + readMode: 'first-chunk', + cancelMode: 'plain', + drainMode: 'tasks', + }, + vue: { + readMode: 'shell-before-deferred', + cancelMode: 'swallow-abort-error', + drainMode: 'tasks', + }, +} + +const documentRequestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildStreamRequest(id: string, signal?: AbortSignal) { + const init: RequestInit = { ...documentRequestInit } + + if (signal) { + init.signal = signal + } + + return new Request(`http://localhost/stream/${id}`, init) +} + +function validateDocumentResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } +} + +async function readFirstChunk(response: Response, request: Request) { + if (!response.body) { + throw new Error(`Expected response body for ${request.url}`) + } + + const reader = response.body.getReader() + const result = await reader.read() + const value = result.value + + if (result.done || !value || value.byteLength === 0) { + await reader.cancel() + throw new Error(`Expected a non-empty first chunk for ${request.url}`) + } + + return { + reader, + value, + } +} + +async function readShellBeforeDeferred( + response: Response, + request: Request, + id: string, +) { + if (!response.body) { + throw new Error(`Expected response body for ${request.url}`) + } + + const reader = response.body.getReader() + let text = '' + + while (true) { + const result = await reader.read() + const value = result.value + + if (result.done || !value || value.byteLength === 0) { + await reader.cancel() + throw new Error(`Expected shell content for ${request.url}`) + } + + text += textDecoder.decode(value, { stream: true }) + + for (const marker of [ + alphaFirstRecord(id), + alphaLastRecord(id), + betaFirstRecord(id), + betaLastRecord(id), + ]) { + if (text.includes(marker)) { + await reader.cancel() + throw new Error( + `Shell chunks already included deferred content ${marker}`, + ) + } + } + + if ( + text.includes(eagerMarker) && + text.includes(alphaFallbackMarker) && + text.includes(betaFallbackMarker) + ) { + return { + reader, + text, + } + } + } +} + +async function readSanityStream( + response: Response, + request: Request, + mode: AbortedRequestMode, + id: string, +) { + if (mode.readMode === 'shell-before-deferred') { + return readShellBeforeDeferred(response, request, id) + } + + const { reader, value } = await readFirstChunk(response, request) + + return { + reader, + text: textDecoder.decode(value), + } +} + +function readLoopStream( + mode: AbortedRequestReadMode, + response: Response, + request: Request, + id: string, +) { + if (mode === 'shell-before-deferred') { + return readShellBeforeDeferred(response, request, id) + } + + return readFirstChunk(response, request) +} + +async function cancelReader( + reader: ReadableStreamDefaultReader, + mode: AbortedRequestCancelMode, +) { + if (mode === 'swallow-abort-error') { + try { + await reader.cancel() + } catch (error) { + if (!(error instanceof DOMException && error.name === 'AbortError')) { + throw error + } + } + + return + } + + await reader.cancel() +} + +async function drainCancellation(mode: AbortedRequestDrainMode) { + await Promise.resolve() + await Promise.resolve() + + if (mode === 'tasks') { + await new Promise((resolve) => setTimeout(resolve, 0)) + } +} + +async function assertAbortedRequestsSanity( + handler: StartRequestHandler, + mode: AbortedRequestMode, +) { + const fullId = 'sanity-full' + const fullRequest = buildStreamRequest(fullId) + const fullResponse = await handler.fetch(fullRequest) + validateDocumentResponse(fullResponse, fullRequest) + + const fullBody = await fullResponse.text() + + if (!fullBody.includes(eagerMarker)) { + throw new Error('Expected full sanity response to include the eager marker') + } + + const midStreamId = 'sanity-mid-stream' + const controller = new AbortController() + const midStreamRequest = buildStreamRequest(midStreamId, controller.signal) + const midStreamResponse = await handler.fetch(midStreamRequest) + validateDocumentResponse(midStreamResponse, midStreamRequest) + + const { reader, text } = await readSanityStream( + midStreamResponse, + midStreamRequest, + mode, + midStreamId, + ) + + if (!text.includes(eagerMarker)) { + throw new Error('Expected sanity stream to include the eager marker') + } + + // reader.cancel() is the response-stream cancellation path if the handler + // does not observe Request.signal for this in-process request. + controller.abort() + await cancelReader(reader, mode.cancelMode) + await drainCancellation(mode.drainMode) +} + +async function runAbortedRequestLoop( + handler: StartRequestHandler, + mode: AbortedRequestMode, +) { + for (let index = 0; index < abortedRequestIterations; index++) { + const controller = new AbortController() + const id = `abort-${(abortedRequestCounter++).toString(36)}` + const request = buildStreamRequest(id, controller.signal) + const response = await handler.fetch(request) + validateDocumentResponse(response, request) + + const { reader } = await readLoopStream( + mode.readMode, + response, + request, + id, + ) + controller.abort() + await cancelReader(reader, mode.cancelMode) + await drainCancellation(mode.drainMode) + } +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const mode = abortedRequestModes[framework] + const run = () => runAbortedRequestLoop(handler, mode) + + return { + sanity: () => assertAbortedRequestsSanity(handler, mode), + workloads: [ + { + name: `mem aborted-requests (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/project.json b/benchmarks/memory/server/scenarios/aborted-requests/solid/project.json new file mode 100644 index 0000000000..345fcc0720 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-aborted-requests-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..04ebed80f0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..aaf7dfdd89 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx new file mode 100644 index 0000000000..3aac56e824 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
aborted-requests-index
+} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..95d69ad91d --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx @@ -0,0 +1,126 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { Show, Suspense, createResource } from 'solid-js' +import { + makeAbortedRequestRecords, + type DeferredRecord, + type RecordGroup, +} from '../../../deferred-records' + +const alphaDelayMs = 50 +const betaDelayMs = 75 +const abortProbeAlphaDelayMs = 500 +const abortProbeBetaDelayMs = 750 + +function isAbortProbeId(id: string) { + return id === 'sanity-mid-stream' || id.startsWith('abort-') +} + +function getDelay(id: string, group: RecordGroup) { + if (isAbortProbeId(id)) { + return group === 'alpha' ? abortProbeAlphaDelayMs : abortProbeBetaDelayMs + } + + return group === 'alpha' ? alphaDelayMs : betaDelayMs +} + +function resolveAfterDelay( + delayMs: number, + signal: AbortSignal, + value: () => T, + abortedValue: () => T, +) { + return new Promise((resolve) => { + if (signal.aborted) { + resolve(abortedValue()) + return + } + + const timeoutId = setTimeout(() => { + signal.removeEventListener('abort', onAbort) + resolve(value()) + }, delayMs) + + const onAbort = () => { + clearTimeout(timeoutId) + resolve(abortedValue()) + } + + signal.addEventListener('abort', onAbort, { once: true }) + }) +} + +function makeDeferredRecords( + id: string, + group: RecordGroup, + signal: AbortSignal, +) { + const delayMs = getDelay(id, group) + + return resolveAfterDelay( + delayMs, + signal, + () => makeAbortedRequestRecords(id, group), + () => [], + ) +} + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params, abortController }) => ({ + eager: `eager-${params.id}`, + alpha: makeDeferredRecords(params.id, 'alpha', abortController.signal), + beta: makeDeferredRecords(params.id, 'beta', abortController.signal), + }), + component: StreamComponent, +}) + +function StreamComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data().eager}

+ loading-alpha

+ } + > + +
+ loading-beta

+ } + > + +
+
+ ) +} + +function DeferredRecords(props: { + promise: Promise> + dataBench: string +}) { + const [records] = createResource( + () => props.promise, + (promise) => promise, + ) + + return ( + + {(resolvedRecords) => ( +
    + {resolvedRecords().map((record) => ( +
  • {record.label}
  • + ))} +
+ )} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json new file mode 100644 index 0000000000..4b61264e11 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts new file mode 100644 index 0000000000..d3a374cab7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server aborted-requests (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/project.json b/benchmarks/memory/server/scenarios/aborted-requests/vue/project.json new file mode 100644 index 0000000000..8ca7d84660 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-aborted-requests-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..192f1d6dc7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx new file mode 100644 index 0000000000..af662b29f7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
aborted-requests-index
+} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..d064bf75e9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx @@ -0,0 +1,120 @@ +import { Await, createFileRoute } from '@tanstack/vue-router' +import { Suspense } from 'vue' +import { + makeAbortedRequestRecords, + type DeferredRecord, + type RecordGroup, +} from '../../../deferred-records' + +const alphaDelayMs = 50 +const betaDelayMs = 75 +const abortProbeAlphaDelayMs = 500 +const abortProbeBetaDelayMs = 750 + +function isAbortProbeId(id: string) { + return id === 'sanity-mid-stream' || id.startsWith('abort-') +} + +function getDelay(id: string, group: RecordGroup) { + if (isAbortProbeId(id)) { + return group === 'alpha' ? abortProbeAlphaDelayMs : abortProbeBetaDelayMs + } + + return group === 'alpha' ? alphaDelayMs : betaDelayMs +} + +function resolveAfterDelay( + delayMs: number, + signal: AbortSignal, + value: () => T, + abortedValue: () => T, +) { + return new Promise((resolve) => { + if (signal.aborted) { + resolve(abortedValue()) + return + } + + const timeoutId = setTimeout(() => { + signal.removeEventListener('abort', onAbort) + resolve(value()) + }, delayMs) + + const onAbort = () => { + clearTimeout(timeoutId) + resolve(abortedValue()) + } + + signal.addEventListener('abort', onAbort, { once: true }) + }) +} + +function makeDeferredRecords( + id: string, + group: RecordGroup, + signal: AbortSignal, +) { + const delayMs = getDelay(id, group) + + return resolveAfterDelay( + delayMs, + signal, + () => makeAbortedRequestRecords(id, group), + () => [], + ) +} + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params, abortController }) => ({ + eager: `eager-${params.id}`, + alpha: makeDeferredRecords(params.id, 'alpha', abortController.signal), + beta: makeDeferredRecords(params.id, 'beta', abortController.signal), + }), + component: StreamComponent, +}) + +function StreamComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.value.eager}

+

loading-alpha

+

loading-beta

+ + {{ + default: () => ( + ) => ( +
    + {records.map((record) => ( +
  • {record.label}
  • + ))} +
+ )} + /> + ), + fallback: () => null, + }} +
+ + {{ + default: () => ( + ) => ( +
    + {records.map((record) => ( +
  • {record.label}
  • + ))} +
+ )} + /> + ), + fallback: () => null, + }} +
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts new file mode 100644 index 0000000000..2b42e2be39 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server aborted-requests (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/project.json b/benchmarks/memory/server/scenarios/error-paths/react/project.json new file mode 100644 index 0000000000..6a399fe41e --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-error-paths-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..fd373be6b4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts @@ -0,0 +1,146 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as TargetIdRouteImport } from './routes/target.$id' +import { Route as MissingIdRouteImport } from './routes/missing.$id' +import { Route as FromIdRouteImport } from './routes/from.$id' +import { Route as BoomIdRouteImport } from './routes/boom.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const TargetIdRoute = TargetIdRouteImport.update({ + id: '/target/$id', + path: '/target/$id', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FromIdRoute = FromIdRouteImport.update({ + id: '/from/$id', + path: '/from/$id', + getParentRoute: () => rootRouteImport, +} as any) +const BoomIdRoute = BoomIdRouteImport.update({ + id: '/boom/$id', + path: '/boom/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + id: + | '__root__' + | '/' + | '/boom/$id' + | '/from/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BoomIdRoute: typeof BoomIdRoute + FromIdRoute: typeof FromIdRoute + MissingIdRoute: typeof MissingIdRoute + TargetIdRoute: typeof TargetIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/target/$id': { + id: '/target/$id' + path: '/target/$id' + fullPath: '/target/$id' + preLoaderRoute: typeof TargetIdRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + '/from/$id': { + id: '/from/$id' + path: '/from/$id' + fullPath: '/from/$id' + preLoaderRoute: typeof FromIdRouteImport + parentRoute: typeof rootRouteImport + } + '/boom/$id': { + id: '/boom/$id' + path: '/boom/$id' + fullPath: '/boom/$id' + preLoaderRoute: typeof BoomIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BoomIdRoute: BoomIdRoute, + FromIdRoute: FromIdRoute, + MissingIdRoute: MissingIdRoute, + TargetIdRoute: TargetIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx new file mode 100644 index 0000000000..e31bd0abf7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx @@ -0,0 +1,23 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + defaultNotFoundComponent: DefaultNotFound, + }) +} + +// Without this, every unmatched-URL request logs a router warning in +// non-production ad-hoc runs. +function DefaultNotFound() { + return

error-paths-unmatched

+} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx new file mode 100644 index 0000000000..fafa81a9bb --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/boom/$id')({ + loader: ({ params }) => { + throw new Error(`boom-${params.id}`) + }, + errorComponent: BoomErrorComponent, + component: BoomComponent, +}) + +function BoomErrorComponent() { + return
error-paths-error-boundary
+} + +function BoomComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx new file mode 100644 index 0000000000..4c45e76c92 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +export const Route = createFileRoute('/from/$id')({ + loader: ({ params }) => { + const { id } = params + + throw redirect({ to: '/target/$id', params: { id }, statusCode: 302 }) + }, + component: FromComponent, +}) + +function FromComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx new file mode 100644 index 0000000000..c5492486b4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
error-paths-index
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..84640e9712 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx @@ -0,0 +1,19 @@ +import { createFileRoute, notFound } from '@tanstack/react-router' + +export const Route = createFileRoute('/missing/$id')({ + loader: () => { + throw notFound() + }, + notFoundComponent: MissingNotFoundComponent, + component: MissingComponent, +}) + +function MissingNotFoundComponent() { + return ( +
error-paths-not-found-boundary
+ ) +} + +function MissingComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx new file mode 100644 index 0000000000..1c13394cb2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/target/$id')({ + component: TargetComponent, +}) + +function TargetComponent() { + const params = Route.useParams() + + return
{`target-${params.id}`}
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts b/benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts new file mode 100644 index 0000000000..2a4fd9818a --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server error-paths (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/shared.ts b/benchmarks/memory/server/scenarios/error-paths/shared.ts new file mode 100644 index 0000000000..d4c1877465 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/shared.ts @@ -0,0 +1,192 @@ +import { + createDeterministicRandom, + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +const errorPathsIterations = 50 +const redirectSeed = 0xdecafbad +const notFoundSeed = 0xdecafb0d +const errorSeed = 0xdecafbed +const unmatchedSeed = 0xdecaf00d +const redirectStatus = 302 +const notFoundStatus = 404 +const errorStatus = 500 +// Module-level so each error-path bench keeps advancing across runner invocations. +const redirectRandom = createDeterministicRandom(redirectSeed) +const notFoundRandom = createDeterministicRandom(notFoundSeed) +const errorRandom = createDeterministicRandom(errorSeed) +const unmatchedRandom = createDeterministicRandom(unmatchedSeed) +let redirectCounter = 0 +let notFoundCounter = 0 +let errorCounter = 0 +let unmatchedCounter = 0 + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildRedirectRequest(random: () => number) { + const id = `${(redirectCounter++).toString(36)}-${randomSegment(random)}` + + return new Request(`http://localhost/from/${id}`, requestInit) +} + +function buildNotFoundRequest(random: () => number) { + const id = `${(notFoundCounter++).toString(36)}-${randomSegment(random)}` + + return new Request(`http://localhost/missing/${id}`, requestInit) +} + +function buildErrorRequest(random: () => number) { + const id = `${(errorCounter++).toString(36)}-${randomSegment(random)}` + + return new Request(`http://localhost/boom/${id}`, requestInit) +} + +function buildUnmatchedRequest(random: () => number) { + const id = `${(unmatchedCounter++).toString(36)}-${randomSegment(random)}` + + return new Request(`http://localhost/nope/${id}`, requestInit) +} + +function getRequestId(request: Request) { + const id = new URL(request.url).pathname.split('/').pop() + + if (!id) { + throw new Error(`Expected request id in ${request.url}`) + } + + return id +} + +function validateRedirectResponse(response: Response, request: Request) { + if (response.status !== redirectStatus) { + throw new Error( + `Expected status ${redirectStatus} for ${request.url}, got ${response.status}`, + ) + } + + const id = getRequestId(request) + const location = response.headers.get('location') + + if (location !== `/target/${id}`) { + throw new Error(`Expected redirect location /target/${id}, got ${location}`) + } +} + +function validateNotFoundResponse(response: Response, request: Request) { + if (response.status !== notFoundStatus) { + throw new Error( + `Expected status ${notFoundStatus} for ${request.url}, got ${response.status}`, + ) + } +} + +function validateErrorResponse(response: Response, request: Request) { + if (response.status !== errorStatus) { + throw new Error( + `Expected status ${errorStatus} for ${request.url}, got ${response.status}`, + ) + } +} + +async function assertStatusSanity( + handler: StartRequestHandler, + request: Request, + validateResponse: (response: Response, request: Request) => void, +) { + const response = await handler.fetch(request) + validateResponse(response, request) + await response.text() +} + +async function assertErrorPathsSanity(handler: StartRequestHandler) { + await assertStatusSanity( + handler, + new Request('http://localhost/from/sanity-redirect', requestInit), + validateRedirectResponse, + ) + await assertStatusSanity( + handler, + new Request('http://localhost/missing/sanity-missing', requestInit), + validateNotFoundResponse, + ) + await assertStatusSanity( + handler, + new Request('http://localhost/boom/sanity-error', requestInit), + validateErrorResponse, + ) + await assertStatusSanity( + handler, + new Request('http://localhost/nope/sanity-unmatched', requestInit), + validateNotFoundResponse, + ) +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const runRedirect = () => + runSequentialRequestLoop(handler, { + random: redirectRandom, + iterations: errorPathsIterations, + buildRequest: buildRedirectRequest, + validateResponse: validateRedirectResponse, + }) + + const runNotFound = () => + runSequentialRequestLoop(handler, { + random: notFoundRandom, + iterations: errorPathsIterations, + buildRequest: buildNotFoundRequest, + validateResponse: validateNotFoundResponse, + }) + + const runError = () => + runSequentialRequestLoop(handler, { + random: errorRandom, + iterations: errorPathsIterations, + buildRequest: buildErrorRequest, + validateResponse: validateErrorResponse, + }) + + const runUnmatched = () => + runSequentialRequestLoop(handler, { + random: unmatchedRandom, + iterations: errorPathsIterations, + buildRequest: buildUnmatchedRequest, + validateResponse: validateNotFoundResponse, + }) + + return { + sanity: () => assertErrorPathsSanity(handler), + workloads: [ + { + name: `mem error-paths redirect (${framework})`, + run: runRedirect, + }, + { + name: `mem error-paths not-found (${framework})`, + run: runNotFound, + }, + { + name: `mem error-paths error (${framework})`, + run: runError, + }, + { + name: `mem error-paths unmatched (${framework})`, + run: runUnmatched, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/project.json b/benchmarks/memory/server/scenarios/error-paths/solid/project.json new file mode 100644 index 0000000000..77fcfaf578 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-error-paths-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts b/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..32a4d56d3a --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts @@ -0,0 +1,146 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as TargetIdRouteImport } from './routes/target.$id' +import { Route as MissingIdRouteImport } from './routes/missing.$id' +import { Route as FromIdRouteImport } from './routes/from.$id' +import { Route as BoomIdRouteImport } from './routes/boom.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const TargetIdRoute = TargetIdRouteImport.update({ + id: '/target/$id', + path: '/target/$id', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FromIdRoute = FromIdRouteImport.update({ + id: '/from/$id', + path: '/from/$id', + getParentRoute: () => rootRouteImport, +} as any) +const BoomIdRoute = BoomIdRouteImport.update({ + id: '/boom/$id', + path: '/boom/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + id: + | '__root__' + | '/' + | '/boom/$id' + | '/from/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BoomIdRoute: typeof BoomIdRoute + FromIdRoute: typeof FromIdRoute + MissingIdRoute: typeof MissingIdRoute + TargetIdRoute: typeof TargetIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/target/$id': { + id: '/target/$id' + path: '/target/$id' + fullPath: '/target/$id' + preLoaderRoute: typeof TargetIdRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + '/from/$id': { + id: '/from/$id' + path: '/from/$id' + fullPath: '/from/$id' + preLoaderRoute: typeof FromIdRouteImport + parentRoute: typeof rootRouteImport + } + '/boom/$id': { + id: '/boom/$id' + path: '/boom/$id' + fullPath: '/boom/$id' + preLoaderRoute: typeof BoomIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BoomIdRoute: BoomIdRoute, + FromIdRoute: FromIdRoute, + MissingIdRoute: MissingIdRoute, + TargetIdRoute: TargetIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx new file mode 100644 index 0000000000..ae51f8b5e9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx @@ -0,0 +1,23 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + defaultNotFoundComponent: DefaultNotFound, + }) +} + +// Without this, every unmatched-URL request logs a router warning in +// non-production ad-hoc runs. +function DefaultNotFound() { + return

error-paths-unmatched

+} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..aaf7dfdd89 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx new file mode 100644 index 0000000000..bebc638caa --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/boom/$id')({ + loader: ({ params }) => { + throw new Error(`boom-${params.id}`) + }, + errorComponent: BoomErrorComponent, + component: BoomComponent, +}) + +function BoomErrorComponent() { + return
error-paths-error-boundary
+} + +function BoomComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx new file mode 100644 index 0000000000..e96b7864f2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/solid-router' + +export const Route = createFileRoute('/from/$id')({ + loader: ({ params }) => { + const { id } = params + + throw redirect({ to: '/target/$id', params: { id }, statusCode: 302 }) + }, + component: FromComponent, +}) + +function FromComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx new file mode 100644 index 0000000000..43282af801 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
error-paths-index
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..1b9ec5e29d --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx @@ -0,0 +1,19 @@ +import { createFileRoute, notFound } from '@tanstack/solid-router' + +export const Route = createFileRoute('/missing/$id')({ + loader: () => { + throw notFound() + }, + notFoundComponent: MissingNotFoundComponent, + component: MissingComponent, +}) + +function MissingNotFoundComponent() { + return ( +
error-paths-not-found-boundary
+ ) +} + +function MissingComponent() { + return null +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx new file mode 100644 index 0000000000..4196fc1183 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/target/$id')({ + component: TargetComponent, +}) + +function TargetComponent() { + const params = Route.useParams() + + return
{`target-${params().id}`}
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json new file mode 100644 index 0000000000..4b61264e11 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts b/benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts new file mode 100644 index 0000000000..7f89abb099 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server error-paths (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/project.json b/benchmarks/memory/server/scenarios/error-paths/vue/project.json new file mode 100644 index 0000000000..8472470bde --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-error-paths-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts b/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..ea5bafa386 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts @@ -0,0 +1,146 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as TargetIdRouteImport } from './routes/target.$id' +import { Route as MissingIdRouteImport } from './routes/missing.$id' +import { Route as FromIdRouteImport } from './routes/from.$id' +import { Route as BoomIdRouteImport } from './routes/boom.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const TargetIdRoute = TargetIdRouteImport.update({ + id: '/target/$id', + path: '/target/$id', + getParentRoute: () => rootRouteImport, +} as any) +const MissingIdRoute = MissingIdRouteImport.update({ + id: '/missing/$id', + path: '/missing/$id', + getParentRoute: () => rootRouteImport, +} as any) +const FromIdRoute = FromIdRouteImport.update({ + id: '/from/$id', + path: '/from/$id', + getParentRoute: () => rootRouteImport, +} as any) +const BoomIdRoute = BoomIdRouteImport.update({ + id: '/boom/$id', + path: '/boom/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/boom/$id': typeof BoomIdRoute + '/from/$id': typeof FromIdRoute + '/missing/$id': typeof MissingIdRoute + '/target/$id': typeof TargetIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/boom/$id' | '/from/$id' | '/missing/$id' | '/target/$id' + id: + | '__root__' + | '/' + | '/boom/$id' + | '/from/$id' + | '/missing/$id' + | '/target/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + BoomIdRoute: typeof BoomIdRoute + FromIdRoute: typeof FromIdRoute + MissingIdRoute: typeof MissingIdRoute + TargetIdRoute: typeof TargetIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/target/$id': { + id: '/target/$id' + path: '/target/$id' + fullPath: '/target/$id' + preLoaderRoute: typeof TargetIdRouteImport + parentRoute: typeof rootRouteImport + } + '/missing/$id': { + id: '/missing/$id' + path: '/missing/$id' + fullPath: '/missing/$id' + preLoaderRoute: typeof MissingIdRouteImport + parentRoute: typeof rootRouteImport + } + '/from/$id': { + id: '/from/$id' + path: '/from/$id' + fullPath: '/from/$id' + preLoaderRoute: typeof FromIdRouteImport + parentRoute: typeof rootRouteImport + } + '/boom/$id': { + id: '/boom/$id' + path: '/boom/$id' + fullPath: '/boom/$id' + preLoaderRoute: typeof BoomIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + BoomIdRoute: BoomIdRoute, + FromIdRoute: FromIdRoute, + MissingIdRoute: MissingIdRoute, + TargetIdRoute: TargetIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx new file mode 100644 index 0000000000..d933b93df2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/boom/$id')({ + loader: ({ params }) => { + throw new Error(`boom-${params.id}`) + }, + errorComponent: BoomErrorComponent, + component: BoomComponent, +}) + +function BoomErrorComponent() { + return
error-paths-error-boundary
+} + +function BoomComponent() { + return <> +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx new file mode 100644 index 0000000000..6ffba96bce --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx @@ -0,0 +1,14 @@ +import { createFileRoute, redirect } from '@tanstack/vue-router' + +export const Route = createFileRoute('/from/$id')({ + loader: ({ params }) => { + const { id } = params + + throw redirect({ to: '/target/$id', params: { id }, statusCode: 302 }) + }, + component: FromComponent, +}) + +function FromComponent() { + return <> +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx new file mode 100644 index 0000000000..53fb604c60 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
error-paths-index
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx new file mode 100644 index 0000000000..0e4972f5c6 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx @@ -0,0 +1,19 @@ +import { createFileRoute, notFound } from '@tanstack/vue-router' + +export const Route = createFileRoute('/missing/$id')({ + loader: () => { + throw notFound() + }, + notFoundComponent: MissingNotFoundComponent, + component: MissingComponent, +}) + +function MissingNotFoundComponent() { + return ( +
error-paths-not-found-boundary
+ ) +} + +function MissingComponent() { + return <> +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx new file mode 100644 index 0000000000..17ade6bf5b --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/target/$id')({ + component: TargetComponent, +}) + +function TargetComponent() { + const params = Route.useParams() + + return
{`target-${params.value.id}`}
+} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts b/benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts new file mode 100644 index 0000000000..b5cf79924a --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server error-paths (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/large-page-data.ts new file mode 100644 index 0000000000..8f77c58731 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/large-page-data.ts @@ -0,0 +1,111 @@ +export interface LargePageRecord { + id: string + name: string + description: string +} + +export interface LargePageLevelData { + level: number + marker: string + records: Array +} + +const recordCount = 200 +const descriptionLength = 104 + +export function makeLargePageLevelData(level: number, seed: number) { + const random = createDeterministicRandom(seed) + + return { + level, + marker: makeLargePageMarker(level), + records: Array.from({ length: recordCount }, (_, index) => { + const idToken = randomSegment(random) + const descriptionTokenA = randomSegment(random) + const descriptionTokenB = randomSegment(random) + + return { + id: `l${level}-${index}-${idToken}`, + name: makeLargePageRecordName(level, index), + description: makeDescription( + level, + index, + descriptionTokenA, + descriptionTokenB, + ), + } + }), + } satisfies LargePageLevelData +} + +export function makeLargePageHead(loaderData: LargePageLevelData | undefined) { + if (!loaderData) { + return { + meta: [{ title: 'Peak Large Page' }], + } + } + + const first = loaderData.records[0]! + const last = loaderData.records[loaderData.records.length - 1]! + + return { + meta: [ + { title: `Peak Large Page L${loaderData.level} ${first.name}` }, + { + name: `peak-large-page-l${loaderData.level}-count`, + content: String(loaderData.records.length), + }, + { + name: `peak-large-page-l${loaderData.level}-first-id`, + content: first.id, + }, + { + name: `peak-large-page-l${loaderData.level}-first-name`, + content: first.name, + }, + { + name: `peak-large-page-l${loaderData.level}-last-id`, + content: last.id, + }, + { + name: `peak-large-page-l${loaderData.level}-description`, + content: first.description, + }, + ], + } +} + +function makeLargePageRecordName(level: number, index: number) { + return `peak-large-page-l${level}-record-${index}` +} + +function makeLargePageMarker(level: number) { + return `peak-large-page-level-${level}` +} + +function makeDescription( + level: number, + index: number, + tokenA: string, + tokenB: string, +) { + return `Level ${level} record ${index} uses seeded fragments ${tokenA} and ${tokenB} to keep a fresh deterministic loader payload.`.padEnd( + descriptionLength, + 'x', + ) +} + +// Local copy of the LCG from benchmarks/memory/server/bench-utils.ts - app +// source cannot import from outside the app root. Keep in sync. +function createDeterministicRandom(seed: number) { + let state = seed >>> 0 + + return () => { + state = (state * 1664525 + 1013904223) >>> 0 + return state / 0x100000000 + } +} + +function randomSegment(random: () => number) { + return Math.floor(random() * 1_000_000_000).toString(36) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/project.json b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json new file mode 100644 index 0000000000..699a243a03 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-peak-large-page-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..60a1bd1fbc --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts @@ -0,0 +1,305 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as L1RouteImport } from './routes/l1' +import { Route as IndexRouteImport } from './routes/index' +import { Route as L1L2RouteImport } from './routes/l1.l2' +import { Route as L1L2L3RouteImport } from './routes/l1.l2.l3' +import { Route as L1L2L3L4RouteImport } from './routes/l1.l2.l3.l4' +import { Route as L1L2L3L4L5RouteImport } from './routes/l1.l2.l3.l4.l5' +import { Route as L1L2L3L4L5L6RouteImport } from './routes/l1.l2.l3.l4.l5.l6' +import { Route as L1L2L3L4L5L6L7RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7' +import { Route as L1L2L3L4L5L6L7L8RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7.l8' + +const L1Route = L1RouteImport.update({ + id: '/l1', + path: '/l1', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const L1L2Route = L1L2RouteImport.update({ + id: '/l2', + path: '/l2', + getParentRoute: () => L1Route, +} as any) +const L1L2L3Route = L1L2L3RouteImport.update({ + id: '/l3', + path: '/l3', + getParentRoute: () => L1L2Route, +} as any) +const L1L2L3L4Route = L1L2L3L4RouteImport.update({ + id: '/l4', + path: '/l4', + getParentRoute: () => L1L2L3Route, +} as any) +const L1L2L3L4L5Route = L1L2L3L4L5RouteImport.update({ + id: '/l5', + path: '/l5', + getParentRoute: () => L1L2L3L4Route, +} as any) +const L1L2L3L4L5L6Route = L1L2L3L4L5L6RouteImport.update({ + id: '/l6', + path: '/l6', + getParentRoute: () => L1L2L3L4L5Route, +} as any) +const L1L2L3L4L5L6L7Route = L1L2L3L4L5L6L7RouteImport.update({ + id: '/l7', + path: '/l7', + getParentRoute: () => L1L2L3L4L5L6Route, +} as any) +const L1L2L3L4L5L6L7L8Route = L1L2L3L4L5L6L7L8RouteImport.update({ + id: '/l8', + path: '/l8', + getParentRoute: () => L1L2L3L4L5L6L7Route, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + id: + | '__root__' + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + L1Route: typeof L1RouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/l1': { + id: '/l1' + path: '/l1' + fullPath: '/l1' + preLoaderRoute: typeof L1RouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l1/l2': { + id: '/l1/l2' + path: '/l2' + fullPath: '/l1/l2' + preLoaderRoute: typeof L1L2RouteImport + parentRoute: typeof L1Route + } + '/l1/l2/l3': { + id: '/l1/l2/l3' + path: '/l3' + fullPath: '/l1/l2/l3' + preLoaderRoute: typeof L1L2L3RouteImport + parentRoute: typeof L1L2Route + } + '/l1/l2/l3/l4': { + id: '/l1/l2/l3/l4' + path: '/l4' + fullPath: '/l1/l2/l3/l4' + preLoaderRoute: typeof L1L2L3L4RouteImport + parentRoute: typeof L1L2L3Route + } + '/l1/l2/l3/l4/l5': { + id: '/l1/l2/l3/l4/l5' + path: '/l5' + fullPath: '/l1/l2/l3/l4/l5' + preLoaderRoute: typeof L1L2L3L4L5RouteImport + parentRoute: typeof L1L2L3L4Route + } + '/l1/l2/l3/l4/l5/l6': { + id: '/l1/l2/l3/l4/l5/l6' + path: '/l6' + fullPath: '/l1/l2/l3/l4/l5/l6' + preLoaderRoute: typeof L1L2L3L4L5L6RouteImport + parentRoute: typeof L1L2L3L4L5Route + } + '/l1/l2/l3/l4/l5/l6/l7': { + id: '/l1/l2/l3/l4/l5/l6/l7' + path: '/l7' + fullPath: '/l1/l2/l3/l4/l5/l6/l7' + preLoaderRoute: typeof L1L2L3L4L5L6L7RouteImport + parentRoute: typeof L1L2L3L4L5L6Route + } + '/l1/l2/l3/l4/l5/l6/l7/l8': { + id: '/l1/l2/l3/l4/l5/l6/l7/l8' + path: '/l8' + fullPath: '/l1/l2/l3/l4/l5/l6/l7/l8' + preLoaderRoute: typeof L1L2L3L4L5L6L7L8RouteImport + parentRoute: typeof L1L2L3L4L5L6L7Route + } + } +} + +interface L1L2L3L4L5L6L7RouteChildren { + L1L2L3L4L5L6L7L8Route: typeof L1L2L3L4L5L6L7L8Route +} + +const L1L2L3L4L5L6L7RouteChildren: L1L2L3L4L5L6L7RouteChildren = { + L1L2L3L4L5L6L7L8Route: L1L2L3L4L5L6L7L8Route, +} + +const L1L2L3L4L5L6L7RouteWithChildren = L1L2L3L4L5L6L7Route._addFileChildren( + L1L2L3L4L5L6L7RouteChildren, +) + +interface L1L2L3L4L5L6RouteChildren { + L1L2L3L4L5L6L7Route: typeof L1L2L3L4L5L6L7RouteWithChildren +} + +const L1L2L3L4L5L6RouteChildren: L1L2L3L4L5L6RouteChildren = { + L1L2L3L4L5L6L7Route: L1L2L3L4L5L6L7RouteWithChildren, +} + +const L1L2L3L4L5L6RouteWithChildren = L1L2L3L4L5L6Route._addFileChildren( + L1L2L3L4L5L6RouteChildren, +) + +interface L1L2L3L4L5RouteChildren { + L1L2L3L4L5L6Route: typeof L1L2L3L4L5L6RouteWithChildren +} + +const L1L2L3L4L5RouteChildren: L1L2L3L4L5RouteChildren = { + L1L2L3L4L5L6Route: L1L2L3L4L5L6RouteWithChildren, +} + +const L1L2L3L4L5RouteWithChildren = L1L2L3L4L5Route._addFileChildren( + L1L2L3L4L5RouteChildren, +) + +interface L1L2L3L4RouteChildren { + L1L2L3L4L5Route: typeof L1L2L3L4L5RouteWithChildren +} + +const L1L2L3L4RouteChildren: L1L2L3L4RouteChildren = { + L1L2L3L4L5Route: L1L2L3L4L5RouteWithChildren, +} + +const L1L2L3L4RouteWithChildren = L1L2L3L4Route._addFileChildren( + L1L2L3L4RouteChildren, +) + +interface L1L2L3RouteChildren { + L1L2L3L4Route: typeof L1L2L3L4RouteWithChildren +} + +const L1L2L3RouteChildren: L1L2L3RouteChildren = { + L1L2L3L4Route: L1L2L3L4RouteWithChildren, +} + +const L1L2L3RouteWithChildren = + L1L2L3Route._addFileChildren(L1L2L3RouteChildren) + +interface L1L2RouteChildren { + L1L2L3Route: typeof L1L2L3RouteWithChildren +} + +const L1L2RouteChildren: L1L2RouteChildren = { + L1L2L3Route: L1L2L3RouteWithChildren, +} + +const L1L2RouteWithChildren = L1L2Route._addFileChildren(L1L2RouteChildren) + +interface L1RouteChildren { + L1L2Route: typeof L1L2RouteWithChildren +} + +const L1RouteChildren: L1RouteChildren = { + L1L2Route: L1L2RouteWithChildren, +} + +const L1RouteWithChildren = L1Route._addFileChildren(L1RouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + L1Route: L1RouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx new file mode 100644 index 0000000000..25099720e4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
peak-large-page-index
+} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx new file mode 100644 index 0000000000..0651ba886d --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ + loader: () => makeLargePageLevelData(8, 0x5eed_1008), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelEightComponent, +}) + +function LevelEightComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx new file mode 100644 index 0000000000..1f89e6f454 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ + loader: () => makeLargePageLevelData(7, 0x5eed_1007), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSevenComponent, +}) + +function LevelSevenComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx new file mode 100644 index 0000000000..1a8c98ceb2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ + loader: () => makeLargePageLevelData(6, 0x5eed_1006), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSixComponent, +}) + +function LevelSixComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx new file mode 100644 index 0000000000..3e9afad0b9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ + loader: () => makeLargePageLevelData(5, 0x5eed_1005), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFiveComponent, +}) + +function LevelFiveComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx new file mode 100644 index 0000000000..73c29bc402 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4')({ + loader: () => makeLargePageLevelData(4, 0x5eed_1004), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFourComponent, +}) + +function LevelFourComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx new file mode 100644 index 0000000000..e5b57463ad --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3')({ + loader: () => makeLargePageLevelData(3, 0x5eed_1003), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelThreeComponent, +}) + +function LevelThreeComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx new file mode 100644 index 0000000000..18999233cc --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2')({ + loader: () => makeLargePageLevelData(2, 0x5eed_1002), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelTwoComponent, +}) + +function LevelTwoComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx new file mode 100644 index 0000000000..8fa7a5e0dc --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/react-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1')({ + loader: () => makeLargePageLevelData(1, 0x5eed_1001), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelOneComponent, +}) + +function LevelOneComponent() { + const data = Route.useLoaderData() + const first = data.records[0]! + + return ( +
+

{data.marker}

+

records: {data.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts new file mode 100644 index 0000000000..7d4fa70196 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server peak-large-page (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/shared.ts b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts new file mode 100644 index 0000000000..36a91fa49d --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts @@ -0,0 +1,68 @@ +import { runSequentialRequestLoop } from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +const benchmarkSeed = 0x5eed_0005 +const peakLargePageIterations = 20 +const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8' +const levelEightMarker = 'data-bench="peak-large-page-level-8"' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildPeakLargePageRequest() { + return new Request(peakLargePageUrl, requestInit) +} + +function validatePeakLargePageResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } +} + +function validatePeakLargePageBody(body: string) { + if (!body.includes(levelEightMarker)) { + throw new Error('Expected peak-large-page level-8 marker in response body') + } +} + +async function assertPeakLargePageSanity(handler: StartRequestHandler) { + const request = new Request(peakLargePageUrl, requestInit) + const response = await handler.fetch(request) + const body = await response.text() + + validatePeakLargePageResponse(response, request) + validatePeakLargePageBody(body) +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: peakLargePageIterations, + buildRequest: buildPeakLargePageRequest, + validateResponse: validatePeakLargePageResponse, + }) + + return { + sanity: () => assertPeakLargePageSanity(handler), + workloads: [ + { + name: `mem peak-large-page (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/project.json b/benchmarks/memory/server/scenarios/peak-large-page/solid/project.json new file mode 100644 index 0000000000..85e8f83224 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-peak-large-page-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..c3ccdaaa0e --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts @@ -0,0 +1,305 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as L1RouteImport } from './routes/l1' +import { Route as IndexRouteImport } from './routes/index' +import { Route as L1L2RouteImport } from './routes/l1.l2' +import { Route as L1L2L3RouteImport } from './routes/l1.l2.l3' +import { Route as L1L2L3L4RouteImport } from './routes/l1.l2.l3.l4' +import { Route as L1L2L3L4L5RouteImport } from './routes/l1.l2.l3.l4.l5' +import { Route as L1L2L3L4L5L6RouteImport } from './routes/l1.l2.l3.l4.l5.l6' +import { Route as L1L2L3L4L5L6L7RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7' +import { Route as L1L2L3L4L5L6L7L8RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7.l8' + +const L1Route = L1RouteImport.update({ + id: '/l1', + path: '/l1', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const L1L2Route = L1L2RouteImport.update({ + id: '/l2', + path: '/l2', + getParentRoute: () => L1Route, +} as any) +const L1L2L3Route = L1L2L3RouteImport.update({ + id: '/l3', + path: '/l3', + getParentRoute: () => L1L2Route, +} as any) +const L1L2L3L4Route = L1L2L3L4RouteImport.update({ + id: '/l4', + path: '/l4', + getParentRoute: () => L1L2L3Route, +} as any) +const L1L2L3L4L5Route = L1L2L3L4L5RouteImport.update({ + id: '/l5', + path: '/l5', + getParentRoute: () => L1L2L3L4Route, +} as any) +const L1L2L3L4L5L6Route = L1L2L3L4L5L6RouteImport.update({ + id: '/l6', + path: '/l6', + getParentRoute: () => L1L2L3L4L5Route, +} as any) +const L1L2L3L4L5L6L7Route = L1L2L3L4L5L6L7RouteImport.update({ + id: '/l7', + path: '/l7', + getParentRoute: () => L1L2L3L4L5L6Route, +} as any) +const L1L2L3L4L5L6L7L8Route = L1L2L3L4L5L6L7L8RouteImport.update({ + id: '/l8', + path: '/l8', + getParentRoute: () => L1L2L3L4L5L6L7Route, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + id: + | '__root__' + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + L1Route: typeof L1RouteWithChildren +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/l1': { + id: '/l1' + path: '/l1' + fullPath: '/l1' + preLoaderRoute: typeof L1RouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l1/l2': { + id: '/l1/l2' + path: '/l2' + fullPath: '/l1/l2' + preLoaderRoute: typeof L1L2RouteImport + parentRoute: typeof L1Route + } + '/l1/l2/l3': { + id: '/l1/l2/l3' + path: '/l3' + fullPath: '/l1/l2/l3' + preLoaderRoute: typeof L1L2L3RouteImport + parentRoute: typeof L1L2Route + } + '/l1/l2/l3/l4': { + id: '/l1/l2/l3/l4' + path: '/l4' + fullPath: '/l1/l2/l3/l4' + preLoaderRoute: typeof L1L2L3L4RouteImport + parentRoute: typeof L1L2L3Route + } + '/l1/l2/l3/l4/l5': { + id: '/l1/l2/l3/l4/l5' + path: '/l5' + fullPath: '/l1/l2/l3/l4/l5' + preLoaderRoute: typeof L1L2L3L4L5RouteImport + parentRoute: typeof L1L2L3L4Route + } + '/l1/l2/l3/l4/l5/l6': { + id: '/l1/l2/l3/l4/l5/l6' + path: '/l6' + fullPath: '/l1/l2/l3/l4/l5/l6' + preLoaderRoute: typeof L1L2L3L4L5L6RouteImport + parentRoute: typeof L1L2L3L4L5Route + } + '/l1/l2/l3/l4/l5/l6/l7': { + id: '/l1/l2/l3/l4/l5/l6/l7' + path: '/l7' + fullPath: '/l1/l2/l3/l4/l5/l6/l7' + preLoaderRoute: typeof L1L2L3L4L5L6L7RouteImport + parentRoute: typeof L1L2L3L4L5L6Route + } + '/l1/l2/l3/l4/l5/l6/l7/l8': { + id: '/l1/l2/l3/l4/l5/l6/l7/l8' + path: '/l8' + fullPath: '/l1/l2/l3/l4/l5/l6/l7/l8' + preLoaderRoute: typeof L1L2L3L4L5L6L7L8RouteImport + parentRoute: typeof L1L2L3L4L5L6L7Route + } + } +} + +interface L1L2L3L4L5L6L7RouteChildren { + L1L2L3L4L5L6L7L8Route: typeof L1L2L3L4L5L6L7L8Route +} + +const L1L2L3L4L5L6L7RouteChildren: L1L2L3L4L5L6L7RouteChildren = { + L1L2L3L4L5L6L7L8Route: L1L2L3L4L5L6L7L8Route, +} + +const L1L2L3L4L5L6L7RouteWithChildren = L1L2L3L4L5L6L7Route._addFileChildren( + L1L2L3L4L5L6L7RouteChildren, +) + +interface L1L2L3L4L5L6RouteChildren { + L1L2L3L4L5L6L7Route: typeof L1L2L3L4L5L6L7RouteWithChildren +} + +const L1L2L3L4L5L6RouteChildren: L1L2L3L4L5L6RouteChildren = { + L1L2L3L4L5L6L7Route: L1L2L3L4L5L6L7RouteWithChildren, +} + +const L1L2L3L4L5L6RouteWithChildren = L1L2L3L4L5L6Route._addFileChildren( + L1L2L3L4L5L6RouteChildren, +) + +interface L1L2L3L4L5RouteChildren { + L1L2L3L4L5L6Route: typeof L1L2L3L4L5L6RouteWithChildren +} + +const L1L2L3L4L5RouteChildren: L1L2L3L4L5RouteChildren = { + L1L2L3L4L5L6Route: L1L2L3L4L5L6RouteWithChildren, +} + +const L1L2L3L4L5RouteWithChildren = L1L2L3L4L5Route._addFileChildren( + L1L2L3L4L5RouteChildren, +) + +interface L1L2L3L4RouteChildren { + L1L2L3L4L5Route: typeof L1L2L3L4L5RouteWithChildren +} + +const L1L2L3L4RouteChildren: L1L2L3L4RouteChildren = { + L1L2L3L4L5Route: L1L2L3L4L5RouteWithChildren, +} + +const L1L2L3L4RouteWithChildren = L1L2L3L4Route._addFileChildren( + L1L2L3L4RouteChildren, +) + +interface L1L2L3RouteChildren { + L1L2L3L4Route: typeof L1L2L3L4RouteWithChildren +} + +const L1L2L3RouteChildren: L1L2L3RouteChildren = { + L1L2L3L4Route: L1L2L3L4RouteWithChildren, +} + +const L1L2L3RouteWithChildren = + L1L2L3Route._addFileChildren(L1L2L3RouteChildren) + +interface L1L2RouteChildren { + L1L2L3Route: typeof L1L2L3RouteWithChildren +} + +const L1L2RouteChildren: L1L2RouteChildren = { + L1L2L3Route: L1L2L3RouteWithChildren, +} + +const L1L2RouteWithChildren = L1L2Route._addFileChildren(L1L2RouteChildren) + +interface L1RouteChildren { + L1L2Route: typeof L1L2RouteWithChildren +} + +const L1RouteChildren: L1RouteChildren = { + L1L2Route: L1L2RouteWithChildren, +} + +const L1RouteWithChildren = L1Route._addFileChildren(L1RouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + L1Route: L1RouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..aaf7dfdd89 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx new file mode 100644 index 0000000000..111ffaa36a --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
peak-large-page-index
+} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx new file mode 100644 index 0000000000..b0f94893fb --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ + loader: () => makeLargePageLevelData(8, 0x5eed_1008), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelEightComponent, +}) + +function LevelEightComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx new file mode 100644 index 0000000000..f8a3819c85 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ + loader: () => makeLargePageLevelData(7, 0x5eed_1007), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSevenComponent, +}) + +function LevelSevenComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx new file mode 100644 index 0000000000..7a438ddbd2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ + loader: () => makeLargePageLevelData(6, 0x5eed_1006), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSixComponent, +}) + +function LevelSixComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx new file mode 100644 index 0000000000..2e05985ac5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ + loader: () => makeLargePageLevelData(5, 0x5eed_1005), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFiveComponent, +}) + +function LevelFiveComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx new file mode 100644 index 0000000000..4f38f0cb76 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4')({ + loader: () => makeLargePageLevelData(4, 0x5eed_1004), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFourComponent, +}) + +function LevelFourComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx new file mode 100644 index 0000000000..3f1f113fe3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3')({ + loader: () => makeLargePageLevelData(3, 0x5eed_1003), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelThreeComponent, +}) + +function LevelThreeComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx new file mode 100644 index 0000000000..bd338fd53f --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2')({ + loader: () => makeLargePageLevelData(2, 0x5eed_1002), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelTwoComponent, +}) + +function LevelTwoComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx new file mode 100644 index 0000000000..9dcfd7448a --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/solid-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1')({ + loader: () => makeLargePageLevelData(1, 0x5eed_1001), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelOneComponent, +}) + +function LevelOneComponent() { + const data = Route.useLoaderData() + const first = () => data().records[0]! + + return ( +
+

{data().marker}

+

records: {data().records.length}

+
+

{first().name}

+

{first().id}

+

{first().description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json new file mode 100644 index 0000000000..4b61264e11 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts new file mode 100644 index 0000000000..ffc80b43aa --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server peak-large-page (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/project.json b/benchmarks/memory/server/scenarios/peak-large-page/vue/project.json new file mode 100644 index 0000000000..f424a2134f --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-peak-large-page-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..26cc9f6058 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts @@ -0,0 +1,305 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as L1RouteImport } from './routes/l1' +import { Route as IndexRouteImport } from './routes/index' +import { Route as L1L2RouteImport } from './routes/l1.l2' +import { Route as L1L2L3RouteImport } from './routes/l1.l2.l3' +import { Route as L1L2L3L4RouteImport } from './routes/l1.l2.l3.l4' +import { Route as L1L2L3L4L5RouteImport } from './routes/l1.l2.l3.l4.l5' +import { Route as L1L2L3L4L5L6RouteImport } from './routes/l1.l2.l3.l4.l5.l6' +import { Route as L1L2L3L4L5L6L7RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7' +import { Route as L1L2L3L4L5L6L7L8RouteImport } from './routes/l1.l2.l3.l4.l5.l6.l7.l8' + +const L1Route = L1RouteImport.update({ + id: '/l1', + path: '/l1', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const L1L2Route = L1L2RouteImport.update({ + id: '/l2', + path: '/l2', + getParentRoute: () => L1Route, +} as any) +const L1L2L3Route = L1L2L3RouteImport.update({ + id: '/l3', + path: '/l3', + getParentRoute: () => L1L2Route, +} as any) +const L1L2L3L4Route = L1L2L3L4RouteImport.update({ + id: '/l4', + path: '/l4', + getParentRoute: () => L1L2L3Route, +} as any) +const L1L2L3L4L5Route = L1L2L3L4L5RouteImport.update({ + id: '/l5', + path: '/l5', + getParentRoute: () => L1L2L3L4Route, +} as any) +const L1L2L3L4L5L6Route = L1L2L3L4L5L6RouteImport.update({ + id: '/l6', + path: '/l6', + getParentRoute: () => L1L2L3L4L5Route, +} as any) +const L1L2L3L4L5L6L7Route = L1L2L3L4L5L6L7RouteImport.update({ + id: '/l7', + path: '/l7', + getParentRoute: () => L1L2L3L4L5L6Route, +} as any) +const L1L2L3L4L5L6L7L8Route = L1L2L3L4L5L6L7L8RouteImport.update({ + id: '/l8', + path: '/l8', + getParentRoute: () => L1L2L3L4L5L6L7Route, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/l1': typeof L1RouteWithChildren + '/l1/l2': typeof L1L2RouteWithChildren + '/l1/l2/l3': typeof L1L2L3RouteWithChildren + '/l1/l2/l3/l4': typeof L1L2L3L4RouteWithChildren + '/l1/l2/l3/l4/l5': typeof L1L2L3L4L5RouteWithChildren + '/l1/l2/l3/l4/l5/l6': typeof L1L2L3L4L5L6RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7': typeof L1L2L3L4L5L6L7RouteWithChildren + '/l1/l2/l3/l4/l5/l6/l7/l8': typeof L1L2L3L4L5L6L7L8Route +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + id: + | '__root__' + | '/' + | '/l1' + | '/l1/l2' + | '/l1/l2/l3' + | '/l1/l2/l3/l4' + | '/l1/l2/l3/l4/l5' + | '/l1/l2/l3/l4/l5/l6' + | '/l1/l2/l3/l4/l5/l6/l7' + | '/l1/l2/l3/l4/l5/l6/l7/l8' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + L1Route: typeof L1RouteWithChildren +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/l1': { + id: '/l1' + path: '/l1' + fullPath: '/l1' + preLoaderRoute: typeof L1RouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/l1/l2': { + id: '/l1/l2' + path: '/l2' + fullPath: '/l1/l2' + preLoaderRoute: typeof L1L2RouteImport + parentRoute: typeof L1Route + } + '/l1/l2/l3': { + id: '/l1/l2/l3' + path: '/l3' + fullPath: '/l1/l2/l3' + preLoaderRoute: typeof L1L2L3RouteImport + parentRoute: typeof L1L2Route + } + '/l1/l2/l3/l4': { + id: '/l1/l2/l3/l4' + path: '/l4' + fullPath: '/l1/l2/l3/l4' + preLoaderRoute: typeof L1L2L3L4RouteImport + parentRoute: typeof L1L2L3Route + } + '/l1/l2/l3/l4/l5': { + id: '/l1/l2/l3/l4/l5' + path: '/l5' + fullPath: '/l1/l2/l3/l4/l5' + preLoaderRoute: typeof L1L2L3L4L5RouteImport + parentRoute: typeof L1L2L3L4Route + } + '/l1/l2/l3/l4/l5/l6': { + id: '/l1/l2/l3/l4/l5/l6' + path: '/l6' + fullPath: '/l1/l2/l3/l4/l5/l6' + preLoaderRoute: typeof L1L2L3L4L5L6RouteImport + parentRoute: typeof L1L2L3L4L5Route + } + '/l1/l2/l3/l4/l5/l6/l7': { + id: '/l1/l2/l3/l4/l5/l6/l7' + path: '/l7' + fullPath: '/l1/l2/l3/l4/l5/l6/l7' + preLoaderRoute: typeof L1L2L3L4L5L6L7RouteImport + parentRoute: typeof L1L2L3L4L5L6Route + } + '/l1/l2/l3/l4/l5/l6/l7/l8': { + id: '/l1/l2/l3/l4/l5/l6/l7/l8' + path: '/l8' + fullPath: '/l1/l2/l3/l4/l5/l6/l7/l8' + preLoaderRoute: typeof L1L2L3L4L5L6L7L8RouteImport + parentRoute: typeof L1L2L3L4L5L6L7Route + } + } +} + +interface L1L2L3L4L5L6L7RouteChildren { + L1L2L3L4L5L6L7L8Route: typeof L1L2L3L4L5L6L7L8Route +} + +const L1L2L3L4L5L6L7RouteChildren: L1L2L3L4L5L6L7RouteChildren = { + L1L2L3L4L5L6L7L8Route: L1L2L3L4L5L6L7L8Route, +} + +const L1L2L3L4L5L6L7RouteWithChildren = L1L2L3L4L5L6L7Route._addFileChildren( + L1L2L3L4L5L6L7RouteChildren, +) + +interface L1L2L3L4L5L6RouteChildren { + L1L2L3L4L5L6L7Route: typeof L1L2L3L4L5L6L7RouteWithChildren +} + +const L1L2L3L4L5L6RouteChildren: L1L2L3L4L5L6RouteChildren = { + L1L2L3L4L5L6L7Route: L1L2L3L4L5L6L7RouteWithChildren, +} + +const L1L2L3L4L5L6RouteWithChildren = L1L2L3L4L5L6Route._addFileChildren( + L1L2L3L4L5L6RouteChildren, +) + +interface L1L2L3L4L5RouteChildren { + L1L2L3L4L5L6Route: typeof L1L2L3L4L5L6RouteWithChildren +} + +const L1L2L3L4L5RouteChildren: L1L2L3L4L5RouteChildren = { + L1L2L3L4L5L6Route: L1L2L3L4L5L6RouteWithChildren, +} + +const L1L2L3L4L5RouteWithChildren = L1L2L3L4L5Route._addFileChildren( + L1L2L3L4L5RouteChildren, +) + +interface L1L2L3L4RouteChildren { + L1L2L3L4L5Route: typeof L1L2L3L4L5RouteWithChildren +} + +const L1L2L3L4RouteChildren: L1L2L3L4RouteChildren = { + L1L2L3L4L5Route: L1L2L3L4L5RouteWithChildren, +} + +const L1L2L3L4RouteWithChildren = L1L2L3L4Route._addFileChildren( + L1L2L3L4RouteChildren, +) + +interface L1L2L3RouteChildren { + L1L2L3L4Route: typeof L1L2L3L4RouteWithChildren +} + +const L1L2L3RouteChildren: L1L2L3RouteChildren = { + L1L2L3L4Route: L1L2L3L4RouteWithChildren, +} + +const L1L2L3RouteWithChildren = + L1L2L3Route._addFileChildren(L1L2L3RouteChildren) + +interface L1L2RouteChildren { + L1L2L3Route: typeof L1L2L3RouteWithChildren +} + +const L1L2RouteChildren: L1L2RouteChildren = { + L1L2L3Route: L1L2L3RouteWithChildren, +} + +const L1L2RouteWithChildren = L1L2Route._addFileChildren(L1L2RouteChildren) + +interface L1RouteChildren { + L1L2Route: typeof L1L2RouteWithChildren +} + +const L1RouteChildren: L1RouteChildren = { + L1L2Route: L1L2RouteWithChildren, +} + +const L1RouteWithChildren = L1Route._addFileChildren(L1RouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + L1Route: L1RouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx new file mode 100644 index 0000000000..e9a57d46fd --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
peak-large-page-index
+} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx new file mode 100644 index 0000000000..bd079aa1a0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx @@ -0,0 +1,28 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ + loader: () => makeLargePageLevelData(8, 0x5eed_1008), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelEightComponent, +}) + +function LevelEightComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx new file mode 100644 index 0000000000..7f69f6f438 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ + loader: () => makeLargePageLevelData(7, 0x5eed_1007), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSevenComponent, +}) + +function LevelSevenComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx new file mode 100644 index 0000000000..75a7c717e3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ + loader: () => makeLargePageLevelData(6, 0x5eed_1006), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelSixComponent, +}) + +function LevelSixComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx new file mode 100644 index 0000000000..f390ae3db3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ + loader: () => makeLargePageLevelData(5, 0x5eed_1005), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFiveComponent, +}) + +function LevelFiveComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx new file mode 100644 index 0000000000..6b30bd50dc --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3/l4')({ + loader: () => makeLargePageLevelData(4, 0x5eed_1004), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelFourComponent, +}) + +function LevelFourComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx new file mode 100644 index 0000000000..6ec6f32fea --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2/l3')({ + loader: () => makeLargePageLevelData(3, 0x5eed_1003), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelThreeComponent, +}) + +function LevelThreeComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx new file mode 100644 index 0000000000..e5b4cf0de1 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1/l2')({ + loader: () => makeLargePageLevelData(2, 0x5eed_1002), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelTwoComponent, +}) + +function LevelTwoComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx new file mode 100644 index 0000000000..a29e465cd4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx @@ -0,0 +1,29 @@ +import { Outlet, createFileRoute } from '@tanstack/vue-router' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' + +export const Route = createFileRoute('/l1')({ + loader: () => makeLargePageLevelData(1, 0x5eed_1001), + head: ({ loaderData }) => makeLargePageHead(loaderData), + component: LevelOneComponent, +}) + +function LevelOneComponent() { + const data = Route.useLoaderData() + const first = data.value.records[0]! + + return ( +
+

{data.value.marker}

+

records: {data.value.records.length}

+
+

{first.name}

+

{first.id}

+

{first.description}

+
+ +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts new file mode 100644 index 0000000000..67bdf9b79d --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server peak-large-page (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/project.json b/benchmarks/memory/server/scenarios/request-churn/react/project.json new file mode 100644 index 0000000000..d997054b95 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-request-churn-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..d22f1bee18 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx new file mode 100644 index 0000000000..e6335b0d83 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
request-churn-index
+} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx new file mode 100644 index 0000000000..5fd86ee5fb --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx @@ -0,0 +1,40 @@ +import { createFileRoute } from '@tanstack/react-router' + +const itemIndexes = Array.from({ length: 5 }, (_, index) => index) + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }) => ({ q: search.q }), + loader: ({ params, deps }) => ({ + id: params.id, + title: `Item ${params.id}`, + q: deps.q, + items: itemIndexes.map((index) => ({ + id: `${params.id}-${index}`, + label: `${deps.q}-${index}`, + })), + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.title}

+

{data.q}

+
    + {data.items.map((item) => ( +
  • {item.label}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts b/benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts new file mode 100644 index 0000000000..4b75255eb1 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server request-churn (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/shared.ts b/benchmarks/memory/server/scenarios/request-churn/shared.ts new file mode 100644 index 0000000000..1739e4cc59 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/shared.ts @@ -0,0 +1,82 @@ +import { + createDeterministicRandom, + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +const benchmarkSeed = 0xdecafbad +const requestChurnIterations = 200 +const itemPageMarker = 'data-bench="request-churn-item"' +// Module-level so CodSpeed warmups and measurement never replay URLs. +const benchmarkRandom = createDeterministicRandom(benchmarkSeed) +let requestCounter = 0 + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function validateItemResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } +} + +function validateItemBody(body: string) { + if (!body.includes(itemPageMarker)) { + throw new Error('Expected request-churn item marker in response body') + } +} + +async function assertRequestChurnSanity(handler: StartRequestHandler) { + const response = await handler.fetch( + new Request('http://localhost/items/sanity-item?q=q-sanity', requestInit), + ) + const body = await response.text() + + if (response.status !== 200) { + throw new Error(`Expected sanity status 200, got ${response.status}`) + } + + validateItemBody(body) +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + function buildItemRequest(random: () => number) { + const counter = (requestCounter++).toString(36) + const id = `${counter}-${randomSegment(random)}` + const q = `q-${randomSegment(random)}` + + return new Request(`http://localhost/items/${id}?q=${q}`, requestInit) + } + + const run = () => + runSequentialRequestLoop(handler, { + random: benchmarkRandom, + iterations: requestChurnIterations, + buildRequest: buildItemRequest, + validateResponse: validateItemResponse, + }) + + return { + sanity: () => assertRequestChurnSanity(handler), + workloads: [ + { + name: `mem request-churn (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/project.json b/benchmarks/memory/server/scenarios/request-churn/solid/project.json new file mode 100644 index 0000000000..3f3434ee24 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-request-churn-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts b/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..8f49ec5d1f --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx b/benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..aaf7dfdd89 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/index.tsx new file mode 100644 index 0000000000..d32d31f301 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
request-churn-index
+} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/items.$id.tsx b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/items.$id.tsx new file mode 100644 index 0000000000..e687e22741 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/src/routes/items.$id.tsx @@ -0,0 +1,40 @@ +import { createFileRoute } from '@tanstack/solid-router' + +const itemIndexes = Array.from({ length: 5 }, (_, index) => index) + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }) => ({ q: search.q }), + loader: ({ params, deps }) => ({ + id: params.id, + title: `Item ${params.id}`, + q: deps.q, + items: itemIndexes.map((index) => ({ + id: `${params.id}-${index}`, + label: `${deps.q}-${index}`, + })), + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data().title}

+

{data().q}

+
    + {data().items.map((item) => ( +
  • {item.label}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/tsconfig.json b/benchmarks/memory/server/scenarios/request-churn/solid/tsconfig.json new file mode 100644 index 0000000000..4b61264e11 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/vite.config.ts b/benchmarks/memory/server/scenarios/request-churn/solid/vite.config.ts new file mode 100644 index 0000000000..8ff879e9e0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server request-churn (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/request-churn/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/project.json b/benchmarks/memory/server/scenarios/request-churn/vue/project.json new file mode 100644 index 0000000000..11eb1d868b --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-request-churn-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts b/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/request-churn/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..86ccc14a0c --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ItemsIdRouteImport } from './routes/items.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ItemsIdRoute = ItemsIdRouteImport.update({ + id: '/items/$id', + path: '/items/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/items/$id': typeof ItemsIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/items/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/items/$id' + id: '__root__' | '/' | '/items/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ItemsIdRoute: typeof ItemsIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/items/$id': { + id: '/items/$id' + path: '/items/$id' + fullPath: '/items/$id' + preLoaderRoute: typeof ItemsIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ItemsIdRoute: ItemsIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/src/router.tsx b/benchmarks/memory/server/scenarios/request-churn/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/index.tsx new file mode 100644 index 0000000000..30751100a4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
request-churn-index
+} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/items.$id.tsx b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/items.$id.tsx new file mode 100644 index 0000000000..a98d036565 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/src/routes/items.$id.tsx @@ -0,0 +1,40 @@ +import { createFileRoute } from '@tanstack/vue-router' + +const itemIndexes = Array.from({ length: 5 }, (_, index) => index) + +type ItemSearch = { + q: string +} + +export const Route = createFileRoute('/items/$id')({ + validateSearch: (search: Record): ItemSearch => ({ + q: typeof search.q === 'string' ? search.q : '', + }), + loaderDeps: ({ search }) => ({ q: search.q }), + loader: ({ params, deps }) => ({ + id: params.id, + title: `Item ${params.id}`, + q: deps.q, + items: itemIndexes.map((index) => ({ + id: `${params.id}-${index}`, + label: `${deps.q}-${index}`, + })), + }), + component: ItemComponent, +}) + +function ItemComponent() { + const data = Route.useLoaderData() + + return ( +
+

{data.value.title}

+

{data.value.q}

+
    + {data.value.items.map((item) => ( +
  • {item.label}
  • + ))} +
+
+ ) +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/tsconfig.json b/benchmarks/memory/server/scenarios/request-churn/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/vite.config.ts b/benchmarks/memory/server/scenarios/request-churn/vue/vite.config.ts new file mode 100644 index 0000000000..bcfa82b0ca --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server request-churn (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/project.json b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json new file mode 100644 index 0000000000..467b40d734 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-serialization-payload-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..7e370fd84c --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DataIdRouteImport } from './routes/data.$id' + +const DataIdRoute = DataIdRouteImport.update({ + id: '/data/$id', + path: '/data/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesByTo { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/data/$id': typeof DataIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/data/$id' + fileRoutesByTo: FileRoutesByTo + to: '/data/$id' + id: '__root__' | '/data/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + DataIdRoute: typeof DataIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/data/$id': { + id: '/data/$id' + path: '/data/$id' + fullPath: '/data/$id' + preLoaderRoute: typeof DataIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + DataIdRoute: DataIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx b/benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx new file mode 100644 index 0000000000..cfba0dbd62 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/react-router' +import { makeSerializationPayload } from '../../../serialization-payload' + +export const Route = createFileRoute('/data/$id')({ + loader: ({ params }) => makeSerializationPayload(params.id), + component: DataComponent, +}) + +function DataComponent() { + const data = Route.useLoaderData() + + return ( +
Map size: {data.lookup.size}
+ ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts new file mode 100644 index 0000000000..cfcc305c1a --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server serialization-payload (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/serialization-payload.ts b/benchmarks/memory/server/scenarios/serialization-payload/serialization-payload.ts new file mode 100644 index 0000000000..b6f401640e --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/serialization-payload.ts @@ -0,0 +1,115 @@ +const mapEntryCount = 500 +const setEntryCount = 500 +const temporalEntryCount = 500 +const nestedTreeDepth = 5 +const nestedTreeBreadth = 6 +const payloadTextLength = 150 + +export interface MapPayloadValue { + index: number + label: string + createdAt: Date + count: bigint + text: string +} + +export interface NestedPayloadNode { + id: string + depth: number + text: string + values: Array + children: Array +} + +export interface SerializationPayload { + id: string + lookup: Map + tags: Set + dates: Array + bigints: Array + tree: NestedPayloadNode +} + +export function makeSerializationPayload(id: string): SerializationPayload { + const hash = hashId(id) + const baseTimestamp = Date.UTC(2024, 0, 1) + hash + + return { + id, + lookup: new Map( + Array.from( + { length: mapEntryCount }, + (_, index): [string, MapPayloadValue] => [ + makeMapKey(id, index), + { + index, + label: `${id}-map-${index}`, + createdAt: new Date(baseTimestamp + index * 1_000), + count: BigInt(hash) * 10_000n + BigInt(index), + text: makePayloadText(id, `map-${index}`), + }, + ], + ), + ), + tags: new Set( + Array.from({ length: setEntryCount }, (_, index) => + makePayloadText(id, `set-${index}`), + ), + ), + dates: Array.from( + { length: temporalEntryCount }, + (_, index) => new Date(baseTimestamp + index * 60_000), + ), + bigints: Array.from( + { length: temporalEntryCount }, + (_, index) => BigInt(hash) * 1_000_000n + BigInt(index), + ), + tree: makeNestedTree(id, 0, 'root', hash), + } +} + +function makeMapKey(id: string, index: number) { + return `map-${id}-${index.toString().padStart(3, '0')}` +} + +function makeNestedTree( + id: string, + depth: number, + path: string, + hash: number, +): NestedPayloadNode { + return { + id: `${id}-node-${path}`, + depth, + text: makePayloadText(id, `tree-${depth}-${path}`), + values: [hash, depth, path.length], + children: + depth === nestedTreeDepth + ? [] + : Array.from({ length: nestedTreeBreadth }, (_, index) => + makeNestedTree(id, depth + 1, `${path}-${index}`, hash), + ), + } +} + +function makePayloadText(id: string, segment: string) { + const filler = 'abcdefghijklmnopqrstuvwxyz0123456789' + let value = `${id}|${segment}|` + + while (value.length < payloadTextLength) { + value += filler + } + + return value.slice(0, payloadTextLength) +} + +function hashId(id: string) { + let hash = 2_166_136_261 + + for (let index = 0; index < id.length; index++) { + hash ^= id.charCodeAt(index) + hash = Math.imul(hash, 16_777_619) + } + + return hash >>> 0 +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/shared.ts b/benchmarks/memory/server/scenarios/serialization-payload/shared.ts new file mode 100644 index 0000000000..c02990bbf9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/shared.ts @@ -0,0 +1,75 @@ +import { + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +const benchmarkSeed = 0x51eaa11 +const serializationPayloadIterations = 20 +const payloadPageMarker = 'data-bench="serialization-payload"' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildPayloadRequest(random: () => number, index: number) { + const id = `payload-${index}-${randomSegment(random)}` + + return new Request(`http://localhost/data/${id}`, requestInit) +} + +function validatePayloadResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } +} + +function validatePayloadBody(body: string) { + if (!body.includes(payloadPageMarker)) { + throw new Error('Expected serialization-payload marker in response body') + } +} + +async function assertSerializationPayloadSanity(handler: StartRequestHandler) { + const request = new Request( + 'http://localhost/data/sanity-payload', + requestInit, + ) + const response = await handler.fetch(request) + const body = await response.text() + + validatePayloadResponse(response, request) + validatePayloadBody(body) +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: serializationPayloadIterations, + buildRequest: buildPayloadRequest, + validateResponse: validatePayloadResponse, + }) + + return { + sanity: () => assertSerializationPayloadSanity(handler), + workloads: [ + { + name: `mem serialization-payload (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/project.json b/benchmarks/memory/server/scenarios/serialization-payload/solid/project.json new file mode 100644 index 0000000000..6f381ef389 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-serialization-payload-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..082cbb290c --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DataIdRouteImport } from './routes/data.$id' + +const DataIdRoute = DataIdRouteImport.update({ + id: '/data/$id', + path: '/data/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesByTo { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/data/$id': typeof DataIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/data/$id' + fileRoutesByTo: FileRoutesByTo + to: '/data/$id' + id: '__root__' | '/data/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + DataIdRoute: typeof DataIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/data/$id': { + id: '/data/$id' + path: '/data/$id' + fullPath: '/data/$id' + preLoaderRoute: typeof DataIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + DataIdRoute: DataIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/src/router.tsx b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..aaf7dfdd89 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/data.$id.tsx b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/data.$id.tsx new file mode 100644 index 0000000000..8cdbdb9046 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/data.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { makeSerializationPayload } from '../../../serialization-payload' + +export const Route = createFileRoute('/data/$id')({ + loader: ({ params }) => makeSerializationPayload(params.id), + component: DataComponent, +}) + +function DataComponent() { + const data = Route.useLoaderData() + + return ( +
+ Map size: {data().lookup.size} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/tsconfig.json b/benchmarks/memory/server/scenarios/serialization-payload/solid/tsconfig.json new file mode 100644 index 0000000000..4b61264e11 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/vite.config.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/vite.config.ts new file mode 100644 index 0000000000..26f57c9baa --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server serialization-payload (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/project.json b/benchmarks/memory/server/scenarios/serialization-payload/vue/project.json new file mode 100644 index 0000000000..425b50a0d5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-serialization-payload-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..8adce01d75 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routeTree.gen.ts @@ -0,0 +1,68 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as DataIdRouteImport } from './routes/data.$id' + +const DataIdRoute = DataIdRouteImport.update({ + id: '/data/$id', + path: '/data/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesByTo { + '/data/$id': typeof DataIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/data/$id': typeof DataIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/data/$id' + fileRoutesByTo: FileRoutesByTo + to: '/data/$id' + id: '__root__' | '/data/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + DataIdRoute: typeof DataIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/data/$id': { + id: '/data/$id' + path: '/data/$id' + fullPath: '/data/$id' + preLoaderRoute: typeof DataIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + DataIdRoute: DataIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/src/router.tsx b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/data.$id.tsx b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/data.$id.tsx new file mode 100644 index 0000000000..e6b4cfadfb --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/data.$id.tsx @@ -0,0 +1,17 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { makeSerializationPayload } from '../../../serialization-payload' + +export const Route = createFileRoute('/data/$id')({ + loader: ({ params }) => makeSerializationPayload(params.id), + component: DataComponent, +}) + +function DataComponent() { + const data = Route.useLoaderData() + + return ( +
+ Map size: {data.value.lookup.size} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/tsconfig.json b/benchmarks/memory/server/scenarios/serialization-payload/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/vite.config.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/vite.config.ts new file mode 100644 index 0000000000..b67693e559 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server serialization-payload (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json new file mode 100644 index 0000000000..841bbedca0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-server-fn-churn-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts new file mode 100644 index 0000000000..22770278cd --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts @@ -0,0 +1,24 @@ +import { createMiddleware, createServerFn } from '@tanstack/react-start' +import { + makeServerFnChurnPayload, + validateServerFnInput, +} from '../../server-fn-payload' + +const contextMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => + next({ + context: { + ctx: 'ctx-server-fn-churn', + }, + }), +) + +export const churnGet = createServerFn({ method: 'GET' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) + +export const churnPost = createServerFn({ method: 'POST' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..10e933da3d --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' + id: '__root__' | '/' | '/api/fn-urls' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts new file mode 100644 index 0000000000..da74a20f62 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/react-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + get: churnGet.url, + post: churnPost.url, + }), + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx new file mode 100644 index 0000000000..de0acfc2b3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/react-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ memory-server-fn-churn-index +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts new file mode 100644 index 0000000000..8a3eea0d36 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server server-fn-churn (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/server-fn-payload.ts b/benchmarks/memory/server/scenarios/server-fn-churn/server-fn-payload.ts new file mode 100644 index 0000000000..0f23390a6c --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/server-fn-payload.ts @@ -0,0 +1,33 @@ +export type ServerFnInput = { + id: string +} + +export type ServerFnChurnContext = { + ctx: string +} + +const recordIndexes = Array.from({ length: 5 }, (_, index) => index) + +export function validateServerFnInput(input: unknown): ServerFnInput { + const payload = input as Partial | null + + if (typeof payload?.id !== 'string') { + throw new Error('invalid server-fn churn input') + } + + return { id: payload.id } +} + +export function makeServerFnChurnPayload( + data: ServerFnInput, + context: ServerFnChurnContext, +) { + return { + id: data.id, + ctx: context.ctx, + payload: recordIndexes.map((index) => ({ + id: `${data.id}-${index}`, + label: `record-${index}`, + })), + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts b/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts new file mode 100644 index 0000000000..ad1022c836 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts @@ -0,0 +1,232 @@ +import { + createDeterministicRandom, + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +type FnUrls = { + get: string + post: string +} + +type PayloadFixture = { + id: string + body: string + query: string +} + +type SerovalNode = + | { + t: 1 + s: string + } + | { + t: 10 + i: number + p: { + k: Array + v: Array + } + o: number + } + +const benchmarkSeed = 0xdecafbad +const payloadSeed = 0x51f0cafe +const fixtureCount = 16 +const serverFnChurnIterations = 150 +const origin = 'http://localhost' +const tssContentTypeFramed = 'application/x-tss-framed' +const acceptHeader = `${tssContentTypeFramed}, application/x-ndjson, application/json` +const xTssSerialized = 'x-tss-serialized' +const contextMarker = 'ctx-server-fn-churn' + +const commonHeaders = { + 'x-tsr-serverFn': 'true', + 'sec-fetch-site': 'same-origin', + accept: acceptHeader, +} satisfies HeadersInit + +const postHeaders = { + ...commonHeaders, + 'content-type': 'application/json', +} satisfies HeadersInit + +// Hand-rolled copy of Start's seroval RPC wire format so POST bodies can be +// precomputed at module level. Coupled to the internal protocol on purpose; +// the module-load sanity check below throws loudly if the protocol drifts. +function stringNode(value: string): SerovalNode { + return { t: 1, s: value } +} + +function objectNode( + id: number, + entries: Array, +): SerovalNode { + return { + t: 10, + i: id, + p: { + k: entries.map(([key]) => key), + v: entries.map(([, value]) => value), + }, + o: 0, + } +} + +function serializePayload(id: string) { + return JSON.stringify({ + t: objectNode(0, [['data', objectNode(1, [['id', stringNode(id)]])]]), + f: 63, + m: [], + }) +} + +function createFixtures(kind: 'get' | 'post') { + const random = createDeterministicRandom(payloadSeed ^ kind.length) + + return Array.from({ length: fixtureCount }, (_, index): PayloadFixture => { + const id = [kind, index, randomSegment(random), randomSegment(random)].join( + '-', + ) + const body = serializePayload(id) + + return { + id, + body, + query: `?${new URLSearchParams({ payload: body })}`, + } + }) +} + +const getFixtures = createFixtures('get') +const postFixtures = createFixtures('post') + +async function discoverUrls(handler: StartRequestHandler) { + const response = await handler.fetch(new Request(`${origin}/api/fn-urls`)) + const text = await response.text() + + if (response.status !== 200) { + throw new Error( + `URL discovery failed with status ${response.status}: ${text}`, + ) + } + + let urls: Partial + + try { + urls = JSON.parse(text) as Partial + } catch (error) { + throw new Error(`URL discovery returned invalid JSON: ${text}`, { + cause: error, + }) + } + + if (typeof urls.get !== 'string' || typeof urls.post !== 'string') { + throw new Error(`URL discovery returned invalid payload: ${text}`) + } + + return urls as FnUrls +} + +function buildGetRequest(url: string, fixture: PayloadFixture) { + return new Request(`${origin}${url}${fixture.query}`, { + method: 'GET', + headers: commonHeaders, + }) +} + +function buildPostRequest(url: string, fixture: PayloadFixture) { + return new Request(`${origin}${url}`, { + method: 'POST', + headers: postHeaders, + body: fixture.body, + }) +} + +function validateServerFnResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } + + if (!response.headers.get(xTssSerialized)) { + throw new Error(`Expected ${xTssSerialized} header for ${request.url}`) + } +} + +function validateEchoedBody( + body: string, + request: Request, + expectedId: string, +) { + if (!body.includes(expectedId)) { + throw new Error(`Expected echoed id ${expectedId} in ${request.url}`) + } + + if (!body.includes(contextMarker)) { + throw new Error( + `Expected context marker ${contextMarker} in ${request.url}`, + ) + } +} + +async function assertServerFnChurnSanity( + handler: StartRequestHandler, + urls: FnUrls, +) { + const getFixture = getFixtures[0]! + const getRequest = buildGetRequest(urls.get, getFixture) + const getResponse = await handler.fetch(getRequest) + const getBody = await getResponse.text() + + validateServerFnResponse(getResponse, getRequest) + validateEchoedBody(getBody, getRequest, getFixture.id) + + const postFixture = postFixtures[0]! + const postRequest = buildPostRequest(urls.post, postFixture) + const postResponse = await handler.fetch(postRequest) + const postBody = await postResponse.text() + + validateServerFnResponse(postResponse, postRequest) + validateEchoedBody(postBody, postRequest, postFixture.id) +} + +export async function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const urls = await discoverUrls(handler) + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: serverFnChurnIterations, + buildRequest: (_random, index) => { + const fixtureIndex = Math.floor(index / 2) % fixtureCount + + if (index % 2 === 0) { + const fixture = getFixtures[fixtureIndex]! + return buildGetRequest(urls.get, fixture) + } else { + const fixture = postFixtures[fixtureIndex]! + return buildPostRequest(urls.post, fixture) + } + }, + validateResponse: validateServerFnResponse, + }) + + return { + sanity: () => assertServerFnChurnSanity(handler, urls), + workloads: [ + { + name: `mem server-fn-churn (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/project.json b/benchmarks/memory/server/scenarios/server-fn-churn/solid/project.json new file mode 100644 index 0000000000..53aca0cea6 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-server-fn-churn-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts new file mode 100644 index 0000000000..31669dff50 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts @@ -0,0 +1,24 @@ +import { createMiddleware, createServerFn } from '@tanstack/solid-start' +import { + makeServerFnChurnPayload, + validateServerFnInput, +} from '../../server-fn-payload' + +const contextMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => + next({ + context: { + ctx: 'ctx-server-fn-churn', + }, + }), +) + +export const churnGet = createServerFn({ method: 'GET' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) + +export const churnPost = createServerFn({ method: 'POST' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..298ac3168d --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' + id: '__root__' | '/' | '/api/fn-urls' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/router.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..aaf7dfdd89 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/api.fn-urls.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/api.fn-urls.ts new file mode 100644 index 0000000000..2063684034 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/api.fn-urls.ts @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + get: churnGet.url, + post: churnPost.url, + }), + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/index.tsx new file mode 100644 index 0000000000..99fea371f8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ memory-server-fn-churn-index +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/tsconfig.json b/benchmarks/memory/server/scenarios/server-fn-churn/solid/tsconfig.json new file mode 100644 index 0000000000..4b61264e11 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/vite.config.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/vite.config.ts new file mode 100644 index 0000000000..8ffdc48227 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server server-fn-churn (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/project.json b/benchmarks/memory/server/scenarios/server-fn-churn/vue/project.json new file mode 100644 index 0000000000..c5809a259b --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-server-fn-churn-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts new file mode 100644 index 0000000000..0fa679f76f --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts @@ -0,0 +1,24 @@ +import { createMiddleware, createServerFn } from '@tanstack/vue-start' +import { + makeServerFnChurnPayload, + validateServerFnInput, +} from '../../server-fn-payload' + +const contextMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => + next({ + context: { + ctx: 'ctx-server-fn-churn', + }, + }), +) + +export const churnGet = createServerFn({ method: 'GET' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) + +export const churnPost = createServerFn({ method: 'POST' }) + .middleware([contextMiddleware]) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..b226f0a25a --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiFnUrlsRouteImport } from './routes/api.fn-urls' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiFnUrlsRoute = ApiFnUrlsRouteImport.update({ + id: '/api/fn-urls', + path: '/api/fn-urls', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/api/fn-urls': typeof ApiFnUrlsRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/api/fn-urls' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/api/fn-urls' + id: '__root__' | '/' | '/api/fn-urls' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + ApiFnUrlsRoute: typeof ApiFnUrlsRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/fn-urls': { + id: '/api/fn-urls' + path: '/api/fn-urls' + fullPath: '/api/fn-urls' + preLoaderRoute: typeof ApiFnUrlsRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + ApiFnUrlsRoute: ApiFnUrlsRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/router.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/api.fn-urls.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/api.fn-urls.ts new file mode 100644 index 0000000000..ff99e89e4c --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/api.fn-urls.ts @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/api/fn-urls')({ + server: { + handlers: { + GET: () => + Response.json({ + get: churnGet.url, + post: churnPost.url, + }), + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/index.tsx new file mode 100644 index 0000000000..0a39b9301c --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { churnGet, churnPost } from '../fns' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return ( +
+ memory-server-fn-churn-index +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/tsconfig.json b/benchmarks/memory/server/scenarios/server-fn-churn/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/vite.config.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/vite.config.ts new file mode 100644 index 0000000000..02b404808e --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server server-fn-churn (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/deferred-section-data.ts b/benchmarks/memory/server/scenarios/streaming-peak/deferred-section-data.ts new file mode 100644 index 0000000000..b30352ef39 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/deferred-section-data.ts @@ -0,0 +1,37 @@ +const deferredRecordCount = 250 +const recordValueLength = 128 + +interface DeferredRecord { + id: string + value: string +} + +export interface DeferredSectionPayload { + index: number + records: Array +} + +export function makeDeferredSectionPayload( + id: string, + sectionIndex: number, +): DeferredSectionPayload { + return { + index: sectionIndex, + records: Array.from({ length: deferredRecordCount }, (_, recordIndex) => ({ + id: `${id}-${sectionIndex}-${recordIndex}`, + value: makeRecordValue(id, sectionIndex, recordIndex), + })), + } +} + +function makeRecordValue( + id: string, + sectionIndex: number, + recordIndex: number, +) { + const token = `${id}:${sectionIndex}:${recordIndex}:streaming-peak-record;` + + return token + .repeat(Math.ceil(recordValueLength / token.length)) + .slice(0, recordValueLength) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/project.json b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json new file mode 100644 index 0000000000..6fc1dcdda0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-streaming-peak-react", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/react-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts new file mode 100644 index 0000000000..0c98f54ddd --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('react', handler) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts new file mode 100644 index 0000000000..fbbc25ecfb --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx new file mode 100644 index 0000000000..7c4eb0babe --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx new file mode 100644 index 0000000000..c5f9de6922 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx @@ -0,0 +1,27 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx new file mode 100644 index 0000000000..669fdef2e1 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
streaming-peak-index
+} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..aa8ba5e865 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx @@ -0,0 +1,83 @@ +import { Await, createFileRoute } from '@tanstack/react-router' +import { Suspense } from 'react' +import { + makeDeferredSectionPayload, + type DeferredSectionPayload, +} from '../../../deferred-section-data' + +const fallbackFlushDelayMs = 1 + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + eager: `streaming-peak-eager-${params.id}`, + deferred0: makeDeferredSection(params.id, 0), + deferred1: makeDeferredSection(params.id, 1), + deferred2: makeDeferredSection(params.id, 2), + deferred3: makeDeferredSection(params.id, 3), + }), + component: StreamComponent, +}) + +// Deferred sections must settle strictly AFTER React's shell flush, which +// React schedules via setImmediate internally. Microtask chains drain during +// router load (sections resolve before the Suspense boundaries are even +// reached) and setImmediate chains registered at loader time win the race +// against React's flush — either way no fallback ever streams and the bench +// stops exercising multi-flush streaming. Timer-phase callbacks reliably lose +// that race, so this is the documented exception to the no-timers convention: +// the few ms of wall-clock are irrelevant to memory metrics, and distinct +// delays keep section ordering deterministic. +function afterFallbackFlush(sectionIndex: number) { + return new Promise((resolve) => { + setTimeout(resolve, fallbackFlushDelayMs + sectionIndex) + }) +} + +function makeDeferredSection(id: string, sectionIndex: number) { + return afterFallbackFlush(sectionIndex).then(() => + makeDeferredSectionPayload(id, sectionIndex), + ) +} + +function StreamComponent() { + const data = Route.useLoaderData() + const deferredSections = [ + { index: 0, promise: data.deferred0 }, + { index: 1, promise: data.deferred1 }, + { index: 2, promise: data.deferred2 }, + { index: 3, promise: data.deferred3 }, + ] as const + + return ( +
+

{data.eager}

+ {deferredSections.map(({ index, promise }) => ( + + streaming-peak-fallback-{index} +

+ } + > + + {(section) => } + +
+ ))} +
+ ) +} + +function DeferredSection({ section }: { section: DeferredSectionPayload }) { + const marker = `streaming-peak-deferred-${section.index}` + + return ( +
+

{marker}

+ {section.records.map((record) => ( +

{record.value}

+ ))} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json new file mode 100644 index 0000000000..11ddcce4ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts new file mode 100644 index 0000000000..733aabb76d --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import react from '@vitejs/plugin-react' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + react(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server streaming-peak (react)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/shared.ts b/benchmarks/memory/server/scenarios/streaming-peak/shared.ts new file mode 100644 index 0000000000..a82c43b5b4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/shared.ts @@ -0,0 +1,112 @@ +import { + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +export type { StartRequestHandler } + +type Framework = 'react' | 'solid' | 'vue' + +const benchmarkSeed = 0xdecafbad +const streamingPeakIterations = 20 +const fallbackMarker = 'streaming-peak-fallback-0' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildStreamingRequest(random: () => number, index: number) { + return new Request( + `http://localhost/stream/${index}-${randomSegment(random)}`, + requestInit, + ) +} + +function validateStreamingResponse(response: Response, request: Request) { + if (response.status !== 200) { + throw new Error( + `Expected status 200 for ${request.url}, got ${response.status}`, + ) + } +} + +function getResponseReader(response: Response) { + const reader = response.body?.getReader() + + if (!reader) { + throw new Error('Expected streaming response body') + } + + return reader +} + +async function readStreamingBody(response: Response) { + const reader = getResponseReader(response) + const decoder = new TextDecoder() + let body = '' + let chunkCount = 0 + + while (true) { + const result = await reader.read() + + if (result.done) { + break + } + + chunkCount++ + body += decoder.decode(result.value, { stream: true }) + } + + body += decoder.decode() + + return { body, chunkCount } +} + +async function assertStreamingPeakSanity(handler: StartRequestHandler) { + const chunkedRequest = new Request( + 'http://localhost/stream/sanity-chunked', + requestInit, + ) + const chunkedResponse = await handler.fetch(chunkedRequest) + + validateStreamingResponse(chunkedResponse, chunkedRequest) + + const chunked = await readStreamingBody(chunkedResponse) + + if (chunked.chunkCount <= 1) { + throw new Error( + `Expected chunked sanity response to produce multiple chunks, got ${chunked.chunkCount}`, + ) + } + + if (!chunked.body.includes(fallbackMarker)) { + throw new Error('Expected streaming-peak fallback marker in response body') + } +} + +export function createWorkloadGroup( + framework: Framework, + handler: StartRequestHandler, +) { + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: streamingPeakIterations, + buildRequest: buildStreamingRequest, + validateResponse: validateStreamingResponse, + }) + + return { + sanity: () => assertStreamingPeakSanity(handler), + workloads: [ + { + name: `mem streaming-peak chunked (${framework})`, + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.flame.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/project.json b/benchmarks/memory/server/scenarios/streaming-peak/solid/project.json new file mode 100644 index 0000000000..80f44d1063 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-streaming-peak-solid", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/solid-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts new file mode 100644 index 0000000000..4fe85c3f00 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('solid', handler) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routeTree.gen.ts new file mode 100644 index 0000000000..04ebed80f0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/solid-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/solid-start' +declare module '@tanstack/solid-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/src/router.tsx b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/router.tsx new file mode 100644 index 0000000000..038ec0ab5e --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/solid-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/__root.tsx new file mode 100644 index 0000000000..aaf7dfdd89 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' +import { HydrationScript } from 'solid-js/web' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charset: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/index.tsx b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/index.tsx new file mode 100644 index 0000000000..5815e36b3d --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
streaming-peak-index
+} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..0a3f64ffd4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/stream.$id.tsx @@ -0,0 +1,75 @@ +import { Await, createFileRoute } from '@tanstack/solid-router' +import { Suspense } from 'solid-js' +import { + makeDeferredSectionPayload, + type DeferredSectionPayload, +} from '../../../deferred-section-data' + +const fallbackFlushDelayMs = 25 + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + eager: `streaming-peak-eager-${params.id}`, + deferred0: makeDeferredSection(params.id, 0), + deferred1: makeDeferredSection(params.id, 1), + deferred2: makeDeferredSection(params.id, 2), + deferred3: makeDeferredSection(params.id, 3), + }), + component: StreamComponent, +}) + +// Deferred sections must settle after the framework shell flush so the bench +// continues exercising multi-flush streaming with visible fallback chunks. +function afterFallbackFlush(sectionIndex: number) { + return new Promise((resolve) => { + setTimeout(resolve, fallbackFlushDelayMs + sectionIndex) + }) +} + +function makeDeferredSection(id: string, sectionIndex: number) { + return afterFallbackFlush(sectionIndex).then(() => + makeDeferredSectionPayload(id, sectionIndex), + ) +} + +function StreamComponent() { + const data = Route.useLoaderData() + const deferredSections = () => + [ + { index: 0, promise: data().deferred0 }, + { index: 1, promise: data().deferred1 }, + { index: 2, promise: data().deferred2 }, + { index: 3, promise: data().deferred3 }, + ] as const + + return ( +
+

{data().eager}

+ {deferredSections().map(({ index, promise }) => ( + <> +

+ streaming-peak-fallback-{index} +

+ + + {(section) => } + + + + ))} +
+ ) +} + +function DeferredSection(props: { section: DeferredSectionPayload }) { + const marker = () => `streaming-peak-deferred-${props.section.index}` + + return ( +
+

{marker()}

+ {props.section.records.map((record) => ( +

{record.value}

+ ))} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/tsconfig.json b/benchmarks/memory/server/scenarios/streaming-peak/solid/tsconfig.json new file mode 100644 index 0000000000..4b61264e11 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/vite.config.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/vite.config.ts new file mode 100644 index 0000000000..b7d98e709c --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/vite.config.ts @@ -0,0 +1,34 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import solid from 'vite-plugin-solid' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + solid({ ssr: true, hot: false, dev: false }), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server streaming-peak (solid)', + watch: false, + environment: 'node', + server: { + deps: { + inline: [/@solidjs/, /@tanstack\/solid-store/], + }, + }, + }, +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts new file mode 100644 index 0000000000..9a5c211fee --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts @@ -0,0 +1,11 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' + +await workloadGroup.sanity() + +describe('memory', () => { + for (const workload of workloadGroup.workloads) { + bench(workload.name, workload.run, memoryBenchOptions) + } +}) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.flame.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.flame.ts new file mode 100644 index 0000000000..0182c472a8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.flame.ts @@ -0,0 +1,4 @@ +import { runServerFlameBenchmark } from '#memory-server/flame-runner' +import { workloadGroup } from './setup.ts' + +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/project.json b/benchmarks/memory/server/scenarios/streaming-peak/vue/project.json new file mode 100644 index 0000000000..55771aebe3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/project.json @@ -0,0 +1,54 @@ +{ + "name": "@benchmarks/memory-server-streaming-peak-vue", + "projectType": "application", + "targets": { + "build:ssr": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts" + } + }, + "build:ssr:flame": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "NODE_ENV=production vite build --config {projectRoot}/vite.config.ts --sourcemap true" + } + }, + "test:flame": { + "executor": "nx:run-commands", + "parallelism": false, + "cache": false, + "dependsOn": ["build:ssr:flame"], + "options": { + "command": "NODE_ENV=production node benchmarks/memory/run-flame.mjs {projectRoot}/memory.flame.ts {projectRoot}/dist", + "cwd": "." + } + }, + "test:types:ssr": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "projects": ["@tanstack/vue-start"], + "target": "build" + } + ], + "options": { + "command": "tsc -p {projectRoot}/tsconfig.json --noEmit" + } + } + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts new file mode 100644 index 0000000000..6683dafe6b --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts @@ -0,0 +1,14 @@ +import type { ServerMemoryWorkloadGroup } from '#memory-server/benchmark' +import { createWorkloadGroup } from '../shared.ts' +import type { StartRequestHandler } from '../shared.ts' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routeTree.gen.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routeTree.gen.ts new file mode 100644 index 0000000000..192f1d6dc7 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as StreamIdRouteImport } from './routes/stream.$id' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const StreamIdRoute = StreamIdRouteImport.update({ + id: '/stream/$id', + path: '/stream/$id', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/stream/$id': typeof StreamIdRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/stream/$id' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/stream/$id' + id: '__root__' | '/' | '/stream/$id' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + StreamIdRoute: typeof StreamIdRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/stream/$id': { + id: '/stream/$id' + path: '/stream/$id' + fullPath: '/stream/$id' + preLoaderRoute: typeof StreamIdRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + StreamIdRoute: StreamIdRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/src/router.tsx b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/router.tsx new file mode 100644 index 0000000000..4290e7cdd3 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/router.tsx @@ -0,0 +1,16 @@ +import { createRouter } from '@tanstack/vue-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + return createRouter({ + routeTree, + defaultPreload: false, + scrollRestoration: false, + }) +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/__root.tsx b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/__root.tsx new file mode 100644 index 0000000000..de29ee1612 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/__root.tsx @@ -0,0 +1,29 @@ +import { + Body, + HeadContent, + Html, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [{ charSet: 'utf-8' }], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/index.tsx b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/index.tsx new file mode 100644 index 0000000000..bc55b49adc --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/index.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: IndexComponent, +}) + +function IndexComponent() { + return
streaming-peak-index
+} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/stream.$id.tsx new file mode 100644 index 0000000000..963782403b --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/stream.$id.tsx @@ -0,0 +1,82 @@ +import { Await, createFileRoute } from '@tanstack/vue-router' +import { Suspense } from 'vue' +import { + makeDeferredSectionPayload, + type DeferredSectionPayload, +} from '../../../deferred-section-data' + +const fallbackFlushDelayMs = 1 + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + eager: `streaming-peak-eager-${params.id}`, + deferred0: makeDeferredSection(params.id, 0), + deferred1: makeDeferredSection(params.id, 1), + deferred2: makeDeferredSection(params.id, 2), + deferred3: makeDeferredSection(params.id, 3), + }), + component: StreamComponent, +}) + +// Deferred sections must settle strictly AFTER Vue's shell has a chance to +// flush, so fallbacks are emitted before deferred section content. +function afterFallbackFlush(sectionIndex: number) { + return new Promise((resolve) => { + setTimeout(resolve, fallbackFlushDelayMs + sectionIndex) + }) +} + +function makeDeferredSection(id: string, sectionIndex: number) { + return afterFallbackFlush(sectionIndex).then(() => + makeDeferredSectionPayload(id, sectionIndex), + ) +} + +function StreamComponent() { + const data = Route.useLoaderData() + const deferredSections = [ + { index: 0, promise: data.value.deferred0 }, + { index: 1, promise: data.value.deferred1 }, + { index: 2, promise: data.value.deferred2 }, + { index: 3, promise: data.value.deferred3 }, + ] as const + + return ( +
+

{data.value.eager}

+ {deferredSections.map(({ index, promise }) => ( + <> +

+ streaming-peak-fallback-{index} +

+ + {{ + default: () => ( + ( + + )} + /> + ), + fallback: () => null, + }} + + + ))} +
+ ) +} + +function DeferredSection({ section }: { section: DeferredSectionPayload }) { + const marker = `streaming-peak-deferred-${section.index}` + + return ( +
+

{marker}

+ {section.records.map((record) => ( +

{record.value}

+ ))} +
+ ) +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/tsconfig.json b/benchmarks/memory/server/scenarios/streaming-peak/vue/tsconfig.json new file mode 100644 index 0000000000..9ad6481342 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.ts", + "memory.flame.ts", + "setup.ts", + "vite.config.ts", + "../../../bench-utils.ts", + "./src/**/*" + ] +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/vite.config.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/vite.config.ts new file mode 100644 index 0000000000..9634031b31 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/vite.config.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' +import codspeedPlugin from '@codspeed/vitest-plugin' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const rootDir = fileURLToPath(new URL('.', import.meta.url)) + +export default defineConfig({ + root: rootDir, + plugins: [ + !!(process.env.VITEST && process.env.WITH_INSTRUMENTATION) && + codspeedPlugin(), + tanstackStart({ + srcDirectory: 'src', + }), + vueJsx(), + ], + build: { + outDir: './dist', + emptyOutDir: true, + minify: false, + }, + test: { + name: '@benchmarks/memory-server streaming-peak (vue)', + watch: false, + environment: 'node', + }, +}) diff --git a/benchmarks/memory/server/tsconfig.json b/benchmarks/memory/server/tsconfig.json new file mode 100644 index 0000000000..5d5dcbf825 --- /dev/null +++ b/benchmarks/memory/server/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": ["bench-utils.ts"] +} diff --git a/benchmarks/memory/server/vitest.react.config.ts b/benchmarks/memory/server/vitest.react.config.ts new file mode 100644 index 0000000000..44643b4d60 --- /dev/null +++ b/benchmarks/memory/server/vitest.react.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/react/vite.config.ts'], + }, +}) diff --git a/benchmarks/memory/server/vitest.solid.config.ts b/benchmarks/memory/server/vitest.solid.config.ts new file mode 100644 index 0000000000..5c8185cdd9 --- /dev/null +++ b/benchmarks/memory/server/vitest.solid.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/solid/vite.config.ts'], + }, +}) diff --git a/benchmarks/memory/server/vitest.vue.config.ts b/benchmarks/memory/server/vitest.vue.config.ts new file mode 100644 index 0000000000..01768185ee --- /dev/null +++ b/benchmarks/memory/server/vitest.vue.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + fileParallelism: false, + projects: ['./scenarios/*/vue/vite.config.ts'], + }, +}) diff --git a/package.json b/package.json index 75bc153169..f544549330 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,14 @@ "benchmark:bundle-size:analyze": "node scripts/benchmarks/bundle-size/analyze.mjs", "benchmark:client-nav": "nx run @benchmarks/client-nav:test:perf", "benchmark:ssr": "nx run @benchmarks/ssr:test:perf", + "benchmark:memory:client:flame": "nx run @benchmarks/memory-client:test:flame:react --parallel=1", + "benchmark:memory:client:flame:react": "nx run @benchmarks/memory-client:test:flame:react --parallel=1", + "benchmark:memory:client:flame:solid": "nx run @benchmarks/memory-client:test:flame:solid --parallel=1", + "benchmark:memory:client:flame:vue": "nx run @benchmarks/memory-client:test:flame:vue --parallel=1", + "benchmark:memory:server:flame": "nx run @benchmarks/memory-server:test:flame:react --parallel=1", + "benchmark:memory:server:flame:react": "nx run @benchmarks/memory-server:test:flame:react --parallel=1", + "benchmark:memory:server:flame:solid": "nx run @benchmarks/memory-server:test:flame:solid --parallel=1", + "benchmark:memory:server:flame:vue": "nx run @benchmarks/memory-server:test:flame:vue --parallel=1", "build": "nx affected --target=build --exclude=e2e/** --exclude=examples/**", "build:all": "nx run-many --target=build --exclude=examples/** --exclude=e2e/**", "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all", diff --git a/packages/vue-router/src/ssr/renderRouterToStream.tsx b/packages/vue-router/src/ssr/renderRouterToStream.tsx index 3b122fda6b..efad4fc2eb 100644 --- a/packages/vue-router/src/ssr/renderRouterToStream.tsx +++ b/packages/vue-router/src/ssr/renderRouterToStream.tsx @@ -9,6 +9,11 @@ import { import type { AnyRouter } from '@tanstack/router-core' import type { Component } from 'vue' +const isAbortError = (request: Request, error: unknown) => + (request.signal.aborted && error === request.signal.reason) || + (error instanceof Error && error.name === 'AbortError') || + (error as any)?.code === 'ABORT_ERR' + function prependDoctype( readable: globalThis.ReadableStream, ): NodeReadableStream { @@ -126,25 +131,57 @@ export const renderRouterToStream = async ({ } } const abortVuePipe = (reason?: unknown) => { - if (writerDone) return + if (writerDone) { + return + } + writerDone = true void innerWriter .abort(reason) .catch(() => {}) .finally(releaseWriter) } + const handleWriterError = (err: unknown) => { + if (isAbortError(request, err)) { + return + } + + throw err + } + const handleWriteError = (err: unknown) => { + if (writerDone || isAbortError(request, err)) { + return + } + + throw err + } const vueWritable = new WritableStream({ write(chunk) { - return innerWriter.write(chunk) + if (writerDone) { + return + } + + return innerWriter.write(chunk).catch(handleWriteError) }, close() { + if (writerDone) { + return + } + writerDone = true - return innerWriter.close().finally(releaseWriter) + return innerWriter.close().catch(handleWriterError).finally(releaseWriter) }, abort(reason) { + if (writerDone) { + return + } + writerDone = true - return innerWriter.abort(reason).finally(releaseWriter) + return innerWriter + .abort(reason) + .catch(handleWriterError) + .finally(releaseWriter) }, }) diff --git a/packages/vue-router/tests/renderRouterToStream.test.tsx b/packages/vue-router/tests/renderRouterToStream.test.tsx index 781096185f..527bfa0e03 100644 --- a/packages/vue-router/tests/renderRouterToStream.test.tsx +++ b/packages/vue-router/tests/renderRouterToStream.test.tsx @@ -117,7 +117,7 @@ describe('renderRouterToStream - sync setup failures', () => { } }) - test('request abort aborts Vue writer and terminates response stream', async () => { + test('request abort drops later Vue writes and terminates response stream', async () => { let vueWriter: WritableStreamDefaultWriter | undefined rendererMocks.pipeToWebWritable.mockImplementationOnce( ( @@ -149,7 +149,7 @@ describe('renderRouterToStream - sync setup failures', () => { await expect( vueWriter!.write(new TextEncoder().encode('
')), - ).rejects.toBeTruthy() + ).resolves.toBeUndefined() const terminated = await Promise.race([ drainBody(response), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efb2ac9b5f..4917d0514f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -311,7 +311,135 @@ importers: version: 2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@1.9.12)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + + benchmarks/memory/client: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/router-core': + specifier: workspace:* + version: link:../../../packages/router-core + '@tanstack/solid-router': + specifier: workspace:* + version: link:../../../packages/solid-router + '@tanstack/vue-router': + specifier: workspace:* + version: link:../../../packages/vue-router + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + solid-js: + specifier: 1.9.12 + version: 1.9.12 + vue: + specifier: ^3.5.16 + version: 3.5.25(typescript@6.0.2) + devDependencies: + '@codspeed/vitest-plugin': + specifier: ^5.5.0 + version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) + '@datadog/pprof': + specifier: ^5.13.2 + version: 5.13.2 + '@platformatic/flame': + specifier: ^1.6.0 + version: 1.6.0 + '@testing-library/react': + specifier: ^16.2.0 + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/jsdom': + specifier: 28.0.0 + version: 28.0.0 + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitejs/plugin-vue': + specifier: ^6.0.5 + version: 6.0.5(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vue@3.5.25(typescript@6.0.2)) + '@vitejs/plugin-vue-jsx': + specifier: ^5.1.5 + version: 5.1.5(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vue@3.5.25(typescript@6.0.2)) + jsdom: + specifier: 29.1.1 + version: 29.1.1(@noble/hashes@2.0.1) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + vite-plugin-solid: + specifier: ^2.11.11 + version: 2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@1.9.12)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + vitest: + specifier: ^4.1.4 + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + + benchmarks/memory/server: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/react-start': + specifier: workspace:* + version: link:../../../packages/react-start + '@tanstack/solid-router': + specifier: workspace:* + version: link:../../../packages/solid-router + '@tanstack/solid-start': + specifier: workspace:* + version: link:../../../packages/solid-start + '@tanstack/vue-router': + specifier: workspace:* + version: link:../../../packages/vue-router + '@tanstack/vue-start': + specifier: workspace:* + version: link:../../../packages/vue-start + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + solid-js: + specifier: 1.9.12 + version: 1.9.12 + vue: + specifier: ^3.5.16 + version: 3.5.25(typescript@6.0.2) + devDependencies: + '@codspeed/vitest-plugin': + specifier: ^5.5.0 + version: 5.5.0(tinybench@2.9.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vitest@4.1.4) + '@datadog/pprof': + specifier: ^5.13.2 + version: 5.13.2 + '@platformatic/flame': + specifier: ^1.6.0 + version: 1.6.0 + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitejs/plugin-vue-jsx': + specifier: ^5.1.5 + version: 5.1.5(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0))(vue@3.5.25(typescript@6.0.2)) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.14 + version: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + vite-plugin-solid: + specifier: ^2.11.11 + version: 2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@1.9.12)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + vitest: + specifier: ^4.1.4 + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) benchmarks/ssr: dependencies: @@ -369,7 +497,7 @@ importers: version: 2.11.11(@testing-library/jest-dom@6.6.3)(solid-js@1.9.12)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) e2e/e2e-utils: devDependencies: @@ -414,7 +542,7 @@ importers: version: 5.9.2 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) e2e/react-router/basepath-file-based: dependencies: @@ -1859,7 +1987,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) sass: specifier: ^1.97.2 version: 1.97.2 @@ -2024,7 +2152,7 @@ importers: version: 2.0.1 '@rsbuild/plugin-react': specifier: ^2.0.0 - version: 2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) + version: 2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.21)) '@tanstack/router-e2e-utils': specifier: workspace:^ version: link:../../e2e-utils @@ -2088,7 +2216,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) srvx: specifier: ^0.11.9 version: 0.11.12 @@ -2186,7 +2314,7 @@ importers: version: 9.2.1 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) typescript: specifier: ^6.0.2 version: 6.0.2 @@ -3222,7 +3350,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) typescript: specifier: ^6.0.2 version: 6.0.2 @@ -3243,7 +3371,7 @@ importers: version: link:../../../packages/start-static-server-functions nitro: specifier: ^3.0.1-alpha.2 - version: 3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.5.1)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) react: specifier: ^19.2.3 version: 19.2.3 @@ -8937,7 +9065,7 @@ importers: version: 6.0.1(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -9679,7 +9807,7 @@ importers: version: 0.5.20(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) nitro: specifier: npm:nitro-nightly@latest - version: nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.1.18 version: 4.2.2 @@ -11428,7 +11556,7 @@ importers: version: 25.0.9 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -11640,7 +11768,7 @@ importers: version: 25.0.9 nitro: specifier: ^3.0.260311-beta - version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -13600,9 +13728,21 @@ packages: '@asamuzakjp/css-color@4.0.5': resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + '@asamuzakjp/dom-selector@6.5.6': resolution: {integrity: sha512-Mj3Hu9ymlsERd7WOsUKNUZnJYL4IZ/I9wVVYgtvOsWYiEFbkQ4G7VRIh2USxTVW4BBDIsLG+gBUgqOqf2Kvqow==} + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} @@ -13940,6 +14080,10 @@ packages: '@braidai/lang@1.1.2': resolution: {integrity: sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@bundled-es-modules/cookie@2.0.1': resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} @@ -14191,6 +14335,10 @@ packages: resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + '@csstools/css-calc@2.1.1': resolution: {integrity: sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==} engines: {node: '>=18'} @@ -14205,6 +14353,13 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-color-parser@3.0.7': resolution: {integrity: sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==} engines: {node: '>=18'} @@ -14219,6 +14374,13 @@ packages: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-color-parser@4.1.3': + resolution: {integrity: sha512-DOgvIPkikIOixQRlD4YF31VN6fLLUTdrzhfRbis8vm0kMTgIbEPX0Ip/YX9fOeV9iywAS4sUUbTclpan7yYP8Q==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms@3.0.4': resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==} engines: {node: '>=18'} @@ -14231,12 +14393,26 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-syntax-patches-for-csstree@1.0.14': resolution: {integrity: sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==} engines: {node: '>=18'} peerDependencies: postcss: ^8.4 + '@csstools/css-syntax-patches-for-csstree@1.1.5': + resolution: {integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + '@csstools/css-tokenizer@3.0.3': resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} engines: {node: '>=18'} @@ -14245,6 +14421,10 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@dabh/diagnostics@2.0.8': resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} @@ -15216,6 +15396,15 @@ packages: resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@fastify/accept-negotiator@2.0.1': resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} @@ -20996,6 +21185,10 @@ packages: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} @@ -21041,6 +21234,10 @@ packages: resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} engines: {node: '>=20'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} @@ -21097,6 +21294,9 @@ packages: decimal.js@10.5.0: resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} peerDependencies: @@ -21439,6 +21639,10 @@ packages: resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==} engines: {node: '>=0.12'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -22350,6 +22554,10 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-entities@2.3.3: resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} @@ -22892,6 +23100,15 @@ packages: canvas: optional: true + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -23216,6 +23433,10 @@ packages: resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} engines: {node: 20 || >=22} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -23285,6 +23506,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -23948,6 +24172,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -25423,6 +25650,10 @@ packages: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -25634,6 +25865,10 @@ packages: resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} engines: {node: '>=20.18.1'} + undici@7.27.2: + resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} + engines: {node: '>=20.18.1'} + unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} @@ -26318,6 +26553,10 @@ packages: resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} engines: {node: '>=20'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-cli@5.1.4: resolution: {integrity: sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==} engines: {node: '>=14.15.0'} @@ -26412,6 +26651,10 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + whatwg-url@14.1.0: resolution: {integrity: sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==} engines: {node: '>=18'} @@ -26420,6 +26663,10 @@ packages: resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} engines: {node: '>=20'} + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -26701,6 +26948,14 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 11.2.2 + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@asamuzakjp/dom-selector@6.5.6': dependencies: '@asamuzakjp/nwsapi': 2.3.9 @@ -26709,6 +26964,16 @@ snapshots: is-potential-custom-element-name: 1.0.1 lru-cache: 11.2.2 + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + '@asamuzakjp/nwsapi@2.3.9': {} '@assemblyscript/loader@0.19.23': {} @@ -27252,6 +27517,10 @@ snapshots: '@braidai/lang@1.1.2': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.1.0 + '@bundled-es-modules/cookie@2.0.1': dependencies: cookie: 0.7.2 @@ -27585,7 +27854,7 @@ snapshots: '@codspeed/core': 5.5.0 tinybench: 2.9.0 vite: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) - vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) transitivePeerDependencies: - debug - supports-color @@ -27626,6 +27895,8 @@ snapshots: '@csstools/color-helpers@5.1.0': {} + '@csstools/color-helpers@6.0.2': {} + '@csstools/css-calc@2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3) @@ -27636,6 +27907,11 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-color-parser@3.0.7(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/color-helpers': 5.0.1 @@ -27650,6 +27926,13 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-color-parser@4.1.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)': dependencies: '@csstools/css-tokenizer': 3.0.3 @@ -27658,14 +27941,24 @@ snapshots: dependencies: '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-syntax-patches-for-csstree@1.0.14(postcss@8.5.15)': dependencies: postcss: 8.5.15 + '@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + '@csstools/css-tokenizer@3.0.3': {} '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-tokenizer@4.0.0': {} + '@dabh/diagnostics@2.0.8': dependencies: '@so-ric/colorspace': 1.1.6 @@ -28352,6 +28645,10 @@ snapshots: '@eslint/core': 0.12.0 levn: 0.4.1 + '@exodus/bytes@1.15.1(@noble/hashes@2.0.1)': + optionalDependencies: + '@noble/hashes': 2.0.1 + '@fastify/accept-negotiator@2.0.1': {} '@fastify/ajv-compiler@4.0.5': @@ -31449,9 +31746,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.23))': + '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.21))': dependencies: - '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(react-refresh@0.18.0) + '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.21))(react-refresh@0.18.0) react-refresh: 0.18.0 optionalDependencies: '@rsbuild/core': 2.0.1 @@ -31642,6 +31939,13 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.23 + '@rspack/core@2.0.5(@swc/helpers@0.5.21)': + dependencies: + '@rspack/binding': 2.0.5 + optionalDependencies: + '@swc/helpers': 0.5.21 + optional: true + '@rspack/core@2.0.5(@swc/helpers@0.5.23)': dependencies: '@rspack/binding': 2.0.5 @@ -31650,6 +31954,12 @@ snapshots: '@rspack/lite-tapable@1.1.0': {} + '@rspack/plugin-react-refresh@2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.21))(react-refresh@0.18.0)': + dependencies: + react-refresh: 0.18.0 + optionalDependencies: + '@rspack/core': 2.0.5(@swc/helpers@0.5.21) + '@rspack/plugin-react-refresh@2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(react-refresh@0.18.0)': dependencies: react-refresh: 0.18.0 @@ -34995,6 +35305,11 @@ snapshots: mdn-data: 2.12.2 source-map-js: 1.2.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css-what@6.1.0: {} css.escape@1.5.1: {} @@ -35036,6 +35351,13 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 15.1.0 + data-urls@7.0.0(@noble/hashes@2.0.1): + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.0.1) + transitivePeerDependencies: + - '@noble/hashes' + dataloader@1.4.0: {} date-fns@2.30.0: @@ -35064,6 +35386,8 @@ snapshots: decimal.js@10.5.0: {} + decimal.js@10.6.0: {} + dedent@1.5.1(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 @@ -35387,6 +35711,8 @@ snapshots: entities@6.0.0: {} + entities@8.0.0: {} + env-paths@2.2.1: {} env-paths@3.0.0: {} @@ -36602,6 +36928,12 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1) + transitivePeerDependencies: + - '@noble/hashes' + html-entities@2.3.3: {} html-link-extractor@1.0.5: @@ -37178,6 +37510,32 @@ snapshots: - supports-color - utf-8-validate + jsdom@29.1.1(@noble/hashes@2.0.1): + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) + '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1) + css-tree: 3.2.1 + data-urls: 7.0.0(@noble/hashes@2.0.1) + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.0.1) + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.27.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1(@noble/hashes@2.0.1) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -37479,6 +37837,8 @@ snapshots: lru-cache@11.2.2: {} + lru-cache@11.5.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -37539,6 +37899,8 @@ snapshots: mdn-data@2.12.2: {} + mdn-data@2.27.1: {} + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -37782,7 +38144,7 @@ snapshots: nf3@0.3.6: {} - nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + nitro-nightly@3.0.260522-beta(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.5(srvx@0.11.15) @@ -37797,7 +38159,7 @@ snapshots: rolldown: 1.0.2 srvx: 0.11.15 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.4.2 giget: 2.0.0 @@ -37834,7 +38196,7 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + nitro@3.0.1-alpha.2(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(ioredis@5.9.2)(lru-cache@11.5.1)(mysql2@3.15.3)(rolldown@1.0.2)(rollup@4.56.0)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.3(srvx@0.10.1) @@ -37849,7 +38211,7 @@ snapshots: srvx: 0.10.1 undici: 7.24.4 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) optionalDependencies: rolldown: 1.0.2 rollup: 4.56.0 @@ -37885,7 +38247,7 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + nitro@3.0.260311-beta(@electric-sql/pglite@0.3.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@libsql/client@0.15.15)(@netlify/blobs@10.1.0)(chokidar@5.0.0)(dotenv@17.4.2)(giget@2.0.0)(jiti@2.7.0)(lru-cache@11.5.1)(miniflare@4.20260317.0)(mysql2@3.15.3)(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: consola: 3.4.2 crossws: 0.4.4(srvx@0.11.15) @@ -37900,7 +38262,7 @@ snapshots: rolldown: 1.0.0-rc.9(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) srvx: 0.11.15 unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3) + unstorage: 2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3) optionalDependencies: dotenv: 17.4.2 giget: 2.0.0 @@ -38555,6 +38917,10 @@ snapshots: dependencies: entities: 6.0.0 + parse5@8.0.1: + dependencies: + entities: 8.0.0 + parseurl@1.3.3: {} pascal-case@3.1.2: @@ -40200,6 +40566,10 @@ snapshots: dependencies: tldts: 7.0.16 + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.16 + tr46@0.0.3: {} tr46@5.0.0: @@ -40375,6 +40745,8 @@ snapshots: undici@7.24.4: {} + undici@7.27.2: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -40476,27 +40848,29 @@ snapshots: db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) ioredis: 5.9.2 - unstorage@2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.2.2)(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.5(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.9.2)(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): optionalDependencies: '@netlify/blobs': 10.1.0 chokidar: 5.0.0 db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) ioredis: 5.9.2 - lru-cache: 11.2.2 + lru-cache: 11.5.1 ofetch: 2.0.0-alpha.3 - unstorage@2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.6(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): optionalDependencies: '@netlify/blobs': 10.1.0 chokidar: 5.0.0 db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) + lru-cache: 11.5.1 ofetch: 2.0.0-alpha.3 - unstorage@2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ofetch@2.0.0-alpha.3): + unstorage@2.0.0-alpha.7(@netlify/blobs@10.1.0)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(lru-cache@11.5.1)(ofetch@2.0.0-alpha.3): optionalDependencies: '@netlify/blobs': 10.1.0 chokidar: 5.0.0 db0: 0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3) + lru-cache: 11.5.1 ofetch: 2.0.0-alpha.3 untun@0.1.3: @@ -40714,10 +41088,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -40743,7 +41117,36 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@27.0.0(postcss@8.5.15))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@5.9.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.0.9 + '@vitest/ui': 4.1.4(vitest@4.1.4) + jsdom: 29.1.1(@noble/hashes@2.0.1) + transitivePeerDependencies: + - msw + + vitest@4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.4 '@vitest/mocker': 4.1.4(msw@2.7.0(@types/node@25.0.9)(typescript@6.0.2))(vite@8.0.14(@types/node@25.0.9)(esbuild@0.27.4)(jiti@2.7.0)(sass@1.97.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.9.0)) @@ -40768,7 +41171,7 @@ snapshots: optionalDependencies: '@types/node': 25.0.9 '@vitest/ui': 4.1.4(vitest@4.1.4) - jsdom: 27.0.0(postcss@8.5.15) + jsdom: 29.1.1(@noble/hashes@2.0.1) transitivePeerDependencies: - msw @@ -40909,6 +41312,8 @@ snapshots: webidl-conversions@8.0.0: {} + webidl-conversions@8.0.1: {} + webpack-cli@5.1.4(webpack-dev-server@5.2.4)(webpack@5.97.1): dependencies: '@discoveryjs/json-ext': 0.5.7 @@ -41100,6 +41505,8 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} + whatwg-url@14.1.0: dependencies: tr46: 5.0.0 @@ -41110,6 +41517,14 @@ snapshots: tr46: 6.0.0 webidl-conversions: 8.0.0 + whatwg-url@16.0.1(@noble/hashes@2.0.1): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@2.0.1) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 25cba27b56..2c130509f2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,6 +7,7 @@ trustPolicy: 'no-downgrade' packages: - 'packages/*' - 'benchmarks/*' + - 'benchmarks/memory/*' - 'examples/react/*' - 'examples/solid/*' - 'examples/vue/*'