From eab757035f4632ddd79d78e85e0b525b76fa6af2 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 00:02:21 +0200 Subject: [PATCH 01/24] benchmark: memory (codspeed) --- .github/workflows/memory-benchmarks.yml | 49 +++ benchmarks/memory/README.md | 122 +++++++ benchmarks/memory/client/bench-utils.ts | 19 ++ benchmarks/memory/client/package.json | 68 ++++ .../react/memory.bench.ts | 51 +++ .../react/project.json | 31 ++ .../interrupted-navigations/react/setup.ts | 262 +++++++++++++++ .../interrupted-navigations/react/src/app.tsx | 37 +++ .../react/src/routeTree.gen.ts | 95 ++++++ .../react/src/router.tsx | 17 + .../react/src/routes/__root.tsx | 9 + .../react/src/routes/fast.$id.tsx | 21 ++ .../react/src/routes/index.tsx | 9 + .../react/src/routes/slow.$id.tsx | 21 ++ .../react/src/slow-loaders.ts | 54 ++++ .../react/tsconfig.json | 18 ++ .../react/vite.config.ts | 34 ++ .../react/memory.bench.ts | 48 +++ .../loader-data-retention/react/project.json | 31 ++ .../loader-data-retention/react/setup.ts | 148 +++++++++ .../loader-data-retention/react/src/app.tsx | 33 ++ .../react/src/loader-data.ts | 48 +++ .../react/src/routeTree.gen.ts | 102 ++++++ .../react/src/router.tsx | 19 ++ .../react/src/routes/__root.tsx | 9 + .../react/src/routes/page.$id.tsx | 21 ++ .../react/src/routes/shell.index.tsx | 9 + .../react/src/routes/shell.tsx | 9 + .../loader-data-retention/react/tsconfig.json | 18 ++ .../react/vite.config.ts | 34 ++ .../mount-unmount/react/memory.bench.ts | 21 ++ .../mount-unmount/react/project.json | 31 ++ .../scenarios/mount-unmount/react/setup.ts | 66 ++++ .../scenarios/mount-unmount/react/src/app.tsx | 29 ++ .../mount-unmount/react/src/routeTree.gen.ts | 59 ++++ .../mount-unmount/react/src/router.tsx | 17 + .../mount-unmount/react/src/routes/__root.tsx | 9 + .../mount-unmount/react/src/routes/a.tsx | 12 + .../mount-unmount/react/tsconfig.json | 18 ++ .../mount-unmount/react/vite.config.ts | 34 ++ .../navigation-churn/react/memory.bench.ts | 34 ++ .../navigation-churn/react/project.json | 31 ++ .../scenarios/navigation-churn/react/setup.ts | 117 +++++++ .../navigation-churn/react/src/app.tsx | 31 ++ .../react/src/routeTree.gen.ts | 77 +++++ .../navigation-churn/react/src/router.tsx | 17 + .../react/src/routes/__root.tsx | 9 + .../navigation-churn/react/src/routes/a.tsx | 14 + .../navigation-churn/react/src/routes/b.tsx | 14 + .../navigation-churn/react/tsconfig.json | 18 ++ .../navigation-churn/react/vite.config.ts | 34 ++ .../preload-churn/react/memory.bench.ts | 55 ++++ .../preload-churn/react/project.json | 31 ++ .../scenarios/preload-churn/react/setup.ts | 239 ++++++++++++++ .../scenarios/preload-churn/react/src/app.tsx | 33 ++ .../preload-churn/react/src/item-payload.ts | 51 +++ .../preload-churn/react/src/routeTree.gen.ts | 77 +++++ .../preload-churn/react/src/router.tsx | 18 ++ .../preload-churn/react/src/routes/__root.tsx | 9 + .../preload-churn/react/src/routes/index.tsx | 9 + .../react/src/routes/items.$id.tsx | 20 ++ .../preload-churn/react/tsconfig.json | 18 ++ .../preload-churn/react/vite.config.ts | 34 ++ .../react/memory.bench.ts | 46 +++ .../unique-location-churn/react/project.json | 31 ++ .../unique-location-churn/react/setup.ts | 123 +++++++ .../unique-location-churn/react/src/app.tsx | 31 ++ .../react/src/routeTree.gen.ts | 59 ++++ .../react/src/router.tsx | 17 + .../react/src/routes/__root.tsx | 9 + .../react/src/routes/items.$id.tsx | 28 ++ .../unique-location-churn/react/tsconfig.json | 18 ++ .../react/vite.config.ts | 34 ++ benchmarks/memory/client/tsconfig.json | 7 + .../memory/client/vitest.react.config.ts | 8 + benchmarks/memory/client/vitest.setup.ts | 6 + benchmarks/memory/server/bench-utils.ts | 63 ++++ benchmarks/memory/server/package.json | 68 ++++ .../aborted-requests/react/memory.bench.ts | 160 +++++++++ .../aborted-requests/react/project.json | 31 ++ .../react/src/routeTree.gen.ts | 86 +++++ .../aborted-requests/react/src/router.tsx | 16 + .../react/src/routes/__root.tsx | 27 ++ .../react/src/routes/index.tsx | 9 + .../react/src/routes/stream.$id.tsx | 77 +++++ .../aborted-requests/react/tsconfig.json | 14 + .../aborted-requests/react/vite.config.ts | 29 ++ .../error-paths/react/memory.bench.ts | 201 ++++++++++++ .../scenarios/error-paths/react/project.json | 31 ++ .../error-paths/react/src/routeTree.gen.ts | 146 +++++++++ .../error-paths/react/src/router.tsx | 23 ++ .../error-paths/react/src/routes/__root.tsx | 27 ++ .../error-paths/react/src/routes/boom.$id.tsx | 17 + .../error-paths/react/src/routes/from.$id.tsx | 14 + .../error-paths/react/src/routes/index.tsx | 9 + .../react/src/routes/missing.$id.tsx | 19 ++ .../react/src/routes/target.$id.tsx | 11 + .../scenarios/error-paths/react/tsconfig.json | 14 + .../error-paths/react/vite.config.ts | 29 ++ .../peak-large-page/react/memory.bench.ts | 76 +++++ .../peak-large-page/react/project.json | 31 ++ .../react/src/large-page-data.ts | 111 +++++++ .../react/src/routeTree.gen.ts | 305 ++++++++++++++++++ .../peak-large-page/react/src/router.tsx | 16 + .../react/src/routes/__root.tsx | 27 ++ .../react/src/routes/index.tsx | 9 + .../src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 25 ++ .../react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 26 ++ .../react/src/routes/l1.l2.l3.l4.l5.l6.tsx | 26 ++ .../react/src/routes/l1.l2.l3.l4.l5.tsx | 26 ++ .../react/src/routes/l1.l2.l3.l4.tsx | 26 ++ .../react/src/routes/l1.l2.l3.tsx | 26 ++ .../react/src/routes/l1.l2.tsx | 26 ++ .../peak-large-page/react/src/routes/l1.tsx | 26 ++ .../peak-large-page/react/tsconfig.json | 14 + .../peak-large-page/react/vite.config.ts | 29 ++ .../request-churn/react/memory.bench.ts | 83 +++++ .../request-churn/react/project.json | 31 ++ .../request-churn/react/src/routeTree.gen.ts | 86 +++++ .../request-churn/react/src/router.tsx | 16 + .../request-churn/react/src/routes/__root.tsx | 27 ++ .../request-churn/react/src/routes/index.tsx | 9 + .../react/src/routes/items.$id.tsx | 40 +++ .../request-churn/react/tsconfig.json | 14 + .../request-churn/react/vite.config.ts | 29 ++ .../react/memory.bench.ts | 105 ++++++ .../serialization-payload/react/project.json | 31 ++ .../react/src/routeTree.gen.ts | 68 ++++ .../react/src/router.tsx | 16 + .../react/src/routes/__root.tsx | 27 ++ .../react/src/routes/data.$id.tsx | 130 ++++++++ .../serialization-payload/react/tsconfig.json | 14 + .../react/vite.config.ts | 29 ++ .../server-fn-churn/react/memory.bench.ts | 261 +++++++++++++++ .../server-fn-churn/react/project.json | 31 ++ .../server-fn-churn/react/src/fns.ts | 47 +++ .../react/src/routeTree.gen.ts | 86 +++++ .../server-fn-churn/react/src/router.tsx | 16 + .../react/src/routes/__root.tsx | 27 ++ .../react/src/routes/api.fn-urls.ts | 14 + .../react/src/routes/index.tsx | 18 ++ .../server-fn-churn/react/tsconfig.json | 14 + .../server-fn-churn/react/vite.config.ts | 29 ++ .../streaming-peak/react/memory.bench.ts | 217 +++++++++++++ .../streaming-peak/react/project.json | 31 ++ .../streaming-peak/react/src/routeTree.gen.ts | 86 +++++ .../streaming-peak/react/src/router.tsx | 16 + .../react/src/routes/__root.tsx | 27 ++ .../streaming-peak/react/src/routes/index.tsx | 9 + .../react/src/routes/stream.$id.tsx | 107 ++++++ .../streaming-peak/react/tsconfig.json | 14 + .../streaming-peak/react/vite.config.ts | 29 ++ benchmarks/memory/server/tsconfig.json | 7 + .../memory/server/vitest.react.config.ts | 8 + .../src/ssr/renderRouterToStream.tsx | 14 +- pnpm-lock.yaml | 87 ++++- pnpm-workspace.yaml | 1 + 157 files changed, 7042 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/memory-benchmarks.yml create mode 100644 benchmarks/memory/README.md create mode 100644 benchmarks/memory/client/bench-utils.ts create mode 100644 benchmarks/memory/client/package.json create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/fast.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/index.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/routes/slow.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/src/slow-loaders.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/project.json create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/page.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.index.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/src/routes/shell.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/project.json create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/src/routes/a.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/project.json create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/a.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/src/routes/b.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/project.json create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/setup.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/item-payload.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/routes/index.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/src/routes/items.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/project.json create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/src/routes/items.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/vite.config.ts create mode 100644 benchmarks/memory/client/tsconfig.json create mode 100644 benchmarks/memory/client/vitest.react.config.ts create mode 100644 benchmarks/memory/client/vitest.setup.ts create mode 100644 benchmarks/memory/server/bench-utils.ts create mode 100644 benchmarks/memory/server/package.json create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/project.json create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/project.json create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/boom.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/from.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/missing.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/src/routes/target.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/project.json create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/large-page-data.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/project.json create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/src/routes/items.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/project.json create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/project.json create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/api.fn-urls.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/project.json create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/vite.config.ts create mode 100644 benchmarks/memory/server/tsconfig.json create mode 100644 benchmarks/memory/server/vitest.react.config.ts diff --git a/.github/workflows/memory-benchmarks.yml b/.github/workflows/memory-benchmarks.yml new file mode 100644 index 0000000000..a7c23f8a1b --- /dev/null +++ b/.github/workflows/memory-benchmarks.yml @@ -0,0 +1,49 @@ +# Setup taken from https://codspeed.io/docs/benchmarks/nodejs/vitest +name: Memory Benchmarks + +on: + push: + branches: + - 'main' + paths: + - 'packages/**' + - 'benchmarks/**' + pull_request: + paths: + - 'packages/**' + - 'benchmarks/**' + workflow_dispatch: + +permissions: + contents: read # required for actions/checkout + id-token: write # required for OIDC authentication with CodSpeed + +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + SERVER_PRESET: 'node-server' + NX_NO_CLOUD: true + +jobs: + benchmarks: + name: Run ${{ matrix.benchmark }} CodSpeed benchmark + strategy: + fail-fast: false + matrix: + benchmark: + - memory-server + - memory-client + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Tools + uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + + - name: Run ${{ matrix.benchmark }} CodSpeed benchmark + uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0 + with: + mode: memory + run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:react diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md new file mode 100644 index 0000000000..c03f112329 --- /dev/null +++ b/benchmarks/memory/README.md @@ -0,0 +1,122 @@ +# Memory Benchmarks + +Dedicated memory benchmarks for TanStack Router / Start, measured with the +CodSpeed **memory instrument** (`mode: memory` in +`.github/workflows/memory-benchmarks.yml`). Two separate benchmarks: + +- `server/` (`@benchmarks/memory-server`) — React Start apps, requests against + the built server handler (`handler.fetch`), Node environment. +- `client/` (`@benchmarks/memory-client`) — router-only React 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. React-first; each scenario keeps a `react/` level so solid/vue can +be added later without renames. + +## Layout + +```text +benchmarks/memory// + package.json Nx targets: build:react, test:perf:react, test:types + bench-utils.ts memoryBenchOptions, seeded LCG (+ sequential request loop on the server side) + vitest.react.config.ts aggregates scenarios/*/react/vite.config.ts + scenarios//react/ one isolated app per scenario + memory.bench.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. + +## 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, because React schedules + stream flushes via `setImmediate` and any non-timer deferral wins that race, + suppressing the Suspense fallbacks the scenario exists to stream. +- 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 + (React root, `__TSR_ROUTER__`, `history.destroy()`); large loader payloads + are never rendered into the DOM. +- `NODE_ENV=production` everywhere (the Nx targets set it). + +## Run + +```bash +pnpm nx run @benchmarks/memory-server:test:perf:react --outputStyle=stream --skipRemoteCache +pnpm nx run @benchmarks/memory-client:test:perf:react --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 +``` + +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-client:test:perf:react +``` diff --git a/benchmarks/memory/client/bench-utils.ts b/benchmarks/memory/client/bench-utils.ts new file mode 100644 index 0000000000..28bfe6c512 --- /dev/null +++ b/benchmarks/memory/client/bench-utils.ts @@ -0,0 +1,19 @@ +export const memoryBenchOptions = { + iterations: 1, + warmupIterations: 1, + time: 0, + warmupTime: 0, +} + +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/package.json b/benchmarks/memory/client/package.json new file mode 100644 index 0000000000..1bb60b0c1c --- /dev/null +++ b/benchmarks/memory/client/package.json @@ -0,0 +1,68 @@ +{ + "name": "@benchmarks/memory-client", + "private": true, + "type": "module", + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/router-core": "workspace:^", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@codspeed/vitest-plugin": "^5.5.0", + "@testing-library/react": "^16.2.0", + "@vitejs/plugin-react": "^6.0.1", + "@types/jsdom": "28.0.0", + "typescript": "^6.0.2", + "vite": "^8.0.14", + "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" + } + ] + }, + "test:perf:react": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + "build:react" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.react.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" + ], + "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..bc4c8ca6cd --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts @@ -0,0 +1,51 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { + createDeterministicRandom, + memoryBenchOptions, + randomSegment, +} from '../../../bench-utils' +import { setup } from './setup' + +const interruptedNavigationIterations = 150 + +const interruptedNavigationPairs = createInterruptedNavigationPairs( + interruptedNavigationIterations, +) + +function createInterruptedNavigationPairs(iterations: number) { + const random = createDeterministicRandom(13) + + return Array.from({ length: iterations }, (_, index) => ({ + slowId: `slow-${index}-${randomSegment(random)}`, + fastId: `fast-${index}-${randomSegment(random)}`, + })) +} + +await setup().sanity() + +describe('memory', () => { + const test = setup() + + /** + * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only + * honors tinybench's setup/teardown options; the CodSpeed runner does the + * exact opposite. Both registrations are load-bearing — exactly one pair + * runs in any given mode. + */ + beforeAll(test.before) + afterAll(test.after) + + bench( + 'mem interrupted-navigations (react)', + async () => { + for (const pair of interruptedNavigationPairs) { + await test.interrupt(pair.slowId, pair.fastId) + } + }, + { + ...memoryBenchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) 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..77ccf8d85a --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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..4cc8182b05 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts @@ -0,0 +1,262 @@ +import type * as App from './src/app' + +type NavigationSettlement = + | { + status: 'fulfilled' + value: void + } + | { + status: 'rejected' + reason: unknown + } + +const appModulePath = './dist/app.js' +const { + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} = (await import(/* @vite-ignore */ appModulePath)) as typeof App + +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'), + }) + +async function drainMicrotasks() { + await Promise.resolve() + await Promise.resolve() +} + +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, + )}`, + ) +} + +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 setup() { + if (process.env.NODE_ENV !== 'production') { + console.warn( + 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', + ) + } + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let resolveRendered: () => void = () => {} + 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 new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + 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 = document.createElement('div') + document.body.append(container) + + const mounted = mountTestApp(container) + const { router } = mounted + unmount = mounted.unmount + getLatestLoadPromise = () => router.latestLoadPromise + + unsub = router.subscribe('onRendered', (event) => { + if ( + expectedRenderedPath && + event.toLocation.pathname !== expectedRenderedPath + ) { + return + } + + const resolve = resolveRendered + resolveRendered = () => {} + 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): NavigationSettlement => ({ status: 'rejected', reason }), + ) + } + + await router.load() + await waitForRenderedPage('shell') + } + + function after() { + resolveAllSlowLoaders() + unmount?.() + container?.remove() + unsub() + + container = undefined + unmount = undefined + unsub = () => {} + resolveRendered = () => {} + expectedRenderedPath = undefined + navigateFast = uninitialized + startSlowNavigation = uninitializedSettlement + getLatestLoadPromise = () => undefined + } + + async function interrupt( + slowId: string, + fastId: string, + assertShape = false, + ) { + 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 + // Superseded loads currently always resolve; the guard keeps the bench + // alive if router-core ever starts rejecting them. + await slowLoadPromise.catch(() => undefined) + await drainMicrotasks() + + if (assertShape) { + assertSlowNavigationSettlement(settlement) + assertRenderedPage('fast', fastId) + } + } + + return { + before, + interrupt, + async sanity() { + await before() + + try { + assertRenderedPage('shell') + await interrupt('sanity-slow', 'sanity-fast', true) + } finally { + after() + } + }, + after, + } +} 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..c77230007a --- /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..146b266f1b --- /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..15c7d996dc --- /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/src/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/slow-loaders.ts new file mode 100644 index 0000000000..c70ccac3da --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/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/react/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json new file mode 100644 index 0000000000..26767bd491 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "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", + "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/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..331c74fb2a --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts @@ -0,0 +1,48 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { + createDeterministicRandom, + memoryBenchOptions, + randomSegment, +} from '../../../bench-utils' +import { setup } from './setup' + +const loaderDataRetentionNavigationCount = 20 +const pageIds = createPageIds() + +await setup().sanity() + +describe('memory', () => { + const test = setup() + + /** + * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only + * honors tinybench's setup/teardown options; the CodSpeed runner does the + * exact opposite. Both registrations are load-bearing — exactly one pair + * runs in any given mode. + */ + beforeAll(test.before) + afterAll(test.after) + + bench( + 'mem loader-data-retention (react)', + async () => { + for (const id of pageIds) { + await test.navigate(id) + } + }, + { + ...memoryBenchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) + +function createPageIds() { + const random = createDeterministicRandom(11) + + return Array.from( + { length: loaderDataRetentionNavigationCount }, + (_, index) => `${index}-${randomSegment(random)}`, + ) +} 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..953558d3f6 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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..3459edec5a --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts @@ -0,0 +1,148 @@ +import type * as App from './src/app' + +const appModulePath = './dist/app.js' +const { loaderPayloadRecordCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +const uninitialized = () => + Promise.reject( + new Error('loader-data-retention benchmark is not initialized'), + ) + +export function setup() { + if (process.env.NODE_ENV !== 'production') { + console.warn( + 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', + ) + } + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let resolveRendered: () => void = () => {} + 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 new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + assertRenderedShell() + } + + function waitForNextRender(pathname: string) { + expectedRenderedPath = pathname + + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = document.createElement('div') + document.body.append(container) + + const mounted = mountTestApp(container) + const { router } = mounted + unmount = mounted.unmount + + unsub = router.subscribe('onRendered', (event) => { + if ( + expectedRenderedPath && + event.toLocation.pathname !== expectedRenderedPath + ) { + return + } + + const resolve = resolveRendered + resolveRendered = () => {} + 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?.() + container?.remove() + unsub() + + container = undefined + unmount = undefined + unsub = () => {} + resolveRendered = () => {} + expectedRenderedPath = undefined + navigateTo = uninitialized + } + + return { + before, + navigate: (id: string) => navigateTo(id), + async sanity() { + await before() + + try { + await navigateTo('sanity-a') + assertRenderedPage('sanity-a') + await navigateTo('sanity-b') + assertRenderedPage('sanity-b') + } finally { + after() + } + }, + after, + } +} 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..8e09296b9c --- /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/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts new file mode 100644 index 0000000000..d2a73a6542 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts @@ -0,0 +1,48 @@ +import { + createDeterministicRandom, + randomSegment, +} from '../../../../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/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..bb387a7034 --- /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..26767bd491 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "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", + "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/mount-unmount/react/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts new file mode 100644 index 0000000000..042109dd19 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts @@ -0,0 +1,21 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '../../../bench-utils' +import { setup } from './setup' + +const mountUnmountIterations = 100 + +await setup().sanity() + +describe('memory', () => { + const test = setup() + + bench( + 'mem mount-unmount (react)', + async () => { + for (let index = 0; index < mountUnmountIterations; index++) { + await test.cycle() + } + }, + memoryBenchOptions, + ) +}) 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..f701b92e2a --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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..3ba792e11a --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts @@ -0,0 +1,66 @@ +import type * as App from './src/app' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +function drainMicrotasks() { + return Promise.resolve().then(() => Promise.resolve()) +} + +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 setup() { + if (process.env.NODE_ENV !== 'production') { + console.warn( + 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', + ) + } + + async function cycle() { + const container = document.createElement('div') + document.body.append(container) + + let unmount = () => {} + let unsubscribe = () => {} + + try { + const mounted = mountTestApp(container) + const { router } = mounted + unmount = mounted.unmount + + const rendered = new Promise((resolve) => { + unsubscribe = router.subscribe('onRendered', () => { + resolve() + }) + }) + + await router.load() + await rendered + unsubscribe() + unsubscribe = () => {} + } finally { + unmount() + container.remove() + unsubscribe() + await drainMicrotasks() + } + } + + return { + cycle, + async sanity() { + assertEmptyBody() + await cycle() + await cycle() + assertEmptyBody() + }, + } +} 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..26767bd491 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "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", + "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/navigation-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts new file mode 100644 index 0000000000..7e2fe82027 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts @@ -0,0 +1,34 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '../../../bench-utils' +import { setup } from './setup' + +const navigationChurnIterations = 300 + +await setup().sanity() + +describe('memory', () => { + const test = setup() + + /** + * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only + * honors tinybench's setup/teardown options; the CodSpeed runner does the + * exact opposite. Both registrations are load-bearing — exactly one pair + * runs in any given mode. + */ + beforeAll(test.before) + afterAll(test.after) + + bench( + 'mem navigation-churn (react)', + async () => { + for (let index = 0; index < navigationChurnIterations; index++) { + await test.navigate(index % 2 === 0 ? '/b' : '/a') + } + }, + { + ...memoryBenchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) 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..1409a41b61 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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..f31c63c360 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts @@ -0,0 +1,117 @@ +import type * as App from './src/app' + +type Target = '/a' | '/b' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +const uninitialized = () => + Promise.reject(new Error('navigation-churn benchmark is not initialized')) + +export function setup() { + if (process.env.NODE_ENV !== 'production') { + console.warn( + 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', + ) + } + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let resolveRendered: () => void = () => {} + 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 new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + assertRenderedPage(target) + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = document.createElement('div') + document.body.append(container) + + const mounted = mountTestApp(container) + const { router } = mounted + 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?.() + container?.remove() + unsub() + + container = undefined + unmount = undefined + unsub = () => {} + resolveRendered = () => {} + navigateTo = uninitialized + } + + return { + before, + navigate: (target: Target) => navigateTo(target), + async sanity() { + await before() + + try { + assertRenderedPage('/a') + await navigateTo('/b') + assertRenderedPage('/b') + } finally { + after() + } + }, + after, + } +} 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..26767bd491 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "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", + "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/preload-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts new file mode 100644 index 0000000000..499fb8461b --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts @@ -0,0 +1,55 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { + createDeterministicRandom, + memoryBenchOptions, + randomSegment, +} from '../../../bench-utils' +import { setup } from './setup' + +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 the CodSpeed runner's multiple +// invocations (warmup + measured) of the bench fn 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 + +await setup().sanity() + +describe('memory', () => { + const test = setup() + + /** + * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only + * honors tinybench's setup/teardown options; the CodSpeed runner does the + * exact opposite. Both registrations are load-bearing — exactly one pair + * runs in any given mode. + */ + beforeAll(test.before) + afterAll(test.after) + + bench( + 'mem preload-churn (react)', + async () => { + for (let index = 0; index < preloadChurnIterations; index++) { + await test.preload( + `${(preloadCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`, + ) + + if ((index + 1) % preloadsPerEvictionNavigation === 0) { + await test.evictPreloads() + } + } + }, + { + ...memoryBenchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) 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..33c837bc6c --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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..34c2e51558 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts @@ -0,0 +1,239 @@ +import type * as App from './src/app' + +const appModulePath = './dist/app.js' +const { getTrackedItemLoaderCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +type MountedApp = ReturnType + +// 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 uninitialized = async (_id: string) => { + throw new Error('preload-churn benchmark is not initialized') +} + +async function drainMicrotasks() { + await Promise.resolve() + await Promise.resolve() +} + +export function setup() { + if (process.env.NODE_ENV !== 'production') { + console.warn( + 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', + ) + } + + let container: HTMLDivElement | undefined = undefined + let router: MountedApp['router'] | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let resolveRendered: () => void = () => {} + 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}`) + } + } + + function assertRenderedItem(id: string) { + const page = + container?.querySelector('[data-bench-page]')?.dataset + .benchPage + const actualId = + container?.querySelector('[data-bench-id]')?.dataset.benchId + + if (page !== 'item' || actualId !== id) { + throw new Error(`Expected rendered item ${id}, got ${page}:${actualId}`) + } + } + + function hasCachedItemMatch(id: string) { + return Boolean( + router?.stores.cachedMatches + .get() + .some((match) => (match.params as { id?: string }).id === id), + ) + } + + async function waitForRenderedIndex() { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedIndex() + return + } catch { + await new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + assertRenderedIndex() + } + + async function waitForRenderedItem(id: string) { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedItem(id) + return + } catch { + await new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + assertRenderedItem(id) + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = document.createElement('div') + document.body.append(container) + + const mounted = mountTestApp(container) + router = mounted.router + 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?.() + container?.remove() + unsub() + + container = undefined + router = undefined + unmount = undefined + unsub = () => {} + resolveRendered = () => {} + 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 { + before, + preload: (id: string) => preloadItem(id), + 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}`, + ) + } + + if (!hasCachedItemMatch(id)) { + throw new Error( + 'Expected preloaded match to sit in router.state.cachedMatches', + ) + } + + // A navigation commit runs clearExpiredCache; with + // defaultPreloadGcTime: 0 it must evict the preloaded match. This is + // the mechanism the bench's flat-floor expectation rests on. + await navigateToItem('sanity-evict-nav') + await waitForRenderedItem('sanity-evict-nav') + + if (hasCachedItemMatch(id)) { + throw new Error( + 'Expected the navigation commit to evict the preloaded match (preloadGcTime 0)', + ) + } + + await preloadItem(id) + + const repreloadedLoaderCount = getTrackedItemLoaderCount(id) + if (repreloadedLoaderCount !== preloadedLoaderCount + 1) { + throw new Error( + 'Expected re-preload after eviction to run the item loader again', + ) + } + } finally { + after() + } + }, + after, + } +} 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..8e60bad71d --- /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/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/react/src/item-payload.ts new file mode 100644 index 0000000000..4a86ea7244 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/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/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..36c8579ddf --- /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..26767bd491 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "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", + "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/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..db56f30d8d --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts @@ -0,0 +1,46 @@ +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { + createDeterministicRandom, + memoryBenchOptions, + randomSegment, +} from '../../../bench-utils' +import { setup } from './setup' + +const benchmarkRandom = createDeterministicRandom(0xdecafbad) +const uniqueLocationChurnIterations = 300 +// Module-level so ids stay unique across the CodSpeed runner's multiple +// invocations (warmup + measured) of the bench fn on one mount; the counter +// prefix removes any residual LCG birthday-collision risk. +let locationCounter = 0 + +await setup().sanity() + +describe('memory', () => { + const test = setup() + + /** + * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only + * honors tinybench's setup/teardown options; the CodSpeed runner does the + * exact opposite. Both registrations are load-bearing — exactly one pair + * runs in any given mode. + */ + beforeAll(test.before) + afterAll(test.after) + + bench( + 'mem unique-location-churn (react)', + async () => { + for (let index = 0; index < uniqueLocationChurnIterations; index++) { + const id = `${(locationCounter++).toString(36)}-${randomSegment(benchmarkRandom)}` + const q = `q-${randomSegment(benchmarkRandom)}` + + await test.navigate({ id, q }) + } + }, + { + ...memoryBenchOptions, + setup: test.before, + teardown: test.after, + }, + ) +}) 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..db7c57b1a3 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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..6f58a1281a --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts @@ -0,0 +1,123 @@ +import type * as App from './src/app' + +type ItemLocation = { + id: string + q: string +} + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +const uninitialized = () => + Promise.reject( + new Error('unique-location-churn benchmark is not initialized'), + ) + +export function setup() { + if (process.env.NODE_ENV !== 'production') { + console.warn( + 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', + ) + } + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let resolveRendered: () => void = () => {} + 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 new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + assertRenderedId(expected) + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = document.createElement('div') + document.body.append(container) + + const mounted = mountTestApp(container) + const { router } = mounted + 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?.() + container?.remove() + unsub() + + container = undefined + unmount = undefined + unsub = () => {} + resolveRendered = () => {} + navigateTo = uninitialized + } + + return { + before, + navigate: (location: ItemLocation) => navigateTo(location), + async sanity() { + await before() + + try { + await navigateTo({ id: 'sanity-one', q: 'q-sanity-one' }) + assertRenderedId('sanity-one') + await navigateTo({ id: 'sanity-two', q: 'q-sanity-two' }) + assertRenderedId('sanity-two') + } finally { + after() + } + }, + after, + } +} 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..26767bd491 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "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", + "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/tsconfig.json b/benchmarks/memory/client/tsconfig.json new file mode 100644 index 0000000000..5350368145 --- /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", "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..356ab7b3a8 --- /dev/null +++ b/benchmarks/memory/client/vitest.react.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: 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..9bac58bd41 --- /dev/null +++ b/benchmarks/memory/client/vitest.setup.ts @@ -0,0 +1,6 @@ +import { vi } from 'vitest' + +// @ts-expect-error +global.IS_REACT_ACT_ENVIRONMENT = true + +window.scrollTo = vi.fn() diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts new file mode 100644 index 0000000000..2613d81828 --- /dev/null +++ b/benchmarks/memory/server/bench-utils.ts @@ -0,0 +1,63 @@ +export interface StartRequestHandler { + fetch: (request: Request) => Promise | Response +} + +export interface RunSequentialRequestLoopOptions { + seed: number + iterations?: number + buildRequest: (random: () => number, index: number) => Request + validateResponse?: (response: Response, request: Request) => void + validateBody?: (body: string, response: Response, request: Request) => void +} + +export const memoryBenchOptions = { + iterations: 1, + warmupIterations: 1, + time: 0, + warmupTime: 0, +} + +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 runSequentialRequestLoop( + handler: StartRequestHandler, + { + seed, + iterations = 10, + buildRequest, + validateResponse, + validateBody, + }: RunSequentialRequestLoopOptions, +) { + const random = createDeterministicRandom(seed) + 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) + + const body = await response.text() + validateBody?.(body, response, request) + } +} diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json new file mode 100644 index 0000000000..5e4273c30b --- /dev/null +++ b/benchmarks/memory/server/package.json @@ -0,0 +1,68 @@ +{ + "name": "@benchmarks/memory-server", + "private": true, + "type": "module", + "dependencies": { + "@tanstack/react-router": "workspace:^", + "@tanstack/react-start": "workspace:^", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@codspeed/vitest-plugin": "^5.5.0", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "^6.0.2", + "vite": "^8.0.14", + "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" + } + ] + }, + "test:perf:react": { + "executor": "nx:run-commands", + "cache": false, + "dependsOn": [ + "build:react" + ], + "options": { + "command": "NODE_ENV=production vitest bench --config ./vitest.react.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" + ], + "target": "test:types:ssr" + } + ] + } + } + } +} 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..b800cb974f --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts @@ -0,0 +1,160 @@ +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '../../../bench-utils' +import type { StartRequestHandler } from '../../../bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const abortedRequestIterations = 100 +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 documentRequestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +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 drainCancellationMicrotasks() { + await Promise.resolve() + await Promise.resolve() +} + +async function assertAbortedRequestsSanity(handler: StartRequestHandler) { + 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') + } + + for (const marker of [ + alphaFirstRecord(fullId), + alphaLastRecord(fullId), + betaFirstRecord(fullId), + betaLastRecord(fullId), + ]) { + if (!fullBody.includes(marker)) { + throw new Error(`Expected full sanity response to include ${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, value } = await readFirstChunk( + midStreamResponse, + midStreamRequest, + ) + const text = textDecoder.decode(value) + + if (!text.includes(eagerMarker)) { + throw new Error('Expected first sanity chunk to include the eager marker') + } + + if ( + !text.includes(alphaFallbackMarker) || + !text.includes(betaFallbackMarker) + ) { + throw new Error('Expected first sanity chunk to include deferred fallbacks') + } + + for (const marker of [ + alphaFirstRecord(midStreamId), + alphaLastRecord(midStreamId), + betaFirstRecord(midStreamId), + betaLastRecord(midStreamId), + ]) { + if (text.includes(marker)) { + throw new Error( + `First sanity chunk already included deferred content ${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 reader.cancel() + await drainCancellationMicrotasks() +} + +async function runAbortedRequestLoop(handler: StartRequestHandler) { + for (let index = 0; index < abortedRequestIterations; index++) { + const controller = new AbortController() + const request = buildStreamRequest(`abort-${index}`, controller.signal) + const response = await handler.fetch(request) + validateDocumentResponse(response, request) + + const { reader } = await readFirstChunk(response, request) + controller.abort() + await reader.cancel() + await drainCancellationMicrotasks() + } +} + +await assertAbortedRequestsSanity(handler) + +describe('memory', () => { + bench( + 'mem aborted-requests (react)', + () => runAbortedRequestLoop(handler), + memoryBenchOptions, + ) +}) 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..c962404c09 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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/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..8772428338 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx @@ -0,0 +1,77 @@ +import { Await, createFileRoute } from '@tanstack/react-router' +import { Suspense } from 'react' + +const recordCount = 20 + +type RecordGroup = 'alpha' | 'beta' + +export interface DeferredRecord { + id: string + label: string +} + +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) +} + +function makeRecords(id: string, group: RecordGroup): Array { + return Array.from({ length: recordCount }, (_, index) => ({ + id: `${group}-${id}-${index}`, + label: `deferred-${group}-${id}-${index}`, + })) +} + +export const Route = createFileRoute('/stream/$id')({ + loader: ({ params }) => ({ + eager: `eager-${params.id}`, + alpha: resolveAfterMicrotasks(32, () => makeRecords(params.id, 'alpha')), + beta: resolveAfterMicrotasks(64, () => makeRecords(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..4d78884cea --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.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/error-paths/react/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts new file mode 100644 index 0000000000..32b536881b --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts @@ -0,0 +1,201 @@ +import { bench, describe } from 'vitest' +import { + memoryBenchOptions, + randomSegment, + runSequentialRequestLoop, +} from '../../../bench-utils' +import type { StartRequestHandler } from '../../../bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const errorPathsIterations = 50 +const redirectSeed = 0xdecafbad +const notFoundSeed = 0xdecafb0d +const errorSeed = 0xdecafbed +const unmatchedSeed = 0xdecaf00d + +const redirectStatus = 302 +const notFoundStatus = 404 +const errorStatus = 500 +const notFoundMarker = 'data-bench="not-found-boundary"' +const errorMarker = 'data-bench="error-boundary"' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +function buildRedirectRequest(random: () => number) { + return new Request( + `http://localhost/from/${randomSegment(random)}`, + requestInit, + ) +} + +function buildNotFoundRequest(random: () => number) { + return new Request( + `http://localhost/missing/${randomSegment(random)}`, + requestInit, + ) +} + +function buildErrorRequest(random: () => number) { + return new Request( + `http://localhost/boom/${randomSegment(random)}`, + requestInit, + ) +} + +function buildUnmatchedRequest(random: () => number) { + return new Request( + `http://localhost/nope/${randomSegment(random)}`, + 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}`, + ) + } +} + +function validateNotFoundBody(body: string) { + if (!body.includes(notFoundMarker)) { + throw new Error('Expected error-paths not-found marker in response body') + } +} + +function validateErrorBody(body: string) { + if (!body.includes(errorMarker)) { + throw new Error('Expected error-paths error marker in response body') + } +} + +async function assertStatusSanity( + request: Request, + validateResponse: (response: Response, request: Request) => void, + validateBody?: (body: string) => void, +) { + const response = await handler.fetch(request) + validateResponse(response, request) + + const body = await response.text() + validateBody?.(body) +} + +async function assertErrorPathsSanity() { + await assertStatusSanity( + new Request('http://localhost/from/sanity-redirect', requestInit), + validateRedirectResponse, + ) + await assertStatusSanity( + new Request('http://localhost/missing/sanity-missing', requestInit), + validateNotFoundResponse, + validateNotFoundBody, + ) + await assertStatusSanity( + new Request('http://localhost/boom/sanity-error', requestInit), + validateErrorResponse, + validateErrorBody, + ) + await assertStatusSanity( + new Request('http://localhost/nope/sanity-unmatched', requestInit), + validateNotFoundResponse, + ) +} + +await assertErrorPathsSanity() + +describe('memory', () => { + bench( + 'mem error-paths redirect (react)', + () => + runSequentialRequestLoop(handler, { + seed: redirectSeed, + iterations: errorPathsIterations, + buildRequest: buildRedirectRequest, + validateResponse: validateRedirectResponse, + }), + memoryBenchOptions, + ) + + bench( + 'mem error-paths not-found (react)', + () => + runSequentialRequestLoop(handler, { + seed: notFoundSeed, + iterations: errorPathsIterations, + buildRequest: buildNotFoundRequest, + validateResponse: validateNotFoundResponse, + validateBody: validateNotFoundBody, + }), + memoryBenchOptions, + ) + + bench( + 'mem error-paths error (react)', + () => + runSequentialRequestLoop(handler, { + seed: errorSeed, + iterations: errorPathsIterations, + buildRequest: buildErrorRequest, + validateResponse: validateErrorResponse, + validateBody: validateErrorBody, + }), + memoryBenchOptions, + ) + + bench( + 'mem error-paths unmatched (react)', + () => + runSequentialRequestLoop(handler, { + seed: unmatchedSeed, + iterations: errorPathsIterations, + buildRequest: buildUnmatchedRequest, + validateResponse: validateNotFoundResponse, + }), + memoryBenchOptions, + ) +}) 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..710048160f --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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/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..4d78884cea --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.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/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..c1809ffd98 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts @@ -0,0 +1,76 @@ +import { bench, describe } from 'vitest' +import { + memoryBenchOptions, + runSequentialRequestLoop, +} from '../../../bench-utils' +import type { StartRequestHandler } from '../../../bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const benchmarkSeed = 0x5eed_0005 +const peakLargePageIterations = 2 +const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8' +const levelEightMarker = 'data-bench="peak-large-page-level-8"' +const knownDehydratedRecordName = 'peak-large-page-l8-record-199' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +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') + } + + if (!body.includes(knownDehydratedRecordName)) { + throw new Error( + 'Expected peak-large-page dehydrated record 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) +} + +await assertPeakLargePageSanity(handler) + +describe('memory', () => { + bench( + 'mem peak-large-page (react)', + () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: peakLargePageIterations, + buildRequest: buildPeakLargePageRequest, + validateResponse: validatePeakLargePageResponse, + validateBody: validatePeakLargePageBody, + }), + memoryBenchOptions, + ) +}) 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..89a5865a74 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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/src/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/src/large-page-data.ts new file mode 100644 index 0000000000..a0cd3b4975 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/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/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..036a2a1911 --- /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,25 @@ +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..0aceecf51b --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx @@ -0,0 +1,26 @@ +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..1bc6588434 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx @@ -0,0 +1,26 @@ +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..bbece2972c --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx @@ -0,0 +1,26 @@ +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..62822e75ea --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.l4.tsx @@ -0,0 +1,26 @@ +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..4a74984f35 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx @@ -0,0 +1,26 @@ +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..a283fe04cb --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.l2.tsx @@ -0,0 +1,26 @@ +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..469a129b37 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/src/routes/l1.tsx @@ -0,0 +1,26 @@ +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..4d78884cea --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.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/request-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts new file mode 100644 index 0000000000..ecfa5bb266 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts @@ -0,0 +1,83 @@ +import { bench, describe } from 'vitest' +import { + memoryBenchOptions, + randomSegment, + runSequentialRequestLoop, +} from '../../../bench-utils' +import type { StartRequestHandler } from '../../../bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const benchmarkSeed = 0xdecafbad +const requestChurnIterations = 200 +const itemPageMarker = 'data-bench="request-churn-item"' +const dehydrationMarker = '$_TSR' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +function buildItemRequest(random: () => number) { + const id = randomSegment(random) + const q = `q-${randomSegment(random)}` + + return new Request(`http://localhost/items/${id}?q=${q}`, 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) + + if (!body.includes(dehydrationMarker)) { + throw new Error( + 'Expected sanity response to include the dehydration marker', + ) + } +} + +await assertRequestChurnSanity(handler) + +describe('memory', () => { + bench( + 'mem request-churn (react)', + () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: requestChurnIterations, + buildRequest: buildItemRequest, + validateResponse: validateItemResponse, + validateBody: validateItemBody, + }), + memoryBenchOptions, + ) +}) 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..03dce4042a --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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/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..4d78884cea --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.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/serialization-payload/react/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts new file mode 100644 index 0000000000..50952fef6c --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts @@ -0,0 +1,105 @@ +import { bench, describe } from 'vitest' +import { + memoryBenchOptions, + randomSegment, + runSequentialRequestLoop, +} from '../../../bench-utils' +import type { StartRequestHandler } from '../../../bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const benchmarkSeed = 0x51eaa11 +const serializationPayloadIterations = 5 +const payloadPageMarker = 'data-bench="serialization-payload"' +const dehydrationMarker = '$_TSR' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +function buildPayloadRequest(random: () => number, index: number) { + const id = `payload-${index}-${randomSegment(random)}` + + return new Request(`http://localhost/data/${id}`, requestInit) +} + +function knownMapKey(id: string) { + return `map-${id}-000` +} + +function getRequestId(request: Request) { + const url = new URL(request.url) + const match = /^\/data\/([^/]+)$/.exec(url.pathname) + const id = match?.[1] + + if (id === undefined) { + throw new Error(`Expected /data/$id request URL, got ${request.url}`) + } + + return decodeURIComponent(id) +} + +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, + _response: Response, + request: Request, +) { + if (!body.includes(payloadPageMarker)) { + throw new Error('Expected serialization-payload marker in response body') + } + + if (!body.includes(dehydrationMarker)) { + throw new Error('Expected serialization-payload dehydration script in body') + } + + const mapKey = knownMapKey(getRequestId(request)) + + if (!body.includes(mapKey)) { + throw new Error(`Expected dehydrated payload to include Map key ${mapKey}`) + } +} + +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, response, request) +} + +await assertSerializationPayloadSanity(handler) + +describe('memory', () => { + bench( + 'mem serialization-payload (react)', + () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: serializationPayloadIterations, + buildRequest: buildPayloadRequest, + validateResponse: validatePayloadResponse, + validateBody: validatePayloadBody, + }), + memoryBenchOptions, + ) +}) 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..5cc35a789f --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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/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..a131991bc6 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/src/routes/data.$id.tsx @@ -0,0 +1,130 @@ +import { createFileRoute } from '@tanstack/react-router' + +const mapEntryCount = 500 +const setEntryCount = 500 +const temporalEntryCount = 500 +const nestedTreeDepth = 5 +const nestedTreeBreadth = 6 +const payloadTextLength = 150 + +interface MapPayloadValue { + index: number + label: string + createdAt: Date + count: bigint + text: string +} + +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 const Route = createFileRoute('/data/$id')({ + loader: ({ params }) => makeSerializationPayload(params.id), + component: DataComponent, +}) + +function DataComponent() { + const data = Route.useLoaderData() + + return ( +
Map size: {data.lookup.size}
+ ) +} + +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/react/tsconfig.json b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json new file mode 100644 index 0000000000..4d78884cea --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.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/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..1bb5899103 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts @@ -0,0 +1,261 @@ +import { bench, describe } from 'vitest' +import { + createDeterministicRandom, + memoryBenchOptions, + randomSegment, + runSequentialRequestLoop, +} from '../../../bench-utils' +import type { StartRequestHandler } from '../../../bench-utils' + +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 appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +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 expectedIdsByRequest = new WeakMap() + +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') + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +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}`, + ) + } + + if (!body.includes(`${expectedId}-4`)) { + throw new Error(`Expected final payload record for ${expectedId}`) + } +} + +function validateServerFnBody( + body: string, + _response: Response, + request: Request, +) { + const expectedId = expectedIdsByRequest.get(request) + + if (expectedId) { + validateEchoedBody(body, request, expectedId) + } +} + +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) +} + +const urls = await discoverUrls(handler) + +await assertServerFnChurnSanity(handler, urls) + +describe('memory', () => { + bench( + 'mem server-fn-churn (react)', + () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: serverFnChurnIterations, + buildRequest: (_random, index) => { + const fixtureIndex = Math.floor(index / 2) % fixtureCount + + if (index % 2 === 0) { + const fixture = getFixtures[fixtureIndex]! + const request = buildGetRequest(urls.get, fixture) + + if (index === 0) { + expectedIdsByRequest.set(request, fixture.id) + } + + return request + } + + const fixture = postFixtures[fixtureIndex]! + const request = buildPostRequest(urls.post, fixture) + + if (index === 1) { + expectedIdsByRequest.set(request, fixture.id) + } + + return request + }, + validateResponse: validateServerFnResponse, + validateBody: validateServerFnBody, + }), + memoryBenchOptions, + ) +}) 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..926a50b9f1 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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/src/fns.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts new file mode 100644 index 0000000000..2ea8643da1 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts @@ -0,0 +1,47 @@ +import { createMiddleware, createServerFn } from '@tanstack/react-start' + +type ServerFnInput = { + id: string +} + +const recordIndexes = Array.from({ length: 5 }, (_, index) => index) + +const contextMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => + next({ + context: { + ctx: 'ctx-server-fn-churn', + }, + }), +) + +function validateInput(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 } +} + +function echoPayload(data: ServerFnInput, context: { ctx: string }) { + return { + id: data.id, + ctx: context.ctx, + payload: recordIndexes.map((index) => ({ + id: `${data.id}-${index}`, + label: `record-${index}`, + })), + } +} + +export const churnGet = createServerFn({ method: 'GET' }) + .middleware([contextMiddleware]) + .validator(validateInput) + .handler(({ data, context }) => echoPayload(data, context)) + +export const churnPost = createServerFn({ method: 'POST' }) + .middleware([contextMiddleware]) + .validator(validateInput) + .handler(({ data, context }) => echoPayload(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..4d78884cea --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.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/streaming-peak/react/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts new file mode 100644 index 0000000000..8b706c48b0 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts @@ -0,0 +1,217 @@ +import { bench, describe } from 'vitest' +import { + createDeterministicRandom, + memoryBenchOptions, + randomSegment, +} from '../../../bench-utils' +import type { StartRequestHandler } from '../../../bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const benchmarkSeed = 0xdecafbad +const streamingPeakIterations = 3 +const fallbackMarkers = [ + 'streaming-peak-fallback-0', + 'streaming-peak-fallback-1', + 'streaming-peak-fallback-2', + 'streaming-peak-fallback-3', +] as const +const deferredSectionMarkers = [ + 'streaming-peak-deferred-0', + 'streaming-peak-deferred-1', + 'streaming-peak-deferred-2', + 'streaming-peak-deferred-3', +] as const + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler +} + +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 consumeResponseChunks(response: Response) { + const reader = getResponseReader(response) + let chunkCount = 0 + + while (true) { + const result = await reader.read() + + if (result.done) { + break + } + + chunkCount++ + } + + return chunkCount +} + +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 } +} + +function assertFallbacksPrecedeDeferredContent(body: string) { + for (let index = 0; index < fallbackMarkers.length; index++) { + const fallbackIndex = body.indexOf(fallbackMarkers[index]!) + const deferredIndex = body.indexOf(deferredSectionMarkers[index]!) + + if (fallbackIndex === -1) { + throw new Error( + `Expected fallback marker ${fallbackMarkers[index]} in body`, + ) + } + + if (deferredIndex === -1) { + throw new Error( + `Expected deferred section marker ${deferredSectionMarkers[index]} in body`, + ) + } + + if (fallbackIndex > deferredIndex) { + throw new Error( + `Expected ${fallbackMarkers[index]} to precede ${deferredSectionMarkers[index]}`, + ) + } + } +} + +function assertBufferedBodyContainsDeferredSections(body: string) { + for (const marker of deferredSectionMarkers) { + if (!body.includes(marker)) { + throw new Error(`Expected buffered body to contain ${marker}`) + } + } +} + +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}`, + ) + } + + assertFallbacksPrecedeDeferredContent(chunked.body) + + const bufferedRequest = new Request( + 'http://localhost/stream/sanity-buffered', + requestInit, + ) + const bufferedResponse = await handler.fetch(bufferedRequest) + + validateStreamingResponse(bufferedResponse, bufferedRequest) + + const bufferedBody = await bufferedResponse.text() + + assertBufferedBodyContainsDeferredSections(bufferedBody) +} + +async function runChunkedStreamingLoop(handler: StartRequestHandler) { + const random = createDeterministicRandom(benchmarkSeed) + let totalChunkCount = 0 + + for (let index = 0; index < streamingPeakIterations; index++) { + const request = buildStreamingRequest(random, index) + const response = await handler.fetch(request) + + validateStreamingResponse(response, request) + totalChunkCount += await consumeResponseChunks(response) + } + + return totalChunkCount +} + +async function runBufferedStreamingLoop(handler: StartRequestHandler) { + const random = createDeterministicRandom(benchmarkSeed) + let totalBodyLength = 0 + + for (let index = 0; index < streamingPeakIterations; index++) { + const request = buildStreamingRequest(random, index) + const response = await handler.fetch(request) + + validateStreamingResponse(response, request) + + const body = await response.text() + totalBodyLength += body.length + } + + return totalBodyLength +} + +await assertStreamingPeakSanity(handler) + +describe('memory', () => { + bench( + 'mem streaming-peak chunked (react)', + async () => { + await runChunkedStreamingLoop(handler) + }, + memoryBenchOptions, + ) + + bench( + 'mem streaming-peak buffered (react)', + async () => { + await runBufferedStreamingLoop(handler) + }, + memoryBenchOptions, + ) +}) 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..d6fb362055 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "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/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..dc93600b47 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx @@ -0,0 +1,107 @@ +import { Await, createFileRoute } from '@tanstack/react-router' +import { Suspense } from 'react' + +const deferredRecordCount = 250 +const recordValueLength = 128 +const fallbackFlushDelayMs = 1 + +interface DeferredRecord { + id: string + value: string +} + +export interface DeferredSectionPayload { + index: number + records: Array +} + +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 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) +} + +function makeDeferredSection(id: string, sectionIndex: number) { + return afterFallbackFlush(sectionIndex).then(() => ({ + index: sectionIndex, + records: Array.from({ length: deferredRecordCount }, (_, recordIndex) => ({ + id: `${id}-${sectionIndex}-${recordIndex}`, + value: makeRecordValue(id, sectionIndex, recordIndex), + })), + })) +} + +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..4d78884cea --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": [ + "memory.bench.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/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..356ab7b3a8 --- /dev/null +++ b/benchmarks/memory/server/vitest.react.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + watch: false, + projects: ['./scenarios/*/react/vite.config.ts'], + }, +}) diff --git a/packages/react-router/src/ssr/renderRouterToStream.tsx b/packages/react-router/src/ssr/renderRouterToStream.tsx index d42a22396a..a5614b5e50 100644 --- a/packages/react-router/src/ssr/renderRouterToStream.tsx +++ b/packages/react-router/src/ssr/renderRouterToStream.tsx @@ -46,11 +46,21 @@ export const renderRouterToStream = async ({ responseHeaders: Headers children: ReactNode }) => { + // A client disconnecting mid-stream is normal operation, not a render + // failure; don't let React's onError log it as one. + const isAbortError = (error: unknown) => + (request.signal.aborted && error === request.signal.reason) || + (error instanceof Error && error.name === 'AbortError') + if (typeof ReactDOMServer.renderToReadableStream === 'function') { const stream = await ReactDOMServer.renderToReadableStream(children, { signal: request.signal, nonce: router.options.ssr?.nonce, progressiveChunkSize: Number.POSITIVE_INFINITY, + onError: (error, info) => { + if (isAbortError(error)) return + console.error('Error in renderToReadableStream:', error, info) + }, }) if (isbot(request.headers.get('User-Agent'))) { @@ -151,7 +161,9 @@ export const renderRouterToStream = async ({ }, }), onError: (error, info) => { - console.error('Error in renderToPipeableStream:', error, info) + if (!isAbortError(error)) { + console.error('Error in renderToPipeableStream:', error, info) + } abortPipeable(error, { defaultError: true }) }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efb2ac9b5f..b30d7b42a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -313,6 +313,74 @@ importers: 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)) + benchmarks/memory/client: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/router-core': + specifier: workspace:* + version: link:../../../packages/router-core + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + 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) + '@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)) + 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) + 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)) + + benchmarks/memory/server: + dependencies: + '@tanstack/react-router': + specifier: workspace:* + version: link:../../../packages/react-router + '@tanstack/react-start': + specifier: workspace:* + version: link:../../../packages/react-start + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + 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) + '@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)) + 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) + 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)) + benchmarks/ssr: dependencies: '@tanstack/react-router': @@ -2024,7 +2092,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 @@ -31449,9 +31517,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 +31710,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 +31725,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 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/*' From 3d31b79315a27fd0ebe2c62999290cd7ab56e752 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 00:37:45 +0200 Subject: [PATCH 02/24] fix codspeed github action --- .github/workflows/client-nav-benchmarks.yml | 31 +++++++++++++ .github/workflows/memory-benchmarks.yml | 49 --------------------- 2 files changed, 31 insertions(+), 49 deletions(-) delete mode 100644 .github/workflows/memory-benchmarks.yml diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml index cd47ca3ba1..0e9de2eb4b 100644 --- a/.github/workflows/client-nav-benchmarks.yml +++ b/.github/workflows/client-nav-benchmarks.yml @@ -51,3 +51,34 @@ jobs: with: mode: simulation run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }} + + # TODO: once we have solid and vue memory benchmarks, we can merge the 2 jobs into one, and + # simply add `memory-server` and `memory-client` to the `benchmark` matrix above. For now, we only run memory benchmarks for react. + # PS: that unification is only possible if we can link "benchmark name" with "mode: simulation|memory" + memory-benchmarks: + name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark + strategy: + fail-fast: false + matrix: + benchmark: + - memory-server + - memory-client + framework: + - react + # - solid + # - vue + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Setup Tools + uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main + + - name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark + uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0 + with: + mode: memory + run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }} diff --git a/.github/workflows/memory-benchmarks.yml b/.github/workflows/memory-benchmarks.yml deleted file mode 100644 index a7c23f8a1b..0000000000 --- a/.github/workflows/memory-benchmarks.yml +++ /dev/null @@ -1,49 +0,0 @@ -# Setup taken from https://codspeed.io/docs/benchmarks/nodejs/vitest -name: Memory Benchmarks - -on: - push: - branches: - - 'main' - paths: - - 'packages/**' - - 'benchmarks/**' - pull_request: - paths: - - 'packages/**' - - 'benchmarks/**' - workflow_dispatch: - -permissions: - contents: read # required for actions/checkout - id-token: write # required for OIDC authentication with CodSpeed - -env: - NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - SERVER_PRESET: 'node-server' - NX_NO_CLOUD: true - -jobs: - benchmarks: - name: Run ${{ matrix.benchmark }} CodSpeed benchmark - strategy: - fail-fast: false - matrix: - benchmark: - - memory-server - - memory-client - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Tools - uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - - name: Run ${{ matrix.benchmark }} CodSpeed benchmark - uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0 - with: - mode: memory - run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:react From 4c6d3aff8edf1a6a04c6ec0f771730fb938c03f4 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 00:44:56 +0200 Subject: [PATCH 03/24] unified codspeed job --- .github/workflows/client-nav-benchmarks.yml | 52 ++++++++------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml index 0e9de2eb4b..e38a59ed6b 100644 --- a/.github/workflows/client-nav-benchmarks.yml +++ b/.github/workflows/client-nav-benchmarks.yml @@ -36,6 +36,25 @@ jobs: benchmark: - client-nav - ssr + include: + - benchmark: client-nav + mode: simulation + - benchmark: ssr + mode: simulation + - benchmark: memory-server + mode: memory + - benchmark: memory-client + mode: memory + # TODO: temp exclude memory benchmarks for solid and vue until we have them working + exclude: + - benchmark: memory-server + framework: solid + - benchmark: memory-server + framework: vue + - benchmark: memory-client + framework: solid + - benchmark: memory-client + framework: vue runs-on: ubuntu-latest steps: - name: Checkout @@ -49,36 +68,5 @@ jobs: - 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 }} - - # TODO: once we have solid and vue memory benchmarks, we can merge the 2 jobs into one, and - # simply add `memory-server` and `memory-client` to the `benchmark` matrix above. For now, we only run memory benchmarks for react. - # PS: that unification is only possible if we can link "benchmark name" with "mode: simulation|memory" - memory-benchmarks: - name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark - strategy: - fail-fast: false - matrix: - benchmark: - - memory-server - - memory-client - framework: - - react - # - solid - # - vue - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Tools - uses: TanStack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3 # main - - - name: Run ${{ matrix.benchmark }}:${{ matrix.framework }} CodSpeed benchmark - uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0 - with: - mode: memory + mode: ${{ matrix.mode }} run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }} From 025d4b40090fe1475931cfaf3fa3d432df3bf86d Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 00:52:33 +0200 Subject: [PATCH 04/24] im dumb --- .github/workflows/client-nav-benchmarks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml index e38a59ed6b..553902f2ba 100644 --- a/.github/workflows/client-nav-benchmarks.yml +++ b/.github/workflows/client-nav-benchmarks.yml @@ -36,6 +36,8 @@ jobs: benchmark: - client-nav - ssr + - memory-server + - memory-client include: - benchmark: client-nav mode: simulation From 537776c1da69b7019ff80970d525582d84a75ced Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 09:30:41 +0200 Subject: [PATCH 05/24] cleaner run command in github action --- .github/workflows/client-nav-benchmarks.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml index 553902f2ba..5db0e1af9c 100644 --- a/.github/workflows/client-nav-benchmarks.yml +++ b/.github/workflows/client-nav-benchmarks.yml @@ -71,4 +71,7 @@ jobs: uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # v4.17.0 with: mode: ${{ matrix.mode }} - run: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }} + run: >- + WITH_INSTRUMENTATION=1 + pnpm nx run + @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }} From 97d1801af862fd9247b5c8f6c5d281e4b6d28767 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 10:28:05 +0200 Subject: [PATCH 06/24] better response stream draining --- benchmarks/memory/server/bench-utils.ts | 25 ++++++++++++++--- .../error-paths/react/memory.bench.ts | 2 -- .../peak-large-page/react/memory.bench.ts | 1 - .../request-churn/react/memory.bench.ts | 1 - .../react/memory.bench.ts | 1 - .../server-fn-churn/react/memory.bench.ts | 28 ++----------------- 6 files changed, 23 insertions(+), 35 deletions(-) diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts index 2613d81828..4614152cc2 100644 --- a/benchmarks/memory/server/bench-utils.ts +++ b/benchmarks/memory/server/bench-utils.ts @@ -7,7 +7,6 @@ export interface RunSequentialRequestLoopOptions { iterations?: number buildRequest: (random: () => number, index: number) => Request validateResponse?: (response: Response, request: Request) => void - validateBody?: (body: string, response: Response, request: Request) => void } export const memoryBenchOptions = { @@ -30,6 +29,26 @@ 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, { @@ -37,7 +56,6 @@ export async function runSequentialRequestLoop( iterations = 10, buildRequest, validateResponse, - validateBody, }: RunSequentialRequestLoopOptions, ) { const random = createDeterministicRandom(seed) @@ -57,7 +75,6 @@ export async function runSequentialRequestLoop( validate(response, request) - const body = await response.text() - validateBody?.(body, response, request) + await drainResponse(response) } } diff --git a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts index 32b536881b..bbbfd63456 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts @@ -169,7 +169,6 @@ describe('memory', () => { iterations: errorPathsIterations, buildRequest: buildNotFoundRequest, validateResponse: validateNotFoundResponse, - validateBody: validateNotFoundBody, }), memoryBenchOptions, ) @@ -182,7 +181,6 @@ describe('memory', () => { iterations: errorPathsIterations, buildRequest: buildErrorRequest, validateResponse: validateErrorResponse, - validateBody: validateErrorBody, }), memoryBenchOptions, ) 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 index c1809ffd98..cbe4449783 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts @@ -69,7 +69,6 @@ describe('memory', () => { iterations: peakLargePageIterations, buildRequest: buildPeakLargePageRequest, validateResponse: validatePeakLargePageResponse, - validateBody: validatePeakLargePageBody, }), memoryBenchOptions, ) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts index ecfa5bb266..bad7ed132e 100644 --- a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts @@ -76,7 +76,6 @@ describe('memory', () => { iterations: requestChurnIterations, buildRequest: buildItemRequest, validateResponse: validateItemResponse, - validateBody: validateItemBody, }), memoryBenchOptions, ) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts index 50952fef6c..95f409aa11 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts @@ -98,7 +98,6 @@ describe('memory', () => { iterations: serializationPayloadIterations, buildRequest: buildPayloadRequest, validateResponse: validatePayloadResponse, - validateBody: validatePayloadBody, }), memoryBenchOptions, ) 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 index 1bb5899103..4945ebdaf9 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts @@ -43,7 +43,6 @@ 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 expectedIdsByRequest = new WeakMap() const commonHeaders = { 'x-tsr-serverFn': 'true', @@ -186,18 +185,6 @@ function validateEchoedBody( } } -function validateServerFnBody( - body: string, - _response: Response, - request: Request, -) { - const expectedId = expectedIdsByRequest.get(request) - - if (expectedId) { - validateEchoedBody(body, request, expectedId) - } -} - async function assertServerFnChurnSanity( handler: StartRequestHandler, urls: FnUrls, @@ -235,26 +222,15 @@ describe('memory', () => { if (index % 2 === 0) { const fixture = getFixtures[fixtureIndex]! - const request = buildGetRequest(urls.get, fixture) - - if (index === 0) { - expectedIdsByRequest.set(request, fixture.id) - } - return request + return buildGetRequest(urls.get, fixture) } const fixture = postFixtures[fixtureIndex]! - const request = buildPostRequest(urls.post, fixture) - - if (index === 1) { - expectedIdsByRequest.set(request, fixture.id) - } - return request + return buildPostRequest(urls.post, fixture) }, validateResponse: validateServerFnResponse, - validateBody: validateServerFnBody, }), memoryBenchOptions, ) From b01f4dedd26781f88b32ca15dbc3058d775a90a7 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 11:47:42 +0200 Subject: [PATCH 07/24] cleanup streaming-peak scenario: we only care about server memory, not client --- .../streaming-peak/react/memory.bench.ts | 86 ++----------------- 1 file changed, 7 insertions(+), 79 deletions(-) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts index 8b706c48b0..cf0e2fa5ea 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts @@ -1,8 +1,8 @@ import { bench, describe } from 'vitest' import { - createDeterministicRandom, memoryBenchOptions, randomSegment, + runSequentialRequestLoop, } from '../../../bench-utils' import type { StartRequestHandler } from '../../../bench-utils' @@ -60,23 +60,6 @@ function getResponseReader(response: Response) { return reader } -async function consumeResponseChunks(response: Response) { - const reader = getResponseReader(response) - let chunkCount = 0 - - while (true) { - const result = await reader.read() - - if (result.done) { - break - } - - chunkCount++ - } - - return chunkCount -} - async function readStreamingBody(response: Response) { const reader = getResponseReader(response) const decoder = new TextDecoder() @@ -124,14 +107,6 @@ function assertFallbacksPrecedeDeferredContent(body: string) { } } -function assertBufferedBodyContainsDeferredSections(body: string) { - for (const marker of deferredSectionMarkers) { - if (!body.includes(marker)) { - throw new Error(`Expected buffered body to contain ${marker}`) - } - } -} - async function assertStreamingPeakSanity(handler: StartRequestHandler) { const chunkedRequest = new Request( 'http://localhost/stream/sanity-chunked', @@ -150,50 +125,6 @@ async function assertStreamingPeakSanity(handler: StartRequestHandler) { } assertFallbacksPrecedeDeferredContent(chunked.body) - - const bufferedRequest = new Request( - 'http://localhost/stream/sanity-buffered', - requestInit, - ) - const bufferedResponse = await handler.fetch(bufferedRequest) - - validateStreamingResponse(bufferedResponse, bufferedRequest) - - const bufferedBody = await bufferedResponse.text() - - assertBufferedBodyContainsDeferredSections(bufferedBody) -} - -async function runChunkedStreamingLoop(handler: StartRequestHandler) { - const random = createDeterministicRandom(benchmarkSeed) - let totalChunkCount = 0 - - for (let index = 0; index < streamingPeakIterations; index++) { - const request = buildStreamingRequest(random, index) - const response = await handler.fetch(request) - - validateStreamingResponse(response, request) - totalChunkCount += await consumeResponseChunks(response) - } - - return totalChunkCount -} - -async function runBufferedStreamingLoop(handler: StartRequestHandler) { - const random = createDeterministicRandom(benchmarkSeed) - let totalBodyLength = 0 - - for (let index = 0; index < streamingPeakIterations; index++) { - const request = buildStreamingRequest(random, index) - const response = await handler.fetch(request) - - validateStreamingResponse(response, request) - - const body = await response.text() - totalBodyLength += body.length - } - - return totalBodyLength } await assertStreamingPeakSanity(handler) @@ -202,15 +133,12 @@ describe('memory', () => { bench( 'mem streaming-peak chunked (react)', async () => { - await runChunkedStreamingLoop(handler) - }, - memoryBenchOptions, - ) - - bench( - 'mem streaming-peak buffered (react)', - async () => { - await runBufferedStreamingLoop(handler) + await runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: streamingPeakIterations, + buildRequest: buildStreamingRequest, + validateResponse: validateStreamingResponse, + }) }, memoryBenchOptions, ) From fa3cb8a87ab94370e10a6794bed887ae3783b05a Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 14:28:27 +0200 Subject: [PATCH 08/24] use platformatic/flame for local memory bench --- .gitignore | 3 + benchmarks/memory/README.md | 53 ++- benchmarks/memory/client/benchmark.ts | 7 + benchmarks/memory/client/flame-runner.ts | 18 + benchmarks/memory/client/jsdom.ts | 51 +++ benchmarks/memory/client/package.json | 54 +++ benchmarks/memory/client/runner.ts | 25 ++ .../react/memory.bench.ts | 48 +-- .../react/memory.flame.ts | 4 + .../react/project.json | 23 ++ .../interrupted-navigations/react/setup.ts | 23 ++ .../react/tsconfig.json | 1 + .../react/memory.bench.ts | 45 +-- .../react/memory.flame.ts | 4 + .../loader-data-retention/react/project.json | 23 ++ .../loader-data-retention/react/setup.ts | 21 + .../react/src/loader-data.ts | 2 +- .../loader-data-retention/react/tsconfig.json | 1 + .../mount-unmount/react/memory.bench.ts | 18 +- .../mount-unmount/react/memory.flame.ts | 4 + .../mount-unmount/react/project.json | 23 ++ .../scenarios/mount-unmount/react/setup.ts | 7 + .../mount-unmount/react/tsconfig.json | 1 + .../navigation-churn/react/memory.bench.ts | 31 +- .../navigation-churn/react/memory.flame.ts | 4 + .../navigation-churn/react/project.json | 23 ++ .../scenarios/navigation-churn/react/setup.ts | 7 + .../navigation-churn/react/tsconfig.json | 1 + .../preload-churn/react/memory.bench.ts | 52 +-- .../preload-churn/react/memory.flame.ts | 4 + .../preload-churn/react/project.json | 23 ++ .../scenarios/preload-churn/react/setup.ts | 27 ++ .../preload-churn/react/tsconfig.json | 1 + .../react/memory.bench.ts | 43 +-- .../react/memory.flame.ts | 4 + .../unique-location-churn/react/project.json | 23 ++ .../unique-location-churn/react/setup.ts | 18 + .../unique-location-churn/react/tsconfig.json | 1 + .../memory/client/vitest.react.config.ts | 1 + benchmarks/memory/flame-control.ts | 49 +++ benchmarks/memory/run-flame.mjs | 110 ++++++ benchmarks/memory/server/benchmark.ts | 10 + benchmarks/memory/server/flame-runner.ts | 11 + benchmarks/memory/server/package.json | 46 +++ benchmarks/memory/server/runner.ts | 9 + .../aborted-requests/react/memory.bench.ts | 162 +------- .../aborted-requests/react/memory.flame.ts | 4 + .../aborted-requests/react/project.json | 23 ++ .../scenarios/aborted-requests/react/setup.ts | 163 ++++++++ .../aborted-requests/react/tsconfig.json | 3 + .../error-paths/react/memory.bench.ts | 201 +--------- .../error-paths/react/memory.flame.ts | 4 + .../scenarios/error-paths/react/project.json | 23 ++ .../scenarios/error-paths/react/setup.ts | 212 ++++++++++ .../scenarios/error-paths/react/tsconfig.json | 3 + .../peak-large-page/react/memory.bench.ts | 77 +--- .../peak-large-page/react/memory.flame.ts | 4 + .../peak-large-page/react/project.json | 23 ++ .../scenarios/peak-large-page/react/setup.ts | 76 ++++ .../peak-large-page/react/tsconfig.json | 3 + .../request-churn/react/memory.bench.ts | 84 +--- .../request-churn/react/memory.flame.ts | 4 + .../request-churn/react/project.json | 23 ++ .../scenarios/request-churn/react/setup.ts | 85 ++++ .../request-churn/react/tsconfig.json | 3 + .../react/memory.bench.ts | 106 +---- .../react/memory.flame.ts | 4 + .../serialization-payload/react/project.json | 23 ++ .../serialization-payload/react/setup.ts | 107 ++++++ .../serialization-payload/react/tsconfig.json | 3 + .../server-fn-churn/react/memory.bench.ts | 239 +----------- .../server-fn-churn/react/memory.flame.ts | 4 + .../server-fn-churn/react/project.json | 23 ++ .../scenarios/server-fn-churn/react/setup.ts | 239 ++++++++++++ .../server-fn-churn/react/tsconfig.json | 3 + .../streaming-peak/react/memory.bench.ts | 147 +------ .../streaming-peak/react/memory.flame.ts | 4 + .../streaming-peak/react/project.json | 23 ++ .../scenarios/streaming-peak/react/setup.ts | 147 +++++++ .../streaming-peak/react/tsconfig.json | 3 + .../memory/server/vitest.react.config.ts | 1 + package.json | 2 + pnpm-lock.yaml | 362 +++++++++++++++--- 83 files changed, 2335 insertions(+), 1242 deletions(-) create mode 100644 benchmarks/memory/client/benchmark.ts create mode 100644 benchmarks/memory/client/flame-runner.ts create mode 100644 benchmarks/memory/client/jsdom.ts create mode 100644 benchmarks/memory/client/runner.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts create mode 100644 benchmarks/memory/flame-control.ts create mode 100644 benchmarks/memory/run-flame.mjs create mode 100644 benchmarks/memory/server/benchmark.ts create mode 100644 benchmarks/memory/server/flame-runner.ts create mode 100644 benchmarks/memory/server/runner.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/react/setup.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/react/setup.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts 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 index c03f112329..d6c007aa43 100644 --- a/benchmarks/memory/README.md +++ b/benchmarks/memory/README.md @@ -18,14 +18,16 @@ be added later without renames. ```text benchmarks/memory// - package.json Nx targets: build:react, test:perf:react, test:types + package.json Nx targets: build:react, test:perf:react, test:flame:react, test:types bench-utils.ts memoryBenchOptions, seeded LCG (+ sequential request loop on the server side) vitest.react.config.ts aggregates scenarios/*/react/vite.config.ts - scenarios//react/ one isolated app per scenario + memory.bench.ts + scenarios//react/ 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` owns the scenario workload, sanity checks, and any deterministic id +generation; `memory.bench.ts` and `memory.flame.ts` are thin runners only. ## How the memory instrument executes a bench @@ -105,6 +107,9 @@ continuity). Never grow an existing scenario for a new case — add a scenario. ## 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-client:test:perf:react --outputStyle=stream --skipRemoteCache @@ -112,6 +117,50 @@ pnpm nx run @benchmarks/memory-server:test:types --outputStyle=stream --skipRemo pnpm nx run @benchmarks/memory-client:test:types --outputStyle=stream --skipRemoteCache ``` +Local attribution profiling, without CodSpeed CLI/login/sudo/upload, uses +`@platformatic/flame` heap profiles. 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 +``` + +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. Reports can include `@platformatic/flame` and pprof shutdown +frames. 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): diff --git a/benchmarks/memory/client/benchmark.ts b/benchmarks/memory/client/benchmark.ts new file mode 100644 index 0000000000..bcf043c953 --- /dev/null +++ b/benchmarks/memory/client/benchmark.ts @@ -0,0 +1,7 @@ +export interface ClientMemoryBenchmark { + 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..504e4cda7d --- /dev/null +++ b/benchmarks/memory/client/flame-runner.ts @@ -0,0 +1,18 @@ +import { profileFlameWorkload } from '../flame-control.ts' +import { window } from './jsdom.ts' +import type { ClientMemoryBenchmark } from './benchmark.ts' + +export async function runClientFlameBenchmark( + setup: () => ClientMemoryBenchmark, +) { + const test = setup() + + try { + await setup().sanity() + await test.before?.() + await profileFlameWorkload(test.run) + } finally { + await test.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/package.json b/benchmarks/memory/client/package.json index 1bb60b0c1c..3602143c57 100644 --- a/benchmarks/memory/client/package.json +++ b/benchmarks/memory/client/package.json @@ -2,6 +2,23 @@ "name": "@benchmarks/memory-client", "private": true, "type": "module", + "scripts": { + "clean:profiles": "rm -rf scenarios/*/*/.profiles" + }, + "imports": { + "#memory-client/bench-utils": { + "types": "./bench-utils.ts", + "default": "./bench-utils.ts" + }, + "#memory-client/flame-runner": { + "types": "./flame-runner.ts", + "default": "./flame-runner.ts" + }, + "#memory-client/runner": { + "types": "./runner.ts", + "default": "./runner.ts" + } + }, "dependencies": { "@tanstack/react-router": "workspace:^", "@tanstack/router-core": "workspace:^", @@ -10,9 +27,11 @@ }, "devDependencies": { "@codspeed/vitest-plugin": "^5.5.0", + "@platformatic/flame": "^1.6.0", "@testing-library/react": "^16.2.0", "@vitejs/plugin-react": "^6.0.1", "@types/jsdom": "28.0.0", + "jsdom": "29.1.1", "typescript": "^6.0.2", "vite": "^8.0.14", "vitest": "^4.1.4" @@ -36,8 +55,43 @@ } ] }, + "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" + } + ] + }, + "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:perf:react": { "executor": "nx:run-commands", + "parallelism": false, "cache": false, "dependsOn": [ "build:react" diff --git a/benchmarks/memory/client/runner.ts b/benchmarks/memory/client/runner.ts new file mode 100644 index 0000000000..a3f29d732a --- /dev/null +++ b/benchmarks/memory/client/runner.ts @@ -0,0 +1,25 @@ +import { afterAll, beforeAll, bench } from 'vitest' +import { memoryBenchOptions } from './bench-utils' +import type { ClientMemoryBenchmark } from './benchmark' + +export function registerClientMemoryBench(test: ClientMemoryBenchmark) { + if (test.before && test.after) { + /** + * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only + * honors tinybench's setup/teardown options; the CodSpeed runner does the + * exact opposite. Both registrations are load-bearing: exactly one pair + * runs in any given mode. + */ + beforeAll(test.before) + afterAll(test.after) + + bench(test.name, test.run, { + ...memoryBenchOptions, + setup: test.before, + teardown: test.after, + }) + return + } + + bench(test.name, test.run, memoryBenchOptions) +} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts index bc4c8ca6cd..5b1c779d71 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts @@ -1,51 +1,9 @@ -import { afterAll, beforeAll, bench, describe } from 'vitest' -import { - createDeterministicRandom, - memoryBenchOptions, - randomSegment, -} from '../../../bench-utils' +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' import { setup } from './setup' -const interruptedNavigationIterations = 150 - -const interruptedNavigationPairs = createInterruptedNavigationPairs( - interruptedNavigationIterations, -) - -function createInterruptedNavigationPairs(iterations: number) { - const random = createDeterministicRandom(13) - - return Array.from({ length: iterations }, (_, index) => ({ - slowId: `slow-${index}-${randomSegment(random)}`, - fastId: `fast-${index}-${randomSegment(random)}`, - })) -} - await setup().sanity() describe('memory', () => { - const test = setup() - - /** - * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only - * honors tinybench's setup/teardown options; the CodSpeed runner does the - * exact opposite. Both registrations are load-bearing — exactly one pair - * runs in any given mode. - */ - beforeAll(test.before) - afterAll(test.after) - - bench( - 'mem interrupted-navigations (react)', - async () => { - for (const pair of interruptedNavigationPairs) { - await test.interrupt(pair.slowId, pair.fastId) - } - }, - { - ...memoryBenchOptions, - setup: test.before, - teardown: test.after, - }, - ) + registerClientMemoryBench(setup()) }) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json index 77ccf8d85a..0ed437c9ca 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/project.json @@ -15,6 +15,29 @@ "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": [ diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts index 4cc8182b05..320d4cfb0b 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts @@ -1,4 +1,8 @@ import type * as App from './src/app' +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' type NavigationSettlement = | { @@ -17,6 +21,10 @@ const { resolveSlowLoader, slowLoaderRegistry, } = (await import(/* @vite-ignore */ appModulePath)) as typeof App +const interruptedNavigationIterations = 150 +const interruptedNavigationPairs = createInterruptedNavigationPairs( + interruptedNavigationIterations, +) const uninitialized = () => Promise.reject( @@ -29,6 +37,15 @@ const uninitializedSettlement = () => 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)}`, + })) +} + async function drainMicrotasks() { await Promise.resolve() await Promise.resolve() @@ -245,8 +262,14 @@ export function setup() { } return { + name: 'mem interrupted-navigations (react)', before, interrupt, + async run() { + for (const pair of interruptedNavigationPairs) { + await interrupt(pair.slowId, pair.fastId) + } + }, async sanity() { await before() diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json index 26767bd491..ea566061d9 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/tsconfig.json @@ -10,6 +10,7 @@ "src/**/*.ts", "src/**/*.tsx", "memory.bench.ts", + "memory.flame.ts", "setup.ts", "vite.config.ts", "../../../bench-utils.ts", 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 index 331c74fb2a..5b1c779d71 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts @@ -1,48 +1,9 @@ -import { afterAll, beforeAll, bench, describe } from 'vitest' -import { - createDeterministicRandom, - memoryBenchOptions, - randomSegment, -} from '../../../bench-utils' +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' import { setup } from './setup' -const loaderDataRetentionNavigationCount = 20 -const pageIds = createPageIds() - await setup().sanity() describe('memory', () => { - const test = setup() - - /** - * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only - * honors tinybench's setup/teardown options; the CodSpeed runner does the - * exact opposite. Both registrations are load-bearing — exactly one pair - * runs in any given mode. - */ - beforeAll(test.before) - afterAll(test.after) - - bench( - 'mem loader-data-retention (react)', - async () => { - for (const id of pageIds) { - await test.navigate(id) - } - }, - { - ...memoryBenchOptions, - setup: test.before, - teardown: test.after, - }, - ) + registerClientMemoryBench(setup()) }) - -function createPageIds() { - const random = createDeterministicRandom(11) - - return Array.from( - { length: loaderDataRetentionNavigationCount }, - (_, index) => `${index}-${randomSegment(random)}`, - ) -} 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json index 953558d3f6..713038e20f 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/project.json @@ -15,6 +15,29 @@ "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": [ diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts index 3459edec5a..934f37c1bf 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts @@ -1,15 +1,30 @@ import type * as App from './src/app' +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' const appModulePath = './dist/app.js' const { loaderPayloadRecordCount, mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App +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 setup() { if (process.env.NODE_ENV !== 'production') { console.warn( @@ -129,8 +144,14 @@ export function setup() { } return { + name: 'mem loader-data-retention (react)', before, navigate: (id: string) => navigateTo(id), + async run() { + for (const id of pageIds) { + await navigateTo(id) + } + }, async sanity() { await before() diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts index d2a73a6542..3bd59c82ef 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts @@ -1,7 +1,7 @@ import { createDeterministicRandom, randomSegment, -} from '../../../../bench-utils' +} from '#memory-client/bench-utils' export const loaderPayloadRecordCount = 512 diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json index 26767bd491..ea566061d9 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/tsconfig.json @@ -10,6 +10,7 @@ "src/**/*.ts", "src/**/*.tsx", "memory.bench.ts", + "memory.flame.ts", "setup.ts", "vite.config.ts", "../../../bench-utils.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 index 042109dd19..5b1c779d71 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts @@ -1,21 +1,9 @@ -import { bench, describe } from 'vitest' -import { memoryBenchOptions } from '../../../bench-utils' +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' import { setup } from './setup' -const mountUnmountIterations = 100 - await setup().sanity() describe('memory', () => { - const test = setup() - - bench( - 'mem mount-unmount (react)', - async () => { - for (let index = 0; index < mountUnmountIterations; index++) { - await test.cycle() - } - }, - memoryBenchOptions, - ) + registerClientMemoryBench(setup()) }) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/project.json b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json index f701b92e2a..05d8aaa4bf 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/react/project.json +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/project.json @@ -15,6 +15,29 @@ "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": [ diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts index 3ba792e11a..203dc39d37 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts @@ -4,6 +4,7 @@ const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App +const mountUnmountIterations = 100 function drainMicrotasks() { return Promise.resolve().then(() => Promise.resolve()) @@ -55,7 +56,13 @@ export function setup() { } return { + name: 'mem mount-unmount (react)', cycle, + async run() { + for (let index = 0; index < mountUnmountIterations; index++) { + await cycle() + } + }, async sanity() { assertEmptyBody() await cycle() diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json index 26767bd491..ea566061d9 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/tsconfig.json @@ -10,6 +10,7 @@ "src/**/*.ts", "src/**/*.tsx", "memory.bench.ts", + "memory.flame.ts", "setup.ts", "vite.config.ts", "../../../bench-utils.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 index 7e2fe82027..5b1c779d71 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts @@ -1,34 +1,9 @@ -import { afterAll, beforeAll, bench, describe } from 'vitest' -import { memoryBenchOptions } from '../../../bench-utils' +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' import { setup } from './setup' -const navigationChurnIterations = 300 - await setup().sanity() describe('memory', () => { - const test = setup() - - /** - * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only - * honors tinybench's setup/teardown options; the CodSpeed runner does the - * exact opposite. Both registrations are load-bearing — exactly one pair - * runs in any given mode. - */ - beforeAll(test.before) - afterAll(test.after) - - bench( - 'mem navigation-churn (react)', - async () => { - for (let index = 0; index < navigationChurnIterations; index++) { - await test.navigate(index % 2 === 0 ? '/b' : '/a') - } - }, - { - ...memoryBenchOptions, - setup: test.before, - teardown: test.after, - }, - ) + registerClientMemoryBench(setup()) }) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/project.json b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json index 1409a41b61..b424277bc6 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/react/project.json +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/project.json @@ -15,6 +15,29 @@ "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": [ diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts index f31c63c360..deac1d1bbf 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts @@ -6,6 +6,7 @@ const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App +const navigationChurnIterations = 300 const uninitialized = () => Promise.reject(new Error('navigation-churn benchmark is not initialized')) @@ -99,8 +100,14 @@ export function setup() { } return { + name: 'mem navigation-churn (react)', 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() diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json index 26767bd491..ea566061d9 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/tsconfig.json @@ -10,6 +10,7 @@ "src/**/*.ts", "src/**/*.tsx", "memory.bench.ts", + "memory.flame.ts", "setup.ts", "vite.config.ts", "../../../bench-utils.ts", diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts index 499fb8461b..5b1c779d71 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts @@ -1,55 +1,9 @@ -import { afterAll, beforeAll, bench, describe } from 'vitest' -import { - createDeterministicRandom, - memoryBenchOptions, - randomSegment, -} from '../../../bench-utils' +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' import { setup } from './setup' -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 the CodSpeed runner's multiple -// invocations (warmup + measured) of the bench fn 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 - await setup().sanity() describe('memory', () => { - const test = setup() - - /** - * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only - * honors tinybench's setup/teardown options; the CodSpeed runner does the - * exact opposite. Both registrations are load-bearing — exactly one pair - * runs in any given mode. - */ - beforeAll(test.before) - afterAll(test.after) - - bench( - 'mem preload-churn (react)', - async () => { - for (let index = 0; index < preloadChurnIterations; index++) { - await test.preload( - `${(preloadCounter++).toString(36)}-${randomSegment(benchmarkRandom)}`, - ) - - if ((index + 1) % preloadsPerEvictionNavigation === 0) { - await test.evictPreloads() - } - } - }, - { - ...memoryBenchOptions, - setup: test.before, - teardown: test.after, - }, - ) + registerClientMemoryBench(setup()) }) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/project.json b/benchmarks/memory/client/scenarios/preload-churn/react/project.json index 33c837bc6c..238d98c816 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/react/project.json +++ b/benchmarks/memory/client/scenarios/preload-churn/react/project.json @@ -15,6 +15,29 @@ "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": [ diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts index 34c2e51558..a1737c644e 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts @@ -1,4 +1,8 @@ import type * as App from './src/app' +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' const appModulePath = './dist/app.js' const { getTrackedItemLoaderCount, mountTestApp } = (await import( @@ -10,6 +14,17 @@ type MountedApp = ReturnType // 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') @@ -184,9 +199,21 @@ export function setup() { } return { + name: 'mem preload-churn (react)', 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() diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json index 26767bd491..ea566061d9 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json +++ b/benchmarks/memory/client/scenarios/preload-churn/react/tsconfig.json @@ -10,6 +10,7 @@ "src/**/*.ts", "src/**/*.tsx", "memory.bench.ts", + "memory.flame.ts", "setup.ts", "vite.config.ts", "../../../bench-utils.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 index db56f30d8d..5b1c779d71 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts @@ -1,46 +1,9 @@ -import { afterAll, beforeAll, bench, describe } from 'vitest' -import { - createDeterministicRandom, - memoryBenchOptions, - randomSegment, -} from '../../../bench-utils' +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' import { setup } from './setup' -const benchmarkRandom = createDeterministicRandom(0xdecafbad) -const uniqueLocationChurnIterations = 300 -// Module-level so ids stay unique across the CodSpeed runner's multiple -// invocations (warmup + measured) of the bench fn on one mount; the counter -// prefix removes any residual LCG birthday-collision risk. -let locationCounter = 0 - await setup().sanity() describe('memory', () => { - const test = setup() - - /** - * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only - * honors tinybench's setup/teardown options; the CodSpeed runner does the - * exact opposite. Both registrations are load-bearing — exactly one pair - * runs in any given mode. - */ - beforeAll(test.before) - afterAll(test.after) - - bench( - 'mem unique-location-churn (react)', - async () => { - for (let index = 0; index < uniqueLocationChurnIterations; index++) { - const id = `${(locationCounter++).toString(36)}-${randomSegment(benchmarkRandom)}` - const q = `q-${randomSegment(benchmarkRandom)}` - - await test.navigate({ id, q }) - } - }, - { - ...memoryBenchOptions, - setup: test.before, - teardown: test.after, - }, - ) + registerClientMemoryBench(setup()) }) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json index db7c57b1a3..1572552650 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/project.json @@ -15,6 +15,29 @@ "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": [ diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts index 6f58a1281a..14fbfe7081 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts @@ -1,4 +1,8 @@ import type * as App from './src/app' +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' type ItemLocation = { id: string @@ -9,6 +13,11 @@ const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App +const benchmarkRandom = createDeterministicRandom(0xdecafbad) +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. +let locationCounter = 0 const uninitialized = () => Promise.reject( @@ -104,8 +113,17 @@ export function setup() { } return { + name: 'mem unique-location-churn (react)', 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() diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json index 26767bd491..ea566061d9 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/tsconfig.json @@ -10,6 +10,7 @@ "src/**/*.ts", "src/**/*.tsx", "memory.bench.ts", + "memory.flame.ts", "setup.ts", "vite.config.ts", "../../../bench-utils.ts", diff --git a/benchmarks/memory/client/vitest.react.config.ts b/benchmarks/memory/client/vitest.react.config.ts index 356ab7b3a8..44643b4d60 100644 --- a/benchmarks/memory/client/vitest.react.config.ts +++ b/benchmarks/memory/client/vitest.react.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { watch: false, + fileParallelism: false, projects: ['./scenarios/*/react/vite.config.ts'], }, }) diff --git a/benchmarks/memory/flame-control.ts b/benchmarks/memory/flame-control.ts new file mode 100644 index 0000000000..e3aa127033 --- /dev/null +++ b/benchmarks/memory/flame-control.ts @@ -0,0 +1,49 @@ +const flameEnabled = process.env.TSR_MEMORY_FLAME === '1' +const flameStartDelayMs = 250 + +function wait(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +function waitForSignalHandler() { + return wait(0) +} + +async function toggleFlameProfile() { + if (!flameEnabled) { + return + } + + if (process.platform === 'win32') { + throw new Error('Flame manual profiling is not supported on Windows') + } + + process.kill(process.pid, 'SIGUSR2') + await waitForSignalHandler() +} + +export async function startFlameProfile() { + if (flameEnabled) { + // Flame initializes sourcemap support asynchronously when the process starts. + await wait(flameStartDelayMs) + } + + await toggleFlameProfile() +} + +export async function stopFlameProfile() { + await toggleFlameProfile() +} + +export async function profileFlameWorkload( + workload: () => Promise | void, +) { + await startFlameProfile() + try { + await workload() + } finally { + await stopFlameProfile() + } +} diff --git a/benchmarks/memory/run-flame.mjs b/benchmarks/memory/run-flame.mjs new file mode 100644 index 0000000000..f999be5340 --- /dev/null +++ b/benchmarks/memory/run-flame.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node +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, startProfiling } = + entrypointRequire('@platformatic/flame') + +process.env.NODE_ENV = 'production' +process.env.TSR_MEMORY_FLAME = '1' + +console.log(`Flame profile directory: ${profileDir}`) + +const { pid, process: childProcess } = startProfiling(entrypointPath, [], { + autoStart: false, + cwd: profileDir, + mdFormat: 'detailed', + sourcemapDirs: [sourcemapDirPath], +}) + +console.log(`Flame profiling process: ${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/benchmark.ts b/benchmarks/memory/server/benchmark.ts new file mode 100644 index 0000000000..714b16baa4 --- /dev/null +++ b/benchmarks/memory/server/benchmark.ts @@ -0,0 +1,10 @@ +export interface ServerMemoryBench { + name: string + run: () => Promise | void +} + +export interface ServerMemoryBenchmark { + sanity: () => Promise | void + run: () => Promise | void + benches: Array +} diff --git a/benchmarks/memory/server/flame-runner.ts b/benchmarks/memory/server/flame-runner.ts new file mode 100644 index 0000000000..d2a3425b73 --- /dev/null +++ b/benchmarks/memory/server/flame-runner.ts @@ -0,0 +1,11 @@ +import { profileFlameWorkload } from '../flame-control.ts' +import type { ServerMemoryBenchmark } from './benchmark.ts' + +export async function runServerFlameBenchmark( + setup: () => Promise, +) { + const test = await setup() + + await test.sanity() + await profileFlameWorkload(test.run) +} diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json index 5e4273c30b..11ec470bd9 100644 --- a/benchmarks/memory/server/package.json +++ b/benchmarks/memory/server/package.json @@ -2,6 +2,14 @@ "name": "@benchmarks/memory-server", "private": true, "type": "module", + "scripts": { + "clean:profiles": "rm -rf scenarios/*/*/.profiles" + }, + "imports": { + "#memory-server/bench-utils": "./bench-utils.ts", + "#memory-server/flame-runner": "./flame-runner.ts", + "#memory-server/runner": "./runner.ts" + }, "dependencies": { "@tanstack/react-router": "workspace:^", "@tanstack/react-start": "workspace:^", @@ -10,6 +18,7 @@ }, "devDependencies": { "@codspeed/vitest-plugin": "^5.5.0", + "@platformatic/flame": "^1.6.0", "@vitejs/plugin-react": "^6.0.1", "typescript": "^6.0.2", "vite": "^8.0.14", @@ -35,8 +44,45 @@ } ] }, + "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" + } + ] + }, + "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:perf:react": { "executor": "nx:run-commands", + "parallelism": false, "cache": false, "dependsOn": [ "build:react" diff --git a/benchmarks/memory/server/runner.ts b/benchmarks/memory/server/runner.ts new file mode 100644 index 0000000000..783facbf3b --- /dev/null +++ b/benchmarks/memory/server/runner.ts @@ -0,0 +1,9 @@ +import { bench } from 'vitest' +import { memoryBenchOptions } from './bench-utils' +import type { ServerMemoryBenchmark } from './benchmark' + +export function registerServerMemoryBenches(test: ServerMemoryBenchmark) { + for (const memoryBench of test.benches) { + bench(memoryBench.name, memoryBench.run, memoryBenchOptions) + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts index b800cb974f..1dc2e0da86 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts @@ -1,160 +1,10 @@ -import { bench, describe } from 'vitest' -import { memoryBenchOptions } from '../../../bench-utils' -import type { StartRequestHandler } from '../../../bench-utils' +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' -const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -const abortedRequestIterations = 100 -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 documentRequestInit = { - method: 'GET', - headers: { - accept: 'text/html', - }, -} satisfies RequestInit - -const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl -)) as { - default: StartRequestHandler -} - -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 drainCancellationMicrotasks() { - await Promise.resolve() - await Promise.resolve() -} - -async function assertAbortedRequestsSanity(handler: StartRequestHandler) { - 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') - } - - for (const marker of [ - alphaFirstRecord(fullId), - alphaLastRecord(fullId), - betaFirstRecord(fullId), - betaLastRecord(fullId), - ]) { - if (!fullBody.includes(marker)) { - throw new Error(`Expected full sanity response to include ${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, value } = await readFirstChunk( - midStreamResponse, - midStreamRequest, - ) - const text = textDecoder.decode(value) - - if (!text.includes(eagerMarker)) { - throw new Error('Expected first sanity chunk to include the eager marker') - } - - if ( - !text.includes(alphaFallbackMarker) || - !text.includes(betaFallbackMarker) - ) { - throw new Error('Expected first sanity chunk to include deferred fallbacks') - } - - for (const marker of [ - alphaFirstRecord(midStreamId), - alphaLastRecord(midStreamId), - betaFirstRecord(midStreamId), - betaLastRecord(midStreamId), - ]) { - if (text.includes(marker)) { - throw new Error( - `First sanity chunk already included deferred content ${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 reader.cancel() - await drainCancellationMicrotasks() -} - -async function runAbortedRequestLoop(handler: StartRequestHandler) { - for (let index = 0; index < abortedRequestIterations; index++) { - const controller = new AbortController() - const request = buildStreamRequest(`abort-${index}`, controller.signal) - const response = await handler.fetch(request) - validateDocumentResponse(response, request) - - const { reader } = await readFirstChunk(response, request) - controller.abort() - await reader.cancel() - await drainCancellationMicrotasks() - } -} - -await assertAbortedRequestsSanity(handler) +const test = await setup() +await test.sanity() describe('memory', () => { - bench( - 'mem aborted-requests (react)', - () => runAbortedRequestLoop(handler), - memoryBenchOptions, - ) + registerServerMemoryBenches(test) }) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/project.json b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json index c962404c09..bea90c8766 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/react/project.json +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/project.json @@ -15,6 +15,29 @@ "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": [ 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..404b8410c2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts @@ -0,0 +1,163 @@ +import type { StartRequestHandler } from '#memory-server/bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const abortedRequestIterations = 100 +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 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 drainCancellationMicrotasks() { + await Promise.resolve() + await Promise.resolve() +} + +async function assertAbortedRequestsSanity(handler: StartRequestHandler) { + 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') + } + + for (const marker of [ + alphaFirstRecord(fullId), + alphaLastRecord(fullId), + betaFirstRecord(fullId), + betaLastRecord(fullId), + ]) { + if (!fullBody.includes(marker)) { + throw new Error(`Expected full sanity response to include ${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, value } = await readFirstChunk( + midStreamResponse, + midStreamRequest, + ) + const text = textDecoder.decode(value) + + if (!text.includes(eagerMarker)) { + throw new Error('Expected first sanity chunk to include the eager marker') + } + + if ( + !text.includes(alphaFallbackMarker) || + !text.includes(betaFallbackMarker) + ) { + throw new Error('Expected first sanity chunk to include deferred fallbacks') + } + + for (const marker of [ + alphaFirstRecord(midStreamId), + alphaLastRecord(midStreamId), + betaFirstRecord(midStreamId), + betaLastRecord(midStreamId), + ]) { + if (text.includes(marker)) { + throw new Error( + `First sanity chunk already included deferred content ${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 reader.cancel() + await drainCancellationMicrotasks() +} + +async function runAbortedRequestLoop(handler: StartRequestHandler) { + for (let index = 0; index < abortedRequestIterations; index++) { + const controller = new AbortController() + const request = buildStreamRequest(`abort-${index}`, controller.signal) + const response = await handler.fetch(request) + validateDocumentResponse(response, request) + + const { reader } = await readFirstChunk(response, request) + controller.abort() + await reader.cancel() + await drainCancellationMicrotasks() + } +} + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + const run = () => runAbortedRequestLoop(handler) + + return { + sanity: () => assertAbortedRequestsSanity(handler), + run, + benches: [ + { + name: 'mem aborted-requests (react)', + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json index 4d78884cea..11ddcce4ea 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/tsconfig.json @@ -1,12 +1,15 @@ { "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/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts index bbbfd63456..1dc2e0da86 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts @@ -1,199 +1,10 @@ -import { bench, describe } from 'vitest' -import { - memoryBenchOptions, - randomSegment, - runSequentialRequestLoop, -} from '../../../bench-utils' -import type { StartRequestHandler } from '../../../bench-utils' +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' -const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -const errorPathsIterations = 50 -const redirectSeed = 0xdecafbad -const notFoundSeed = 0xdecafb0d -const errorSeed = 0xdecafbed -const unmatchedSeed = 0xdecaf00d - -const redirectStatus = 302 -const notFoundStatus = 404 -const errorStatus = 500 -const notFoundMarker = 'data-bench="not-found-boundary"' -const errorMarker = 'data-bench="error-boundary"' - -const requestInit = { - method: 'GET', - headers: { - accept: 'text/html', - }, -} satisfies RequestInit - -const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl -)) as { - default: StartRequestHandler -} - -function buildRedirectRequest(random: () => number) { - return new Request( - `http://localhost/from/${randomSegment(random)}`, - requestInit, - ) -} - -function buildNotFoundRequest(random: () => number) { - return new Request( - `http://localhost/missing/${randomSegment(random)}`, - requestInit, - ) -} - -function buildErrorRequest(random: () => number) { - return new Request( - `http://localhost/boom/${randomSegment(random)}`, - requestInit, - ) -} - -function buildUnmatchedRequest(random: () => number) { - return new Request( - `http://localhost/nope/${randomSegment(random)}`, - 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}`, - ) - } -} - -function validateNotFoundBody(body: string) { - if (!body.includes(notFoundMarker)) { - throw new Error('Expected error-paths not-found marker in response body') - } -} - -function validateErrorBody(body: string) { - if (!body.includes(errorMarker)) { - throw new Error('Expected error-paths error marker in response body') - } -} - -async function assertStatusSanity( - request: Request, - validateResponse: (response: Response, request: Request) => void, - validateBody?: (body: string) => void, -) { - const response = await handler.fetch(request) - validateResponse(response, request) - - const body = await response.text() - validateBody?.(body) -} - -async function assertErrorPathsSanity() { - await assertStatusSanity( - new Request('http://localhost/from/sanity-redirect', requestInit), - validateRedirectResponse, - ) - await assertStatusSanity( - new Request('http://localhost/missing/sanity-missing', requestInit), - validateNotFoundResponse, - validateNotFoundBody, - ) - await assertStatusSanity( - new Request('http://localhost/boom/sanity-error', requestInit), - validateErrorResponse, - validateErrorBody, - ) - await assertStatusSanity( - new Request('http://localhost/nope/sanity-unmatched', requestInit), - validateNotFoundResponse, - ) -} - -await assertErrorPathsSanity() +const test = await setup() +await test.sanity() describe('memory', () => { - bench( - 'mem error-paths redirect (react)', - () => - runSequentialRequestLoop(handler, { - seed: redirectSeed, - iterations: errorPathsIterations, - buildRequest: buildRedirectRequest, - validateResponse: validateRedirectResponse, - }), - memoryBenchOptions, - ) - - bench( - 'mem error-paths not-found (react)', - () => - runSequentialRequestLoop(handler, { - seed: notFoundSeed, - iterations: errorPathsIterations, - buildRequest: buildNotFoundRequest, - validateResponse: validateNotFoundResponse, - }), - memoryBenchOptions, - ) - - bench( - 'mem error-paths error (react)', - () => - runSequentialRequestLoop(handler, { - seed: errorSeed, - iterations: errorPathsIterations, - buildRequest: buildErrorRequest, - validateResponse: validateErrorResponse, - }), - memoryBenchOptions, - ) - - bench( - 'mem error-paths unmatched (react)', - () => - runSequentialRequestLoop(handler, { - seed: unmatchedSeed, - iterations: errorPathsIterations, - buildRequest: buildUnmatchedRequest, - validateResponse: validateNotFoundResponse, - }), - memoryBenchOptions, - ) + registerServerMemoryBenches(test) }) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/project.json b/benchmarks/memory/server/scenarios/error-paths/react/project.json index 710048160f..6a399fe41e 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/project.json +++ b/benchmarks/memory/server/scenarios/error-paths/react/project.json @@ -15,6 +15,29 @@ "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": [ 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..d51bebd948 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts @@ -0,0 +1,212 @@ +import { + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const errorPathsIterations = 50 +const redirectSeed = 0xdecafbad +const notFoundSeed = 0xdecafb0d +const errorSeed = 0xdecafbed +const unmatchedSeed = 0xdecaf00d + +const redirectStatus = 302 +const notFoundStatus = 404 +const errorStatus = 500 +const notFoundMarker = 'data-bench="not-found-boundary"' +const errorMarker = 'data-bench="error-boundary"' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildRedirectRequest(random: () => number) { + return new Request( + `http://localhost/from/${randomSegment(random)}`, + requestInit, + ) +} + +function buildNotFoundRequest(random: () => number) { + return new Request( + `http://localhost/missing/${randomSegment(random)}`, + requestInit, + ) +} + +function buildErrorRequest(random: () => number) { + return new Request( + `http://localhost/boom/${randomSegment(random)}`, + requestInit, + ) +} + +function buildUnmatchedRequest(random: () => number) { + return new Request( + `http://localhost/nope/${randomSegment(random)}`, + 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}`, + ) + } +} + +function validateNotFoundBody(body: string) { + if (!body.includes(notFoundMarker)) { + throw new Error('Expected error-paths not-found marker in response body') + } +} + +function validateErrorBody(body: string) { + if (!body.includes(errorMarker)) { + throw new Error('Expected error-paths error marker in response body') + } +} + +async function assertStatusSanity( + handler: StartRequestHandler, + request: Request, + validateResponse: (response: Response, request: Request) => void, + validateBody?: (body: string) => void, +) { + const response = await handler.fetch(request) + validateResponse(response, request) + + const body = await response.text() + validateBody?.(body) +} + +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, + validateNotFoundBody, + ) + await assertStatusSanity( + handler, + new Request('http://localhost/boom/sanity-error', requestInit), + validateErrorResponse, + validateErrorBody, + ) + await assertStatusSanity( + handler, + new Request('http://localhost/nope/sanity-unmatched', requestInit), + validateNotFoundResponse, + ) +} + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + const runRedirect = () => + runSequentialRequestLoop(handler, { + seed: redirectSeed, + iterations: errorPathsIterations, + buildRequest: buildRedirectRequest, + validateResponse: validateRedirectResponse, + }) + + const runNotFound = () => + runSequentialRequestLoop(handler, { + seed: notFoundSeed, + iterations: errorPathsIterations, + buildRequest: buildNotFoundRequest, + validateResponse: validateNotFoundResponse, + }) + + const runError = () => + runSequentialRequestLoop(handler, { + seed: errorSeed, + iterations: errorPathsIterations, + buildRequest: buildErrorRequest, + validateResponse: validateErrorResponse, + }) + + const runUnmatched = () => + runSequentialRequestLoop(handler, { + seed: unmatchedSeed, + iterations: errorPathsIterations, + buildRequest: buildUnmatchedRequest, + validateResponse: validateNotFoundResponse, + }) + + return { + sanity: () => assertErrorPathsSanity(handler), + async run() { + await runRedirect() + await runNotFound() + await runError() + await runUnmatched() + }, + benches: [ + { + name: 'mem error-paths redirect (react)', + run: runRedirect, + }, + { + name: 'mem error-paths not-found (react)', + run: runNotFound, + }, + { + name: 'mem error-paths error (react)', + run: runError, + }, + { + name: 'mem error-paths unmatched (react)', + run: runUnmatched, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json index 4d78884cea..11ddcce4ea 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json +++ b/benchmarks/memory/server/scenarios/error-paths/react/tsconfig.json @@ -1,12 +1,15 @@ { "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/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts index cbe4449783..1dc2e0da86 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts @@ -1,75 +1,10 @@ -import { bench, describe } from 'vitest' -import { - memoryBenchOptions, - runSequentialRequestLoop, -} from '../../../bench-utils' -import type { StartRequestHandler } from '../../../bench-utils' +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' -const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -const benchmarkSeed = 0x5eed_0005 -const peakLargePageIterations = 2 -const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8' -const levelEightMarker = 'data-bench="peak-large-page-level-8"' -const knownDehydratedRecordName = 'peak-large-page-l8-record-199' - -const requestInit = { - method: 'GET', - headers: { - accept: 'text/html', - }, -} satisfies RequestInit - -const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl -)) as { - default: StartRequestHandler -} - -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') - } - - if (!body.includes(knownDehydratedRecordName)) { - throw new Error( - 'Expected peak-large-page dehydrated record 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) -} - -await assertPeakLargePageSanity(handler) +const test = await setup() +await test.sanity() describe('memory', () => { - bench( - 'mem peak-large-page (react)', - () => - runSequentialRequestLoop(handler, { - seed: benchmarkSeed, - iterations: peakLargePageIterations, - buildRequest: buildPeakLargePageRequest, - validateResponse: validatePeakLargePageResponse, - }), - memoryBenchOptions, - ) + registerServerMemoryBenches(test) }) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/project.json b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json index 89a5865a74..699a243a03 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/project.json +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/project.json @@ -15,6 +15,29 @@ "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": [ 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..e10d034aa2 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts @@ -0,0 +1,76 @@ +import { runSequentialRequestLoop } from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const benchmarkSeed = 0x5eed_0005 +const peakLargePageIterations = 2 +const peakLargePageUrl = 'http://localhost/l1/l2/l3/l4/l5/l6/l7/l8' +const levelEightMarker = 'data-bench="peak-large-page-level-8"' +const knownDehydratedRecordName = 'peak-large-page-l8-record-199' + +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') + } + + if (!body.includes(knownDehydratedRecordName)) { + throw new Error( + 'Expected peak-large-page dehydrated record 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 async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: peakLargePageIterations, + buildRequest: buildPeakLargePageRequest, + validateResponse: validatePeakLargePageResponse, + }) + + return { + sanity: () => assertPeakLargePageSanity(handler), + run, + benches: [ + { + name: 'mem peak-large-page (react)', + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json index 4d78884cea..11ddcce4ea 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/tsconfig.json @@ -1,12 +1,15 @@ { "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/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts index bad7ed132e..1dc2e0da86 100644 --- a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts @@ -1,82 +1,10 @@ -import { bench, describe } from 'vitest' -import { - memoryBenchOptions, - randomSegment, - runSequentialRequestLoop, -} from '../../../bench-utils' -import type { StartRequestHandler } from '../../../bench-utils' +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' -const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -const benchmarkSeed = 0xdecafbad -const requestChurnIterations = 200 -const itemPageMarker = 'data-bench="request-churn-item"' -const dehydrationMarker = '$_TSR' - -const requestInit = { - method: 'GET', - headers: { - accept: 'text/html', - }, -} satisfies RequestInit - -const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl -)) as { - default: StartRequestHandler -} - -function buildItemRequest(random: () => number) { - const id = randomSegment(random) - const q = `q-${randomSegment(random)}` - - return new Request(`http://localhost/items/${id}?q=${q}`, 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) - - if (!body.includes(dehydrationMarker)) { - throw new Error( - 'Expected sanity response to include the dehydration marker', - ) - } -} - -await assertRequestChurnSanity(handler) +const test = await setup() +await test.sanity() describe('memory', () => { - bench( - 'mem request-churn (react)', - () => - runSequentialRequestLoop(handler, { - seed: benchmarkSeed, - iterations: requestChurnIterations, - buildRequest: buildItemRequest, - validateResponse: validateItemResponse, - }), - memoryBenchOptions, - ) + registerServerMemoryBenches(test) }) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/project.json b/benchmarks/memory/server/scenarios/request-churn/react/project.json index 03dce4042a..d997054b95 100644 --- a/benchmarks/memory/server/scenarios/request-churn/react/project.json +++ b/benchmarks/memory/server/scenarios/request-churn/react/project.json @@ -15,6 +15,29 @@ "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": [ 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..733578ce7f --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts @@ -0,0 +1,85 @@ +import { + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const benchmarkSeed = 0xdecafbad +const requestChurnIterations = 200 +const itemPageMarker = 'data-bench="request-churn-item"' +const dehydrationMarker = '$_TSR' + +const requestInit = { + method: 'GET', + headers: { + accept: 'text/html', + }, +} satisfies RequestInit + +function buildItemRequest(random: () => number) { + const id = randomSegment(random) + const q = `q-${randomSegment(random)}` + + return new Request(`http://localhost/items/${id}?q=${q}`, 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) + + if (!body.includes(dehydrationMarker)) { + throw new Error( + 'Expected sanity response to include the dehydration marker', + ) + } +} + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: requestChurnIterations, + buildRequest: buildItemRequest, + validateResponse: validateItemResponse, + }) + + return { + sanity: () => assertRequestChurnSanity(handler), + run, + benches: [ + { + name: 'mem request-churn (react)', + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json index 4d78884cea..11ddcce4ea 100644 --- a/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json +++ b/benchmarks/memory/server/scenarios/request-churn/react/tsconfig.json @@ -1,12 +1,15 @@ { "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/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts index 95f409aa11..1dc2e0da86 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts @@ -1,104 +1,10 @@ -import { bench, describe } from 'vitest' -import { - memoryBenchOptions, - randomSegment, - runSequentialRequestLoop, -} from '../../../bench-utils' -import type { StartRequestHandler } from '../../../bench-utils' +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' -const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -const benchmarkSeed = 0x51eaa11 -const serializationPayloadIterations = 5 -const payloadPageMarker = 'data-bench="serialization-payload"' -const dehydrationMarker = '$_TSR' - -const requestInit = { - method: 'GET', - headers: { - accept: 'text/html', - }, -} satisfies RequestInit - -const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl -)) as { - default: StartRequestHandler -} - -function buildPayloadRequest(random: () => number, index: number) { - const id = `payload-${index}-${randomSegment(random)}` - - return new Request(`http://localhost/data/${id}`, requestInit) -} - -function knownMapKey(id: string) { - return `map-${id}-000` -} - -function getRequestId(request: Request) { - const url = new URL(request.url) - const match = /^\/data\/([^/]+)$/.exec(url.pathname) - const id = match?.[1] - - if (id === undefined) { - throw new Error(`Expected /data/$id request URL, got ${request.url}`) - } - - return decodeURIComponent(id) -} - -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, - _response: Response, - request: Request, -) { - if (!body.includes(payloadPageMarker)) { - throw new Error('Expected serialization-payload marker in response body') - } - - if (!body.includes(dehydrationMarker)) { - throw new Error('Expected serialization-payload dehydration script in body') - } - - const mapKey = knownMapKey(getRequestId(request)) - - if (!body.includes(mapKey)) { - throw new Error(`Expected dehydrated payload to include Map key ${mapKey}`) - } -} - -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, response, request) -} - -await assertSerializationPayloadSanity(handler) +const test = await setup() +await test.sanity() describe('memory', () => { - bench( - 'mem serialization-payload (react)', - () => - runSequentialRequestLoop(handler, { - seed: benchmarkSeed, - iterations: serializationPayloadIterations, - buildRequest: buildPayloadRequest, - validateResponse: validatePayloadResponse, - }), - memoryBenchOptions, - ) + registerServerMemoryBenches(test) }) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/project.json b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json index 5cc35a789f..467b40d734 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/react/project.json +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/project.json @@ -15,6 +15,29 @@ "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": [ 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..f9242dd942 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts @@ -0,0 +1,107 @@ +import { + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const benchmarkSeed = 0x51eaa11 +const serializationPayloadIterations = 5 +const payloadPageMarker = 'data-bench="serialization-payload"' +const dehydrationMarker = '$_TSR' + +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 knownMapKey(id: string) { + return `map-${id}-000` +} + +function getRequestId(request: Request) { + const url = new URL(request.url) + const match = /^\/data\/([^/]+)$/.exec(url.pathname) + const id = match?.[1] + + if (id === undefined) { + throw new Error(`Expected /data/$id request URL, got ${request.url}`) + } + + return decodeURIComponent(id) +} + +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, + _response: Response, + request: Request, +) { + if (!body.includes(payloadPageMarker)) { + throw new Error('Expected serialization-payload marker in response body') + } + + if (!body.includes(dehydrationMarker)) { + throw new Error('Expected serialization-payload dehydration script in body') + } + + const mapKey = knownMapKey(getRequestId(request)) + + if (!body.includes(mapKey)) { + throw new Error(`Expected dehydrated payload to include Map key ${mapKey}`) + } +} + +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, response, request) +} + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: serializationPayloadIterations, + buildRequest: buildPayloadRequest, + validateResponse: validatePayloadResponse, + }) + + return { + sanity: () => assertSerializationPayloadSanity(handler), + run, + benches: [ + { + name: 'mem serialization-payload (react)', + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json index 4d78884cea..11ddcce4ea 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/tsconfig.json @@ -1,12 +1,15 @@ { "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/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts index 4945ebdaf9..1dc2e0da86 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts @@ -1,237 +1,10 @@ -import { bench, describe } from 'vitest' -import { - createDeterministicRandom, - memoryBenchOptions, - randomSegment, - runSequentialRequestLoop, -} from '../../../bench-utils' -import type { StartRequestHandler } from '../../../bench-utils' +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' -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 appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -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') - -const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl -)) as { - default: StartRequestHandler -} - -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}`, - ) - } - - if (!body.includes(`${expectedId}-4`)) { - throw new Error(`Expected final payload record for ${expectedId}`) - } -} - -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) -} - -const urls = await discoverUrls(handler) - -await assertServerFnChurnSanity(handler, urls) +const test = await setup() +await test.sanity() describe('memory', () => { - bench( - 'mem server-fn-churn (react)', - () => - 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) - } - - const fixture = postFixtures[fixtureIndex]! - - return buildPostRequest(urls.post, fixture) - }, - validateResponse: validateServerFnResponse, - }), - memoryBenchOptions, - ) + registerServerMemoryBenches(test) }) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json index 926a50b9f1..841bbedca0 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/project.json @@ -15,6 +15,29 @@ "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": [ 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..c93fc4ab9b --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts @@ -0,0 +1,239 @@ +import { + createDeterministicRandom, + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +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 appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +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}`, + ) + } + + if (!body.includes(`${expectedId}-4`)) { + throw new Error(`Expected final payload record for ${expectedId}`) + } +} + +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 setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: 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) + } + + const fixture = postFixtures[fixtureIndex]! + + return buildPostRequest(urls.post, fixture) + }, + validateResponse: validateServerFnResponse, + }) + + return { + sanity: () => assertServerFnChurnSanity(handler, urls), + run, + benches: [ + { + name: 'mem server-fn-churn (react)', + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json index 4d78884cea..11ddcce4ea 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/tsconfig.json @@ -1,12 +1,15 @@ { "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/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts index cf0e2fa5ea..1dc2e0da86 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts @@ -1,145 +1,10 @@ -import { bench, describe } from 'vitest' -import { - memoryBenchOptions, - randomSegment, - runSequentialRequestLoop, -} from '../../../bench-utils' -import type { StartRequestHandler } from '../../../bench-utils' +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' -const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -const benchmarkSeed = 0xdecafbad -const streamingPeakIterations = 3 -const fallbackMarkers = [ - 'streaming-peak-fallback-0', - 'streaming-peak-fallback-1', - 'streaming-peak-fallback-2', - 'streaming-peak-fallback-3', -] as const -const deferredSectionMarkers = [ - 'streaming-peak-deferred-0', - 'streaming-peak-deferred-1', - 'streaming-peak-deferred-2', - 'streaming-peak-deferred-3', -] as const - -const requestInit = { - method: 'GET', - headers: { - accept: 'text/html', - }, -} satisfies RequestInit - -const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl -)) as { - default: StartRequestHandler -} - -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 } -} - -function assertFallbacksPrecedeDeferredContent(body: string) { - for (let index = 0; index < fallbackMarkers.length; index++) { - const fallbackIndex = body.indexOf(fallbackMarkers[index]!) - const deferredIndex = body.indexOf(deferredSectionMarkers[index]!) - - if (fallbackIndex === -1) { - throw new Error( - `Expected fallback marker ${fallbackMarkers[index]} in body`, - ) - } - - if (deferredIndex === -1) { - throw new Error( - `Expected deferred section marker ${deferredSectionMarkers[index]} in body`, - ) - } - - if (fallbackIndex > deferredIndex) { - throw new Error( - `Expected ${fallbackMarkers[index]} to precede ${deferredSectionMarkers[index]}`, - ) - } - } -} - -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}`, - ) - } - - assertFallbacksPrecedeDeferredContent(chunked.body) -} - -await assertStreamingPeakSanity(handler) +const test = await setup() +await test.sanity() describe('memory', () => { - bench( - 'mem streaming-peak chunked (react)', - async () => { - await runSequentialRequestLoop(handler, { - seed: benchmarkSeed, - iterations: streamingPeakIterations, - buildRequest: buildStreamingRequest, - validateResponse: validateStreamingResponse, - }) - }, - memoryBenchOptions, - ) + registerServerMemoryBenches(test) }) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/project.json b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json index d6fb362055..6fc1dcdda0 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/react/project.json +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/project.json @@ -15,6 +15,29 @@ "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": [ 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..75657e5998 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts @@ -0,0 +1,147 @@ +import { + randomSegment, + runSequentialRequestLoop, +} from '#memory-server/bench-utils' +import type { StartRequestHandler } from '#memory-server/bench-utils' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href +const benchmarkSeed = 0xdecafbad +const streamingPeakIterations = 3 +const fallbackMarkers = [ + 'streaming-peak-fallback-0', + 'streaming-peak-fallback-1', + 'streaming-peak-fallback-2', + 'streaming-peak-fallback-3', +] as const +const deferredSectionMarkers = [ + 'streaming-peak-deferred-0', + 'streaming-peak-deferred-1', + 'streaming-peak-deferred-2', + 'streaming-peak-deferred-3', +] as const + +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 } +} + +function assertFallbacksPrecedeDeferredContent(body: string) { + for (let index = 0; index < fallbackMarkers.length; index++) { + const fallbackIndex = body.indexOf(fallbackMarkers[index]!) + const deferredIndex = body.indexOf(deferredSectionMarkers[index]!) + + if (fallbackIndex === -1) { + throw new Error( + `Expected fallback marker ${fallbackMarkers[index]} in body`, + ) + } + + if (deferredIndex === -1) { + throw new Error( + `Expected deferred section marker ${deferredSectionMarkers[index]} in body`, + ) + } + + if (fallbackIndex > deferredIndex) { + throw new Error( + `Expected ${fallbackMarkers[index]} to precede ${deferredSectionMarkers[index]}`, + ) + } + } +} + +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}`, + ) + } + + assertFallbacksPrecedeDeferredContent(chunked.body) +} + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: streamingPeakIterations, + buildRequest: buildStreamingRequest, + validateResponse: validateStreamingResponse, + }) + + return { + sanity: () => assertStreamingPeakSanity(handler), + run, + benches: [ + { + name: 'mem streaming-peak chunked (react)', + run, + }, + ], + } +} diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json index 4d78884cea..11ddcce4ea 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/tsconfig.json @@ -1,12 +1,15 @@ { "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/vitest.react.config.ts b/benchmarks/memory/server/vitest.react.config.ts index 356ab7b3a8..44643b4d60 100644 --- a/benchmarks/memory/server/vitest.react.config.ts +++ b/benchmarks/memory/server/vitest.react.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { watch: false, + fileParallelism: false, projects: ['./scenarios/*/react/vite.config.ts'], }, }) diff --git a/package.json b/package.json index 75bc153169..4868afde6a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "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:server:flame": "nx run @benchmarks/memory-server:test:flame:react --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/pnpm-lock.yaml b/pnpm-lock.yaml index b30d7b42a1..dde49c342a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -311,7 +311,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)) benchmarks/memory/client: dependencies: @@ -331,6 +331,9 @@ importers: '@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) + '@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) @@ -340,6 +343,9 @@ importers: '@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)) + jsdom: + specifier: 29.1.1 + version: 29.1.1(@noble/hashes@2.0.1) typescript: specifier: ^6.0.2 version: 6.0.2 @@ -348,7 +354,7 @@ importers: 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) 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/server: dependencies: @@ -368,6 +374,9 @@ importers: '@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) + '@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)) @@ -379,7 +388,7 @@ importers: 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) 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/ssr: dependencies: @@ -437,7 +446,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: @@ -482,7 +491,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: @@ -1927,7 +1936,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 @@ -2092,7 +2101,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.21)) + version: 2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.23)) '@tanstack/router-e2e-utils': specifier: workspace:^ version: link:../../e2e-utils @@ -2156,7 +2165,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 @@ -2254,7 +2263,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 @@ -3290,7 +3299,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 @@ -3311,7 +3320,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 @@ -9005,7 +9014,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 @@ -9747,7 +9756,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 @@ -11496,7 +11505,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 @@ -11708,7 +11717,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 @@ -13668,9 +13677,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==} @@ -14008,6 +14029,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==} @@ -14259,6 +14284,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'} @@ -14273,6 +14302,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'} @@ -14287,6 +14323,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'} @@ -14299,12 +14342,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'} @@ -14313,6 +14370,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==} @@ -15284,6 +15345,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==} @@ -21064,6 +21134,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'} @@ -21109,6 +21183,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==} @@ -21165,6 +21243,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: @@ -21507,6 +21588,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'} @@ -22418,6 +22503,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==} @@ -22960,6 +23049,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'} @@ -23284,6 +23382,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==} @@ -23353,6 +23455,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'} @@ -24016,6 +24121,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'} @@ -25491,6 +25599,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==} @@ -25702,6 +25814,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==} @@ -26386,6 +26502,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'} @@ -26480,6 +26600,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'} @@ -26488,6 +26612,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==} @@ -26769,6 +26897,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 @@ -26777,6 +26913,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': {} @@ -27320,6 +27466,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 @@ -27653,7 +27803,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 @@ -27694,6 +27844,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) @@ -27704,6 +27856,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 @@ -27718,6 +27875,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 @@ -27726,14 +27890,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 @@ -28420,6 +28594,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': @@ -31517,9 +31695,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.21))': + '@rsbuild/plugin-react@2.0.0(@rsbuild/core@2.0.1)(@rspack/core@2.0.5(@swc/helpers@0.5.23))': dependencies: - '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.21))(react-refresh@0.18.0) + '@rspack/plugin-react-refresh': 2.0.0(@rspack/core@2.0.5(@swc/helpers@0.5.23))(react-refresh@0.18.0) react-refresh: 0.18.0 optionalDependencies: '@rsbuild/core': 2.0.1 @@ -31710,13 +31888,6 @@ 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 @@ -31725,12 +31896,6 @@ 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 @@ -33610,7 +33775,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@25.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)) + 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)) '@vitest/utils@4.1.4': dependencies: @@ -35076,6 +35241,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: {} @@ -35117,6 +35287,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: @@ -35145,6 +35322,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 @@ -35468,6 +35647,8 @@ snapshots: entities@6.0.0: {} + entities@8.0.0: {} + env-paths@2.2.1: {} env-paths@3.0.0: {} @@ -36683,6 +36864,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: @@ -37259,6 +37446,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: {} @@ -37560,6 +37773,8 @@ snapshots: lru-cache@11.2.2: {} + lru-cache@11.5.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -37620,6 +37835,8 @@ snapshots: mdn-data@2.12.2: {} + mdn-data@2.27.1: {} + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -37863,7 +38080,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) @@ -37878,7 +38095,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 @@ -37915,7 +38132,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) @@ -37930,7 +38147,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 @@ -37966,7 +38183,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) @@ -37981,7 +38198,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 @@ -38636,6 +38853,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: @@ -40281,6 +40502,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: @@ -40456,6 +40681,8 @@ snapshots: undici@7.24.4: {} + undici@7.27.2: {} + unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 @@ -40557,27 +40784,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: @@ -40795,10 +41024,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 @@ -40824,7 +41053,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)) @@ -40849,7 +41107,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 @@ -40990,6 +41248,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 @@ -41181,6 +41441,8 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} + whatwg-url@14.1.0: dependencies: tr46: 5.0.0 @@ -41191,6 +41453,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 From 9bd39c0feb84565c0432ab13874b07a8b268ffda Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 14:43:10 +0200 Subject: [PATCH 09/24] direct pprof calls for cleaner output --- benchmarks/memory/README.md | 12 ++- benchmarks/memory/client/flame-runner.ts | 2 +- benchmarks/memory/client/package.json | 1 + benchmarks/memory/flame-control.ts | 115 ++++++++++++++++++----- benchmarks/memory/run-flame.mjs | 23 +++-- benchmarks/memory/server/package.json | 1 + pnpm-lock.yaml | 6 ++ 7 files changed, 121 insertions(+), 39 deletions(-) diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md index d6c007aa43..167ab28f27 100644 --- a/benchmarks/memory/README.md +++ b/benchmarks/memory/README.md @@ -118,7 +118,8 @@ pnpm nx run @benchmarks/memory-client:test:types --outputStyle=stream --skipRemo ``` Local attribution profiling, without CodSpeed CLI/login/sudo/upload, uses -`@platformatic/flame` heap profiles. These targets rebuild the scenarios with +`@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 @@ -144,10 +145,11 @@ directory, including `heap-profile-*.html` and `heap-profile-*.md`. The 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. Reports can include `@platformatic/flame` and pprof shutdown -frames. 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. +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: diff --git a/benchmarks/memory/client/flame-runner.ts b/benchmarks/memory/client/flame-runner.ts index 504e4cda7d..93abe6cf13 100644 --- a/benchmarks/memory/client/flame-runner.ts +++ b/benchmarks/memory/client/flame-runner.ts @@ -8,7 +8,7 @@ export async function runClientFlameBenchmark( const test = setup() try { - await setup().sanity() + await test.sanity() await test.before?.() await profileFlameWorkload(test.run) } finally { diff --git a/benchmarks/memory/client/package.json b/benchmarks/memory/client/package.json index 3602143c57..abee683e8c 100644 --- a/benchmarks/memory/client/package.json +++ b/benchmarks/memory/client/package.json @@ -27,6 +27,7 @@ }, "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", diff --git a/benchmarks/memory/flame-control.ts b/benchmarks/memory/flame-control.ts index e3aa127033..4ebfe3c791 100644 --- a/benchmarks/memory/flame-control.ts +++ b/benchmarks/memory/flame-control.ts @@ -1,49 +1,114 @@ +import fs from 'node:fs' +import { createRequire } from 'node:module' +import path from 'node:path' + const flameEnabled = process.env.TSR_MEMORY_FLAME === '1' -const flameStartDelayMs = 250 +const heapIntervalBytes = 512 * 1024 +const heapStackDepth = 64 -function wait(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }) +interface HeapProfile { + encode: () => Uint8Array } -function waitForSignalHandler() { - return wait(0) +interface HeapProfiler { + start: (intervalBytes: number, stackDepth: number) => void + stop: () => void + v8Profile: () => unknown + convertProfile: ( + v8Profile: unknown, + ignoreSamplePath?: string, + sourceMapper?: unknown, + ) => HeapProfile } -async function toggleFlameProfile() { - if (!flameEnabled) { - return - } +interface SourceMapperConstructor { + create: (searchDirs: Array) => Promise +} - if (process.platform === 'win32') { - throw new Error('Flame manual profiling is not supported on Windows') - } +interface PprofModule { + heap: HeapProfiler + SourceMapper: SourceMapperConstructor +} - process.kill(process.pid, 'SIGUSR2') - await waitForSignalHandler() +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) } -export async function startFlameProfile() { - if (flameEnabled) { - // Flame initializes sourcemap support asynchronously when the process starts. - await wait(flameStartDelayMs) +async function createSourceMapper(pprof: PprofModule) { + const sourcemapDirs = getSourcemapDirs() + + if (sourcemapDirs.length === 0) { + return undefined } - await toggleFlameProfile() + return pprof.SourceMapper.create(sourcemapDirs) } -export async function stopFlameProfile() { - await toggleFlameProfile() +function formatTimestamp() { + return new Date().toISOString().replace(/[:.]/g, '-') } export async function profileFlameWorkload( workload: () => Promise | void, ) { - await startFlameProfile() + 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 { - await stopFlameProfile() + try { + v8Profile = pprof.heap.v8Profile() + } finally { + pprof.heap.stop() + } + } + + const heapProfile = pprof.heap.convertProfile( + v8Profile, + undefined, + sourceMapper, + ) + const profilePath = path.join( + profileDir, + `heap-profile-${formatTimestamp()}.pb`, + ) + + 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 index f999be5340..cc568f3f77 100644 --- a/benchmarks/memory/run-flame.mjs +++ b/benchmarks/memory/run-flame.mjs @@ -1,4 +1,5 @@ #!/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' @@ -35,22 +36,28 @@ const profileDir = path.join( fs.mkdirSync(profileDir, { recursive: true }) const entrypointRequire = createRequire(entrypointPath) -const { generateFlamegraph, generateMarkdown, startProfiling } = - entrypointRequire('@platformatic/flame') +const { generateFlamegraph, generateMarkdown } = entrypointRequire( + '@platformatic/flame', +) process.env.NODE_ENV = 'production' -process.env.TSR_MEMORY_FLAME = '1' console.log(`Flame profile directory: ${profileDir}`) -const { pid, process: childProcess } = startProfiling(entrypointPath, [], { - autoStart: false, +const childProcess = spawn(process.execPath, [entrypointPath], { cwd: profileDir, - mdFormat: 'detailed', - sourcemapDirs: [sourcemapDirPath], + 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: ${pid}`) +console.log(`Flame profiling process: ${childProcess.pid}`) const childCode = await new Promise((resolve) => { childProcess.on('error', (error) => { diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json index 11ec470bd9..f9fcae7cc8 100644 --- a/benchmarks/memory/server/package.json +++ b/benchmarks/memory/server/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@codspeed/vitest-plugin": "^5.5.0", + "@datadog/pprof": "^5.13.2", "@platformatic/flame": "^1.6.0", "@vitejs/plugin-react": "^6.0.1", "typescript": "^6.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dde49c342a..2b2d43c119 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,6 +331,9 @@ importers: '@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 @@ -374,6 +377,9 @@ importers: '@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 From 3e63fd2c101edb757f163115ce544c9b82f9cc19 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 15:29:26 +0200 Subject: [PATCH 10/24] review --- benchmarks/memory/README.md | 2 +- benchmarks/memory/server/bench-utils.ts | 26 +++++++++----- .../scenarios/aborted-requests/react/setup.ts | 4 ++- .../scenarios/error-paths/react/setup.ts | 34 ++++++++++++++----- .../scenarios/request-churn/react/setup.ts | 8 +++-- 5 files changed, 53 insertions(+), 21 deletions(-) diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md index 167ab28f27..9cea416f0b 100644 --- a/benchmarks/memory/README.md +++ b/benchmarks/memory/README.md @@ -2,7 +2,7 @@ Dedicated memory benchmarks for TanStack Router / Start, measured with the CodSpeed **memory instrument** (`mode: memory` in -`.github/workflows/memory-benchmarks.yml`). Two separate benchmarks: +`.github/workflows/client-nav-benchmarks.yml`). Two separate benchmarks: - `server/` (`@benchmarks/memory-server`) — React Start apps, requests against the built server handler (`handler.fetch`), Node environment. diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts index 4614152cc2..ac4cdd082d 100644 --- a/benchmarks/memory/server/bench-utils.ts +++ b/benchmarks/memory/server/bench-utils.ts @@ -2,8 +2,17 @@ export interface StartRequestHandler { fetch: (request: Request) => Promise | Response } -export interface RunSequentialRequestLoopOptions { - seed: number +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 @@ -51,14 +60,13 @@ export async function drainResponse(response: Response) { export async function runSequentialRequestLoop( handler: StartRequestHandler, - { - seed, - iterations = 10, - buildRequest, - validateResponse, - }: RunSequentialRequestLoopOptions, + options: RunSequentialRequestLoopOptions, ) { - const random = createDeterministicRandom(seed) + const { iterations = 10, buildRequest, validateResponse } = options + const random = + options.seed !== undefined + ? createDeterministicRandom(options.seed) + : options.random const validate = validateResponse ?? ((response: Response, request: Request) => { diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts index 404b8410c2..4c453a5245 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts @@ -2,6 +2,7 @@ import type { StartRequestHandler } from '#memory-server/bench-utils' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href 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"' @@ -130,7 +131,8 @@ async function assertAbortedRequestsSanity(handler: StartRequestHandler) { async function runAbortedRequestLoop(handler: StartRequestHandler) { for (let index = 0; index < abortedRequestIterations; index++) { const controller = new AbortController() - const request = buildStreamRequest(`abort-${index}`, controller.signal) + const id = `abort-${(abortedRequestCounter++).toString(36)}` + const request = buildStreamRequest(id, controller.signal) const response = await handler.fetch(request) validateDocumentResponse(response, request) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts index d51bebd948..de256d4686 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts +++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts @@ -1,4 +1,5 @@ import { + createDeterministicRandom, randomSegment, runSequentialRequestLoop, } from '#memory-server/bench-utils' @@ -10,6 +11,15 @@ const redirectSeed = 0xdecafbad const notFoundSeed = 0xdecafb0d const errorSeed = 0xdecafbed const unmatchedSeed = 0xdecaf00d +// 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 redirectStatus = 302 const notFoundStatus = 404 @@ -25,29 +35,37 @@ const requestInit = { } satisfies RequestInit function buildRedirectRequest(random: () => number) { + const id = `${(redirectCounter++).toString(36)}-${randomSegment(random)}` + return new Request( - `http://localhost/from/${randomSegment(random)}`, + `http://localhost/from/${id}`, requestInit, ) } function buildNotFoundRequest(random: () => number) { + const id = `${(notFoundCounter++).toString(36)}-${randomSegment(random)}` + return new Request( - `http://localhost/missing/${randomSegment(random)}`, + `http://localhost/missing/${id}`, requestInit, ) } function buildErrorRequest(random: () => number) { + const id = `${(errorCounter++).toString(36)}-${randomSegment(random)}` + return new Request( - `http://localhost/boom/${randomSegment(random)}`, + `http://localhost/boom/${id}`, requestInit, ) } function buildUnmatchedRequest(random: () => number) { + const id = `${(unmatchedCounter++).toString(36)}-${randomSegment(random)}` + return new Request( - `http://localhost/nope/${randomSegment(random)}`, + `http://localhost/nope/${id}`, requestInit, ) } @@ -152,7 +170,7 @@ export async function setup() { const runRedirect = () => runSequentialRequestLoop(handler, { - seed: redirectSeed, + random: redirectRandom, iterations: errorPathsIterations, buildRequest: buildRedirectRequest, validateResponse: validateRedirectResponse, @@ -160,7 +178,7 @@ export async function setup() { const runNotFound = () => runSequentialRequestLoop(handler, { - seed: notFoundSeed, + random: notFoundRandom, iterations: errorPathsIterations, buildRequest: buildNotFoundRequest, validateResponse: validateNotFoundResponse, @@ -168,7 +186,7 @@ export async function setup() { const runError = () => runSequentialRequestLoop(handler, { - seed: errorSeed, + random: errorRandom, iterations: errorPathsIterations, buildRequest: buildErrorRequest, validateResponse: validateErrorResponse, @@ -176,7 +194,7 @@ export async function setup() { const runUnmatched = () => runSequentialRequestLoop(handler, { - seed: unmatchedSeed, + random: unmatchedRandom, iterations: errorPathsIterations, buildRequest: buildUnmatchedRequest, validateResponse: validateNotFoundResponse, diff --git a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts index 733578ce7f..13cdfa8bc5 100644 --- a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts +++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts @@ -1,4 +1,5 @@ import { + createDeterministicRandom, randomSegment, runSequentialRequestLoop, } from '#memory-server/bench-utils' @@ -9,6 +10,9 @@ const benchmarkSeed = 0xdecafbad const requestChurnIterations = 200 const itemPageMarker = 'data-bench="request-churn-item"' const dehydrationMarker = '$_TSR' +// Module-level so CodSpeed warmups and measurement never replay URLs. +const benchmarkRandom = createDeterministicRandom(benchmarkSeed) +let requestCounter = 0 const requestInit = { method: 'GET', @@ -18,7 +22,7 @@ const requestInit = { } satisfies RequestInit function buildItemRequest(random: () => number) { - const id = randomSegment(random) + const id = `${(requestCounter++).toString(36)}-${randomSegment(random)}` const q = `q-${randomSegment(random)}` return new Request(`http://localhost/items/${id}?q=${q}`, requestInit) @@ -66,7 +70,7 @@ export async function setup() { const run = () => runSequentialRequestLoop(handler, { - seed: benchmarkSeed, + random: benchmarkRandom, iterations: requestChurnIterations, buildRequest: buildItemRequest, validateResponse: validateItemResponse, From 073a0364e853e54cdc505a18c54e22c701fb6222 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 15:41:35 +0200 Subject: [PATCH 11/24] increase iteration count for low benches --- .../memory/server/scenarios/peak-large-page/react/setup.ts | 2 +- .../server/scenarios/serialization-payload/react/setup.ts | 2 +- .../memory/server/scenarios/streaming-peak/react/setup.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts index e10d034aa2..bbe9fb51d9 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts @@ -3,7 +3,7 @@ import type { StartRequestHandler } from '#memory-server/bench-utils' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href const benchmarkSeed = 0x5eed_0005 -const peakLargePageIterations = 2 +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 knownDehydratedRecordName = 'peak-large-page-l8-record-199' diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts index f9242dd942..dd33533200 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts @@ -6,7 +6,7 @@ import type { StartRequestHandler } from '#memory-server/bench-utils' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href const benchmarkSeed = 0x51eaa11 -const serializationPayloadIterations = 5 +const serializationPayloadIterations = 20 const payloadPageMarker = 'data-bench="serialization-payload"' const dehydrationMarker = '$_TSR' diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts index 75657e5998..74b2f49271 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts @@ -6,7 +6,7 @@ import type { StartRequestHandler } from '#memory-server/bench-utils' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href const benchmarkSeed = 0xdecafbad -const streamingPeakIterations = 3 +const streamingPeakIterations = 20 const fallbackMarkers = [ 'streaming-peak-fallback-0', 'streaming-peak-fallback-1', From d8a4e76a3e7b161d1b89085fb84a81b1ce28e64c Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 16:03:29 +0200 Subject: [PATCH 12/24] flame run splits by benches, like codspeed --- benchmarks/memory/flame-control.ts | 14 +++++++++++++- benchmarks/memory/server/benchmark.ts | 1 - benchmarks/memory/server/flame-runner.ts | 5 ++++- .../scenarios/aborted-requests/react/setup.ts | 1 - .../server/scenarios/error-paths/react/setup.ts | 6 ------ .../scenarios/peak-large-page/react/setup.ts | 1 - .../server/scenarios/request-churn/react/setup.ts | 1 - .../scenarios/serialization-payload/react/setup.ts | 1 - .../scenarios/server-fn-churn/react/setup.ts | 1 - .../server/scenarios/streaming-peak/react/setup.ts | 1 - 10 files changed, 17 insertions(+), 15 deletions(-) diff --git a/benchmarks/memory/flame-control.ts b/benchmarks/memory/flame-control.ts index 4ebfe3c791..fda037cddd 100644 --- a/benchmarks/memory/flame-control.ts +++ b/benchmarks/memory/flame-control.ts @@ -59,8 +59,20 @@ 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() @@ -102,7 +114,7 @@ export async function profileFlameWorkload( ) const profilePath = path.join( profileDir, - `heap-profile-${formatTimestamp()}.pb`, + formatProfileFileName(profileName), ) fs.writeFileSync(profilePath, heapProfile.encode()) diff --git a/benchmarks/memory/server/benchmark.ts b/benchmarks/memory/server/benchmark.ts index 714b16baa4..02379a8ecf 100644 --- a/benchmarks/memory/server/benchmark.ts +++ b/benchmarks/memory/server/benchmark.ts @@ -5,6 +5,5 @@ export interface ServerMemoryBench { export interface ServerMemoryBenchmark { sanity: () => Promise | void - run: () => Promise | void benches: Array } diff --git a/benchmarks/memory/server/flame-runner.ts b/benchmarks/memory/server/flame-runner.ts index d2a3425b73..d1892815c9 100644 --- a/benchmarks/memory/server/flame-runner.ts +++ b/benchmarks/memory/server/flame-runner.ts @@ -7,5 +7,8 @@ export async function runServerFlameBenchmark( const test = await setup() await test.sanity() - await profileFlameWorkload(test.run) + + for (const bench of test.benches) { + await profileFlameWorkload(bench.run, bench.name) + } } diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts index 4c453a5245..b3f1fc047b 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts @@ -154,7 +154,6 @@ export async function setup() { return { sanity: () => assertAbortedRequestsSanity(handler), - run, benches: [ { name: 'mem aborted-requests (react)', diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts index de256d4686..50288f4358 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts +++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts @@ -202,12 +202,6 @@ export async function setup() { return { sanity: () => assertErrorPathsSanity(handler), - async run() { - await runRedirect() - await runNotFound() - await runError() - await runUnmatched() - }, benches: [ { name: 'mem error-paths redirect (react)', diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts index bbe9fb51d9..28936ea0c6 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts @@ -65,7 +65,6 @@ export async function setup() { return { sanity: () => assertPeakLargePageSanity(handler), - run, benches: [ { name: 'mem peak-large-page (react)', diff --git a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts index 13cdfa8bc5..8da5590818 100644 --- a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts +++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts @@ -78,7 +78,6 @@ export async function setup() { return { sanity: () => assertRequestChurnSanity(handler), - run, benches: [ { name: 'mem request-churn (react)', diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts index dd33533200..b67c0089e4 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts @@ -96,7 +96,6 @@ export async function setup() { return { sanity: () => assertSerializationPayloadSanity(handler), - run, benches: [ { name: 'mem serialization-payload (react)', diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts index c93fc4ab9b..9aaf3e0256 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts @@ -228,7 +228,6 @@ export async function setup() { return { sanity: () => assertServerFnChurnSanity(handler, urls), - run, benches: [ { name: 'mem server-fn-churn (react)', diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts index 74b2f49271..895520d45f 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts @@ -136,7 +136,6 @@ export async function setup() { return { sanity: () => assertStreamingPeakSanity(handler), - run, benches: [ { name: 'mem streaming-peak chunked (react)', From d1ef03d77768d86859452c78aca19fd29efd9c7a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:04:50 +0000 Subject: [PATCH 13/24] ci: apply automated fixes --- benchmarks/memory/flame-control.ts | 5 +---- benchmarks/memory/server/bench-utils.ts | 11 +++++----- .../scenarios/error-paths/react/setup.ts | 20 ++++--------------- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/benchmarks/memory/flame-control.ts b/benchmarks/memory/flame-control.ts index fda037cddd..0d6f42df33 100644 --- a/benchmarks/memory/flame-control.ts +++ b/benchmarks/memory/flame-control.ts @@ -112,10 +112,7 @@ export async function profileFlameWorkload( undefined, sourceMapper, ) - const profilePath = path.join( - profileDir, - formatProfileFileName(profileName), - ) + const profilePath = path.join(profileDir, formatProfileFileName(profileName)) fs.writeFileSync(profilePath, heapProfile.encode()) console.log(`Heap profile written to: ${profilePath}`) diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts index ac4cdd082d..d1f1406b51 100644 --- a/benchmarks/memory/server/bench-utils.ts +++ b/benchmarks/memory/server/bench-utils.ts @@ -12,11 +12,12 @@ type RunSequentialRequestLoopRandomOptions = seed?: never } -export type RunSequentialRequestLoopOptions = RunSequentialRequestLoopRandomOptions & { - iterations?: number - buildRequest: (random: () => number, index: number) => Request - validateResponse?: (response: Response, request: Request) => void -} +export type RunSequentialRequestLoopOptions = + RunSequentialRequestLoopRandomOptions & { + iterations?: number + buildRequest: (random: () => number, index: number) => Request + validateResponse?: (response: Response, request: Request) => void + } export const memoryBenchOptions = { iterations: 1, diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts index 50288f4358..3e5b658ff6 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts +++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts @@ -37,37 +37,25 @@ const requestInit = { function buildRedirectRequest(random: () => number) { const id = `${(redirectCounter++).toString(36)}-${randomSegment(random)}` - return new Request( - `http://localhost/from/${id}`, - requestInit, - ) + 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, - ) + 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, - ) + 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, - ) + return new Request(`http://localhost/nope/${id}`, requestInit) } function getRequestId(request: Request) { From 2ff1a0de2a65e48f78b9020f2e3a154970a9787c Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 18:19:23 +0200 Subject: [PATCH 14/24] QA --- benchmarks/memory/client/bench-utils.ts | 1 + .../interrupted-navigations/react/setup.ts | 21 +++++++++++++++---- benchmarks/memory/client/vitest.setup.ts | 9 +++++--- benchmarks/memory/server/bench-utils.ts | 1 + 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/benchmarks/memory/client/bench-utils.ts b/benchmarks/memory/client/bench-utils.ts index 28bfe6c512..0cbaedbd71 100644 --- a/benchmarks/memory/client/bench-utils.ts +++ b/benchmarks/memory/client/bench-utils.ts @@ -3,6 +3,7 @@ export const memoryBenchOptions = { warmupIterations: 1, time: 0, warmupTime: 0, + throws: true, } export function createDeterministicRandom(seed: number) { diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts index 320d4cfb0b..3f62369ec5 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts @@ -82,6 +82,21 @@ function assertSlowNavigationSettlement(settlement: NavigationSettlement) { ) } +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' } @@ -250,13 +265,11 @@ export function setup() { resolveSlowLoader(slowId) const settlement = await slowNavigation - // Superseded loads currently always resolve; the guard keeps the bench - // alive if router-core ever starts rejecting them. - await slowLoadPromise.catch(() => undefined) + assertSlowNavigationSettlement(settlement) + await awaitExpectedLoadSettlement(slowLoadPromise) await drainMicrotasks() if (assertShape) { - assertSlowNavigationSettlement(settlement) assertRenderedPage('fast', fastId) } } diff --git a/benchmarks/memory/client/vitest.setup.ts b/benchmarks/memory/client/vitest.setup.ts index 9bac58bd41..ce3127275a 100644 --- a/benchmarks/memory/client/vitest.setup.ts +++ b/benchmarks/memory/client/vitest.setup.ts @@ -1,6 +1,9 @@ -import { vi } from 'vitest' - // @ts-expect-error global.IS_REACT_ACT_ENVIRONMENT = true -window.scrollTo = vi.fn() +const scrollTo = () => {} + +window.scrollTo = scrollTo +globalThis.scrollTo = scrollTo + +export {} diff --git a/benchmarks/memory/server/bench-utils.ts b/benchmarks/memory/server/bench-utils.ts index d1f1406b51..f1f5dadea0 100644 --- a/benchmarks/memory/server/bench-utils.ts +++ b/benchmarks/memory/server/bench-utils.ts @@ -24,6 +24,7 @@ export const memoryBenchOptions = { warmupIterations: 1, time: 0, warmupTime: 0, + throws: true, } export function createDeterministicRandom(seed: number) { From 92c26889bf2b503c2cea345e2392c14e654cd684 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 18:27:45 +0200 Subject: [PATCH 15/24] build outside of codspeed instrumentation --- .github/workflows/client-nav-benchmarks.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml index 5db0e1af9c..8ccb0d12cc 100644 --- a/.github/workflows/client-nav-benchmarks.yml +++ b/.github/workflows/client-nav-benchmarks.yml @@ -67,6 +67,13 @@ 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: @@ -75,3 +82,4 @@ jobs: WITH_INSTRUMENTATION=1 pnpm nx run @benchmarks/${{ matrix.benchmark }}:test:perf:${{ matrix.framework }} + --excludeTaskDependencies From fab18e6f11c76fbb34a0f2f8b0924d379ae08046 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 16:29:52 +0000 Subject: [PATCH 16/24] ci: apply automated fixes --- .../client/scenarios/interrupted-navigations/react/setup.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts index 3f62369ec5..d0532e87e6 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts @@ -86,10 +86,7 @@ async function awaitExpectedLoadSettlement(loadPromise: Promise) { try { await loadPromise } catch (reason) { - if ( - reasonHasAbortShape(reason) || - reasonHasCancellationShape(reason) - ) { + if (reasonHasAbortShape(reason) || reasonHasCancellationShape(reason)) { return } From 6a4c1f6fae4ae6c9b8e6dc565c9b3c9871e76724 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 18:49:15 +0200 Subject: [PATCH 17/24] nitpick --- .../memory/server/scenarios/server-fn-churn/react/setup.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts index 9aaf3e0256..adc07b6b85 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts @@ -215,13 +215,12 @@ export async function setup() { if (index % 2 === 0) { const fixture = getFixtures[fixtureIndex]! - return buildGetRequest(urls.get, fixture) + } else { + const fixture = postFixtures[fixtureIndex]! + return buildPostRequest(urls.post, fixture) } - const fixture = postFixtures[fixtureIndex]! - - return buildPostRequest(urls.post, fixture) }, validateResponse: validateServerFnResponse, }) From a30c37e7afd20a4a8649df2d8e4441cab60833d3 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 18:52:17 +0200 Subject: [PATCH 18/24] fix workspace deps --- benchmarks/memory/client/package.json | 4 ++-- benchmarks/memory/server/package.json | 4 ++-- .../memory/server/scenarios/server-fn-churn/react/setup.ts | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/benchmarks/memory/client/package.json b/benchmarks/memory/client/package.json index abee683e8c..534225d2a3 100644 --- a/benchmarks/memory/client/package.json +++ b/benchmarks/memory/client/package.json @@ -20,8 +20,8 @@ } }, "dependencies": { - "@tanstack/react-router": "workspace:^", - "@tanstack/router-core": "workspace:^", + "@tanstack/react-router": "workspace:*", + "@tanstack/router-core": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/benchmarks/memory/server/package.json b/benchmarks/memory/server/package.json index f9fcae7cc8..2427549e87 100644 --- a/benchmarks/memory/server/package.json +++ b/benchmarks/memory/server/package.json @@ -11,8 +11,8 @@ "#memory-server/runner": "./runner.ts" }, "dependencies": { - "@tanstack/react-router": "workspace:^", - "@tanstack/react-start": "workspace:^", + "@tanstack/react-router": "workspace:*", + "@tanstack/react-start": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts index adc07b6b85..0469c70909 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts @@ -220,7 +220,6 @@ export async function setup() { const fixture = postFixtures[fixtureIndex]! return buildPostRequest(urls.post, fixture) } - }, validateResponse: validateServerFnResponse, }) From cbfde6df47025cd1a41246d8266f96c6f3f3e99a Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 22:32:59 +0200 Subject: [PATCH 19/24] solid & vue --- .github/workflows/client-nav-benchmarks.yml | 10 - benchmarks/memory/README.md | 34 +- benchmarks/memory/client/package.json | 149 +++++++- .../interrupted-navigations/react/setup.ts | 291 +-------------- .../interrupted-navigations/shared.ts | 339 ++++++++++++++++++ .../solid/memory.bench.ts | 9 + .../solid/memory.flame.ts | 4 + .../solid/project.json | 54 +++ .../interrupted-navigations/solid/setup.ts | 20 ++ .../interrupted-navigations/solid/src/app.tsx | 35 ++ .../solid/src/routeTree.gen.ts | 95 +++++ .../solid/src/router.tsx | 17 + .../solid/src/routes/__root.tsx | 9 + .../solid/src/routes/fast.$id.tsx | 21 ++ .../solid/src/routes/index.tsx | 9 + .../solid/src/routes/slow.$id.tsx | 21 ++ .../solid/src/slow-loaders.ts | 54 +++ .../solid/tsconfig.json | 19 + .../solid/vite.config.ts | 34 ++ .../vue/memory.bench.ts | 9 + .../vue/memory.flame.ts | 4 + .../interrupted-navigations/vue/project.json | 54 +++ .../interrupted-navigations/vue/setup.ts | 20 ++ .../interrupted-navigations/vue/src/app.tsx | 42 +++ .../vue/src/routeTree.gen.ts | 95 +++++ .../vue/src/router.tsx | 17 + .../vue/src/routes/__root.tsx | 9 + .../vue/src/routes/fast.$id.tsx | 21 ++ .../vue/src/routes/index.tsx | 9 + .../vue/src/routes/slow.$id.tsx | 21 ++ .../vue/src/slow-loaders.ts | 54 +++ .../interrupted-navigations/vue/tsconfig.json | 19 + .../vue/vite.config.ts | 36 ++ .../loader-data-retention/react/setup.ts | 162 +-------- .../scenarios/loader-data-retention/shared.ts | 205 +++++++++++ .../solid/memory.bench.ts | 9 + .../solid/memory.flame.ts | 4 + .../loader-data-retention/solid/project.json | 54 +++ .../loader-data-retention/solid/setup.ts | 11 + .../loader-data-retention/solid/src/app.tsx | 31 ++ .../solid/src/loader-data.ts | 48 +++ .../solid/src/routeTree.gen.ts | 102 ++++++ .../solid/src/router.tsx | 19 + .../solid/src/routes/__root.tsx | 9 + .../solid/src/routes/page.$id.tsx | 21 ++ .../solid/src/routes/shell.index.tsx | 9 + .../solid/src/routes/shell.tsx | 9 + .../loader-data-retention/solid/tsconfig.json | 19 + .../solid/vite.config.ts | 34 ++ .../loader-data-retention/vue/memory.bench.ts | 9 + .../loader-data-retention/vue/memory.flame.ts | 4 + .../loader-data-retention/vue/project.json | 54 +++ .../loader-data-retention/vue/setup.ts | 11 + .../loader-data-retention/vue/src/app.tsx | 38 ++ .../vue/src/loader-data.ts | 48 +++ .../vue/src/routeTree.gen.ts | 102 ++++++ .../loader-data-retention/vue/src/router.tsx | 19 + .../vue/src/routes/__root.tsx | 9 + .../vue/src/routes/page.$id.tsx | 22 ++ .../vue/src/routes/shell.index.tsx | 9 + .../vue/src/routes/shell.tsx | 9 + .../loader-data-retention/vue/tsconfig.json | 19 + .../loader-data-retention/vue/vite.config.ts | 36 ++ .../scenarios/mount-unmount/react/setup.ts | 66 +--- .../client/scenarios/mount-unmount/shared.ts | 90 +++++ .../mount-unmount/solid/memory.bench.ts | 9 + .../mount-unmount/solid/memory.flame.ts | 4 + .../mount-unmount/solid/project.json | 54 +++ .../scenarios/mount-unmount/solid/setup.ts | 11 + .../scenarios/mount-unmount/solid/src/app.tsx | 27 ++ .../mount-unmount/solid/src/routeTree.gen.ts | 59 +++ .../mount-unmount/solid/src/router.tsx | 17 + .../mount-unmount/solid/src/routes/__root.tsx | 9 + .../mount-unmount/solid/src/routes/a.tsx | 12 + .../mount-unmount/solid/tsconfig.json | 19 + .../mount-unmount/solid/vite.config.ts | 34 ++ .../mount-unmount/vue/memory.bench.ts | 9 + .../mount-unmount/vue/memory.flame.ts | 4 + .../scenarios/mount-unmount/vue/project.json | 54 +++ .../scenarios/mount-unmount/vue/setup.ts | 11 + .../scenarios/mount-unmount/vue/src/app.tsx | 34 ++ .../mount-unmount/vue/src/routeTree.gen.ts | 59 +++ .../mount-unmount/vue/src/router.tsx | 17 + .../mount-unmount/vue/src/routes/__root.tsx | 9 + .../mount-unmount/vue/src/routes/a.tsx | 12 + .../scenarios/mount-unmount/vue/tsconfig.json | 19 + .../mount-unmount/vue/vite.config.ts | 36 ++ .../scenarios/navigation-churn/react/setup.ts | 117 +----- .../scenarios/navigation-churn/shared.ts | 141 ++++++++ .../navigation-churn/solid/memory.bench.ts | 9 + .../navigation-churn/solid/memory.flame.ts | 4 + .../navigation-churn/solid/project.json | 54 +++ .../scenarios/navigation-churn/solid/setup.ts | 11 + .../navigation-churn/solid/src/app.tsx | 29 ++ .../solid/src/routeTree.gen.ts | 77 ++++ .../navigation-churn/solid/src/router.tsx | 17 + .../solid/src/routes/__root.tsx | 9 + .../navigation-churn/solid/src/routes/a.tsx | 14 + .../navigation-churn/solid/src/routes/b.tsx | 14 + .../navigation-churn/solid/tsconfig.json | 19 + .../navigation-churn/solid/vite.config.ts | 34 ++ .../navigation-churn/vue/memory.bench.ts | 9 + .../navigation-churn/vue/memory.flame.ts | 4 + .../navigation-churn/vue/project.json | 54 +++ .../scenarios/navigation-churn/vue/setup.ts | 11 + .../navigation-churn/vue/src/app.tsx | 36 ++ .../navigation-churn/vue/src/routeTree.gen.ts | 77 ++++ .../navigation-churn/vue/src/router.tsx | 17 + .../vue/src/routes/__root.tsx | 9 + .../navigation-churn/vue/src/routes/a.tsx | 14 + .../navigation-churn/vue/src/routes/b.tsx | 14 + .../navigation-churn/vue/tsconfig.json | 19 + .../navigation-churn/vue/vite.config.ts | 36 ++ .../scenarios/preload-churn/react/setup.ts | 259 +------------ .../client/scenarios/preload-churn/shared.ts | 308 ++++++++++++++++ .../preload-churn/solid/memory.bench.ts | 9 + .../preload-churn/solid/memory.flame.ts | 4 + .../preload-churn/solid/project.json | 54 +++ .../scenarios/preload-churn/solid/setup.ts | 11 + .../scenarios/preload-churn/solid/src/app.tsx | 31 ++ .../preload-churn/solid/src/item-payload.ts | 51 +++ .../preload-churn/solid/src/routeTree.gen.ts | 77 ++++ .../preload-churn/solid/src/router.tsx | 18 + .../preload-churn/solid/src/routes/__root.tsx | 9 + .../preload-churn/solid/src/routes/index.tsx | 9 + .../solid/src/routes/items.$id.tsx | 20 ++ .../preload-churn/solid/tsconfig.json | 19 + .../preload-churn/solid/vite.config.ts | 34 ++ .../preload-churn/vue/memory.bench.ts | 9 + .../preload-churn/vue/memory.flame.ts | 4 + .../scenarios/preload-churn/vue/project.json | 54 +++ .../scenarios/preload-churn/vue/setup.ts | 11 + .../scenarios/preload-churn/vue/src/app.tsx | 38 ++ .../preload-churn/vue/src/item-payload.ts | 51 +++ .../preload-churn/vue/src/routeTree.gen.ts | 77 ++++ .../preload-churn/vue/src/router.tsx | 18 + .../preload-churn/vue/src/routes/__root.tsx | 9 + .../preload-churn/vue/src/routes/index.tsx | 9 + .../vue/src/routes/items.$id.tsx | 20 ++ .../scenarios/preload-churn/vue/tsconfig.json | 19 + .../preload-churn/vue/vite.config.ts | 36 ++ .../unique-location-churn/react/setup.ts | 134 +------ .../scenarios/unique-location-churn/shared.ts | 165 +++++++++ .../solid/memory.bench.ts | 9 + .../solid/memory.flame.ts | 4 + .../unique-location-churn/solid/project.json | 54 +++ .../unique-location-churn/solid/setup.ts | 11 + .../unique-location-churn/solid/src/app.tsx | 29 ++ .../solid/src/routeTree.gen.ts | 59 +++ .../solid/src/router.tsx | 17 + .../solid/src/routes/__root.tsx | 9 + .../solid/src/routes/items.$id.tsx | 28 ++ .../unique-location-churn/solid/tsconfig.json | 19 + .../solid/vite.config.ts | 34 ++ .../unique-location-churn/vue/memory.bench.ts | 9 + .../unique-location-churn/vue/memory.flame.ts | 4 + .../unique-location-churn/vue/project.json | 54 +++ .../unique-location-churn/vue/setup.ts | 11 + .../unique-location-churn/vue/src/app.tsx | 36 ++ .../vue/src/routeTree.gen.ts | 59 +++ .../unique-location-churn/vue/src/router.tsx | 17 + .../vue/src/routes/__root.tsx | 9 + .../vue/src/routes/items.$id.tsx | 34 ++ .../unique-location-churn/vue/tsconfig.json | 19 + .../unique-location-churn/vue/vite.config.ts | 36 ++ .../memory/client/vitest.solid.config.ts | 9 + benchmarks/memory/client/vitest.vue.config.ts | 9 + benchmarks/memory/server/package.json | 158 +++++++- .../scenarios/aborted-requests/react/setup.ts | 156 +------- .../scenarios/aborted-requests/shared.ts | 309 ++++++++++++++++ .../aborted-requests/solid/memory.bench.ts | 10 + .../aborted-requests/solid/memory.flame.ts | 4 + .../aborted-requests/solid/project.json | 54 +++ .../scenarios/aborted-requests/solid/setup.ts | 14 + .../solid/src/routeTree.gen.ts | 86 +++++ .../aborted-requests/solid/src/router.tsx | 16 + .../solid/src/routes/__root.tsx | 29 ++ .../solid/src/routes/index.tsx | 9 + .../solid/src/routes/stream.$id.tsx | 133 +++++++ .../aborted-requests/solid/tsconfig.json | 17 + .../aborted-requests/solid/vite.config.ts | 34 ++ .../aborted-requests/vue/memory.bench.ts | 10 + .../aborted-requests/vue/memory.flame.ts | 4 + .../aborted-requests/vue/project.json | 54 +++ .../scenarios/aborted-requests/vue/setup.ts | 14 + .../aborted-requests/vue/src/routeTree.gen.ts | 86 +++++ .../aborted-requests/vue/src/router.tsx | 16 + .../vue/src/routes/__root.tsx | 29 ++ .../aborted-requests/vue/src/routes/index.tsx | 9 + .../vue/src/routes/stream.$id.tsx | 130 +++++++ .../aborted-requests/vue/tsconfig.json | 17 + .../aborted-requests/vue/vite.config.ts | 29 ++ .../scenarios/error-paths/react/setup.ts | 204 +---------- .../server/scenarios/error-paths/shared.ts | 211 +++++++++++ .../error-paths/solid/memory.bench.ts | 10 + .../error-paths/solid/memory.flame.ts | 4 + .../scenarios/error-paths/solid/project.json | 54 +++ .../scenarios/error-paths/solid/setup.ts | 14 + .../error-paths/solid/src/routeTree.gen.ts | 146 ++++++++ .../error-paths/solid/src/router.tsx | 23 ++ .../error-paths/solid/src/routes/__root.tsx | 29 ++ .../error-paths/solid/src/routes/boom.$id.tsx | 17 + .../error-paths/solid/src/routes/from.$id.tsx | 14 + .../error-paths/solid/src/routes/index.tsx | 9 + .../solid/src/routes/missing.$id.tsx | 19 + .../solid/src/routes/target.$id.tsx | 11 + .../scenarios/error-paths/solid/tsconfig.json | 17 + .../error-paths/solid/vite.config.ts | 34 ++ .../scenarios/error-paths/vue/memory.bench.ts | 10 + .../scenarios/error-paths/vue/memory.flame.ts | 4 + .../scenarios/error-paths/vue/project.json | 54 +++ .../server/scenarios/error-paths/vue/setup.ts | 14 + .../error-paths/vue/src/routeTree.gen.ts | 146 ++++++++ .../scenarios/error-paths/vue/src/router.tsx | 16 + .../error-paths/vue/src/routes/__root.tsx | 29 ++ .../error-paths/vue/src/routes/boom.$id.tsx | 17 + .../error-paths/vue/src/routes/from.$id.tsx | 14 + .../error-paths/vue/src/routes/index.tsx | 9 + .../vue/src/routes/missing.$id.tsx | 19 + .../error-paths/vue/src/routes/target.$id.tsx | 11 + .../scenarios/error-paths/vue/tsconfig.json | 17 + .../scenarios/error-paths/vue/vite.config.ts | 29 ++ .../scenarios/peak-large-page/react/setup.ts | 67 +--- .../scenarios/peak-large-page/shared.ts | 75 ++++ .../peak-large-page/solid/memory.bench.ts | 10 + .../peak-large-page/solid/memory.flame.ts | 4 + .../peak-large-page/solid/project.json | 54 +++ .../scenarios/peak-large-page/solid/setup.ts | 14 + .../solid/src/large-page-data.ts | 111 ++++++ .../solid/src/routeTree.gen.ts | 305 ++++++++++++++++ .../peak-large-page/solid/src/router.tsx | 16 + .../solid/src/routes/__root.tsx | 29 ++ .../solid/src/routes/index.tsx | 9 + .../src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 25 ++ .../solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 26 ++ .../solid/src/routes/l1.l2.l3.l4.l5.l6.tsx | 26 ++ .../solid/src/routes/l1.l2.l3.l4.l5.tsx | 26 ++ .../solid/src/routes/l1.l2.l3.l4.tsx | 26 ++ .../solid/src/routes/l1.l2.l3.tsx | 26 ++ .../solid/src/routes/l1.l2.tsx | 26 ++ .../peak-large-page/solid/src/routes/l1.tsx | 26 ++ .../peak-large-page/solid/tsconfig.json | 17 + .../peak-large-page/solid/vite.config.ts | 34 ++ .../peak-large-page/vue/memory.bench.ts | 10 + .../peak-large-page/vue/memory.flame.ts | 4 + .../peak-large-page/vue/project.json | 54 +++ .../scenarios/peak-large-page/vue/setup.ts | 14 + .../vue/src/large-page-data.ts | 111 ++++++ .../peak-large-page/vue/src/routeTree.gen.ts | 305 ++++++++++++++++ .../peak-large-page/vue/src/router.tsx | 16 + .../peak-large-page/vue/src/routes/__root.tsx | 29 ++ .../peak-large-page/vue/src/routes/index.tsx | 9 + .../src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 25 ++ .../vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 26 ++ .../vue/src/routes/l1.l2.l3.l4.l5.l6.tsx | 26 ++ .../vue/src/routes/l1.l2.l3.l4.l5.tsx | 26 ++ .../vue/src/routes/l1.l2.l3.l4.tsx | 26 ++ .../vue/src/routes/l1.l2.l3.tsx | 26 ++ .../peak-large-page/vue/src/routes/l1.l2.tsx | 26 ++ .../peak-large-page/vue/src/routes/l1.tsx | 26 ++ .../peak-large-page/vue/tsconfig.json | 17 + .../peak-large-page/vue/vite.config.ts | 29 ++ .../scenarios/request-churn/react/setup.ts | 80 +---- .../server/scenarios/request-churn/shared.ts | 89 +++++ .../request-churn/solid/memory.bench.ts | 10 + .../request-churn/solid/memory.flame.ts | 4 + .../request-churn/solid/project.json | 54 +++ .../scenarios/request-churn/solid/setup.ts | 14 + .../request-churn/solid/src/routeTree.gen.ts | 86 +++++ .../request-churn/solid/src/router.tsx | 16 + .../request-churn/solid/src/routes/__root.tsx | 29 ++ .../request-churn/solid/src/routes/index.tsx | 9 + .../solid/src/routes/items.$id.tsx | 40 +++ .../request-churn/solid/tsconfig.json | 17 + .../request-churn/solid/vite.config.ts | 34 ++ .../request-churn/vue/memory.bench.ts | 10 + .../request-churn/vue/memory.flame.ts | 4 + .../scenarios/request-churn/vue/project.json | 54 +++ .../scenarios/request-churn/vue/setup.ts | 14 + .../request-churn/vue/src/routeTree.gen.ts | 86 +++++ .../request-churn/vue/src/router.tsx | 16 + .../request-churn/vue/src/routes/__root.tsx | 29 ++ .../request-churn/vue/src/routes/index.tsx | 9 + .../vue/src/routes/items.$id.tsx | 40 +++ .../scenarios/request-churn/vue/tsconfig.json | 17 + .../request-churn/vue/vite.config.ts | 29 ++ .../serialization-payload/react/setup.ts | 98 +---- .../scenarios/serialization-payload/shared.ts | 106 ++++++ .../solid/memory.bench.ts | 10 + .../solid/memory.flame.ts | 4 + .../serialization-payload/solid/project.json | 54 +++ .../serialization-payload/solid/setup.ts | 14 + .../solid/src/routeTree.gen.ts | 68 ++++ .../solid/src/router.tsx | 16 + .../solid/src/routes/__root.tsx | 29 ++ .../solid/src/routes/data.$id.tsx | 132 +++++++ .../serialization-payload/solid/tsconfig.json | 17 + .../solid/vite.config.ts | 34 ++ .../serialization-payload/vue/memory.bench.ts | 10 + .../serialization-payload/vue/memory.flame.ts | 4 + .../serialization-payload/vue/project.json | 54 +++ .../serialization-payload/vue/setup.ts | 14 + .../vue/src/routeTree.gen.ts | 68 ++++ .../serialization-payload/vue/src/router.tsx | 16 + .../vue/src/routes/__root.tsx | 29 ++ .../vue/src/routes/data.$id.tsx | 132 +++++++ .../serialization-payload/vue/tsconfig.json | 17 + .../serialization-payload/vue/vite.config.ts | 29 ++ .../scenarios/server-fn-churn/react/setup.ts | 228 +----------- .../scenarios/server-fn-churn/shared.ts | 236 ++++++++++++ .../server-fn-churn/solid/memory.bench.ts | 10 + .../server-fn-churn/solid/memory.flame.ts | 4 + .../server-fn-churn/solid/project.json | 54 +++ .../scenarios/server-fn-churn/solid/setup.ts | 14 + .../server-fn-churn/solid/src/fns.ts | 47 +++ .../solid/src/routeTree.gen.ts | 86 +++++ .../server-fn-churn/solid/src/router.tsx | 16 + .../solid/src/routes/__root.tsx | 29 ++ .../solid/src/routes/api.fn-urls.ts | 14 + .../solid/src/routes/index.tsx | 18 + .../server-fn-churn/solid/tsconfig.json | 17 + .../server-fn-churn/solid/vite.config.ts | 34 ++ .../server-fn-churn/vue/memory.bench.ts | 10 + .../server-fn-churn/vue/memory.flame.ts | 4 + .../server-fn-churn/vue/project.json | 54 +++ .../scenarios/server-fn-churn/vue/setup.ts | 14 + .../scenarios/server-fn-churn/vue/src/fns.ts | 47 +++ .../server-fn-churn/vue/src/routeTree.gen.ts | 86 +++++ .../server-fn-churn/vue/src/router.tsx | 16 + .../server-fn-churn/vue/src/routes/__root.tsx | 29 ++ .../vue/src/routes/api.fn-urls.ts | 14 + .../server-fn-churn/vue/src/routes/index.tsx | 18 + .../server-fn-churn/vue/tsconfig.json | 17 + .../server-fn-churn/vue/vite.config.ts | 29 ++ .../scenarios/streaming-peak/react/setup.ts | 138 +------ .../server/scenarios/streaming-peak/shared.ts | 146 ++++++++ .../streaming-peak/solid/memory.bench.ts | 10 + .../streaming-peak/solid/memory.flame.ts | 4 + .../streaming-peak/solid/project.json | 54 +++ .../scenarios/streaming-peak/solid/setup.ts | 14 + .../streaming-peak/solid/src/routeTree.gen.ts | 86 +++++ .../streaming-peak/solid/src/router.tsx | 16 + .../solid/src/routes/__root.tsx | 29 ++ .../streaming-peak/solid/src/routes/index.tsx | 9 + .../solid/src/routes/stream.$id.tsx | 99 +++++ .../streaming-peak/solid/tsconfig.json | 17 + .../streaming-peak/solid/vite.config.ts | 34 ++ .../streaming-peak/vue/memory.bench.ts | 10 + .../streaming-peak/vue/memory.flame.ts | 4 + .../scenarios/streaming-peak/vue/project.json | 54 +++ .../scenarios/streaming-peak/vue/setup.ts | 14 + .../streaming-peak/vue/src/routeTree.gen.ts | 86 +++++ .../streaming-peak/vue/src/router.tsx | 16 + .../streaming-peak/vue/src/routes/__root.tsx | 29 ++ .../streaming-peak/vue/src/routes/index.tsx | 9 + .../vue/src/routes/stream.$id.tsx | 106 ++++++ .../streaming-peak/vue/tsconfig.json | 17 + .../streaming-peak/vue/vite.config.ts | 29 ++ .../memory/server/vitest.solid.config.ts | 9 + benchmarks/memory/server/vitest.vue.config.ts | 9 + package.json | 6 + .../src/ssr/renderRouterToStream.tsx | 42 ++- .../tests/renderRouterToStream.test.tsx | 4 +- pnpm-lock.yaml | 66 +++- 364 files changed, 12888 insertions(+), 1996 deletions(-) create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/project.json create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/fast.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/index.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/routes/slow.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/slow-loaders.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/project.json create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/fast.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/index.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/routes/slow.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/slow-loaders.ts create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/shared.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/project.json create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/loader-data.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.index.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/shell.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/project.json create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/loader-data.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.index.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/shell.tsx create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/shared.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/project.json create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/src/routes/a.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/solid/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/project.json create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/src/routes/a.tsx create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/mount-unmount/vue/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/shared.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/project.json create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/a.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/src/routes/b.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/solid/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/project.json create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/navigation-churn/vue/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/shared.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/project.json create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/item-payload.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/index.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/project.json create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/item-payload.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/index.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/shared.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/project.json create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/src/routes/items.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/solid/vite.config.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/project.json create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/src/app.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/src/router.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/src/routes/items.$id.tsx create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/tsconfig.json create mode 100644 benchmarks/memory/client/scenarios/unique-location-churn/vue/vite.config.ts create mode 100644 benchmarks/memory/client/vitest.solid.config.ts create mode 100644 benchmarks/memory/client/vitest.vue.config.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/shared.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/project.json create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/solid/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/project.json create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/vue/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/shared.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/project.json create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/setup.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/boom.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/from.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/missing.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/src/routes/target.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/error-paths/solid/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/project.json create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/setup.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/boom.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/from.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/missing.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/src/routes/target.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/error-paths/vue/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/shared.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/project.json create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/large-page-data.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/solid/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/project.json create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/shared.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/project.json create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/setup.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/src/routes/items.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/request-churn/solid/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/project.json create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/setup.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/src/routes/items.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/request-churn/vue/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/shared.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/project.json create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/data.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/solid/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/project.json create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/data.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/vue/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/shared.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/project.json create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/api.fn-urls.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/solid/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/project.json create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/api.fn-urls.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/vue/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/shared.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/project.json create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/stream.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/solid/vite.config.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/memory.flame.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/project.json create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/src/routeTree.gen.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/src/router.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/__root.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/index.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/stream.$id.tsx create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/tsconfig.json create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/vue/vite.config.ts create mode 100644 benchmarks/memory/server/vitest.solid.config.ts create mode 100644 benchmarks/memory/server/vitest.vue.config.ts diff --git a/.github/workflows/client-nav-benchmarks.yml b/.github/workflows/client-nav-benchmarks.yml index 8ccb0d12cc..eae3cd7448 100644 --- a/.github/workflows/client-nav-benchmarks.yml +++ b/.github/workflows/client-nav-benchmarks.yml @@ -47,16 +47,6 @@ jobs: mode: memory - benchmark: memory-client mode: memory - # TODO: temp exclude memory benchmarks for solid and vue until we have them working - exclude: - - benchmark: memory-server - framework: solid - - benchmark: memory-server - framework: vue - - benchmark: memory-client - framework: solid - - benchmark: memory-client - framework: vue runs-on: ubuntu-latest steps: - name: Checkout diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md index 9cea416f0b..511acc6693 100644 --- a/benchmarks/memory/README.md +++ b/benchmarks/memory/README.md @@ -4,24 +4,25 @@ 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 Start apps, requests against +- `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 apps in jsdom. +- `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. React-first; each scenario keeps a `react/` level so solid/vue can -be added later without renames. +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:react, test:perf:react, test:flame:react, test:types + 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.react.config.ts aggregates scenarios/*/react/vite.config.ts - scenarios//react/ one isolated app per scenario + setup.ts + memory.bench.ts + memory.flame.ts + 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 @@ -91,9 +92,8 @@ generation; `memory.bench.ts` and `memory.flame.ts` are thin runners only. 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, because React schedules - stream flushes via `setImmediate` and any non-timer deferral wins that race, - suppressing the Suspense fallbacks the scenario exists to stream. + 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 @@ -101,7 +101,7 @@ generation; `memory.bench.ts` and `memory.flame.ts` are thin runners only. 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 - (React root, `__TSR_ROUTER__`, `history.destroy()`); large loader payloads + (framework root, `__TSR_ROUTER__`, `history.destroy()`); large loader payloads are never rendered into the DOM. - `NODE_ENV=production` everywhere (the Nx targets set it). @@ -112,7 +112,11 @@ 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 ``` @@ -130,6 +134,10 @@ 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: @@ -169,5 +177,9 @@ 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/package.json b/benchmarks/memory/client/package.json index 534225d2a3..d7fd463bde 100644 --- a/benchmarks/memory/client/package.json +++ b/benchmarks/memory/client/package.json @@ -22,8 +22,12 @@ "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" + "react-dom": "^19.0.0", + "solid-js": "^1.9.10", + "vue": "^3.5.16" }, "devDependencies": { "@codspeed/vitest-plugin": "^5.5.0", @@ -31,10 +35,13 @@ "@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": { @@ -56,6 +63,40 @@ } ] }, + "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, @@ -73,6 +114,40 @@ } ] }, + "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, @@ -90,6 +165,40 @@ } ] }, + "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, @@ -102,6 +211,30 @@ "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": [ @@ -112,7 +245,19 @@ "@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-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/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts index d0532e87e6..5a61a00e61 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts @@ -1,18 +1,5 @@ import type * as App from './src/app' -import { - createDeterministicRandom, - randomSegment, -} from '#memory-client/bench-utils' - -type NavigationSettlement = - | { - status: 'fulfilled' - value: void - } - | { - status: 'rejected' - reason: unknown - } +import { createSetup } from '../shared' const appModulePath = './dist/app.js' const { @@ -21,275 +8,13 @@ const { resolveSlowLoader, slowLoaderRegistry, } = (await import(/* @vite-ignore */ appModulePath)) as typeof App -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)}`, - })) -} - -async function drainMicrotasks() { - await Promise.resolve() - await Promise.resolve() -} - -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 setup() { - if (process.env.NODE_ENV !== 'production') { - console.warn( - 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', - ) - } - - let container: HTMLDivElement | undefined = undefined - let unmount: (() => void) | undefined = undefined - let unsub = () => {} - let resolveRendered: () => void = () => {} - 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 new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) - } - } - - 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 = document.createElement('div') - document.body.append(container) - - const mounted = mountTestApp(container) - const { router } = mounted - unmount = mounted.unmount - getLatestLoadPromise = () => router.latestLoadPromise - - unsub = router.subscribe('onRendered', (event) => { - if ( - expectedRenderedPath && - event.toLocation.pathname !== expectedRenderedPath - ) { - return - } - - const resolve = resolveRendered - resolveRendered = () => {} - 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): NavigationSettlement => ({ status: 'rejected', reason }), - ) - } - - await router.load() - await waitForRenderedPage('shell') - } - - function after() { - resolveAllSlowLoaders() - unmount?.() - container?.remove() - unsub() - - container = undefined - unmount = undefined - unsub = () => {} - resolveRendered = () => {} - expectedRenderedPath = undefined - navigateFast = uninitialized - startSlowNavigation = uninitializedSettlement - getLatestLoadPromise = () => undefined - } - - async function interrupt( - slowId: string, - fastId: string, - assertShape = false, - ) { - 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 - assertSlowNavigationSettlement(settlement) - await awaitExpectedLoadSettlement(slowLoadPromise) - await drainMicrotasks() - - if (assertShape) { - assertRenderedPage('fast', fastId) - } - } - - return { - name: 'mem interrupted-navigations (react)', - 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', true) - } finally { - after() - } - }, - after, - } + return createSetup( + 'react', + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, + ) } 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..ede8ad2bc0 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts @@ -0,0 +1,339 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' + +type Framework = 'react' | 'solid' | 'vue' + +type NavigationSettlement = + | { + status: 'fulfilled' + value: void + } + | { + status: 'rejected' + reason: unknown + } + +type MountedApp = { + router: unknown + unmount: () => void +} + +type MountTestApp = (container: HTMLDivElement) => MountedApp +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 frameworkNames = { + react: 'React', + solid: 'Solid', + vue: 'Vue', +} satisfies Record +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 warnDevMode(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.`, + ) + } +} + +function createInterruptedNavigationPairs(iterations: number) { + const random = createDeterministicRandom(13) + + return Array.from({ length: iterations }, (_, index) => ({ + slowId: `slow-${index}-${randomSegment(random)}`, + fastId: `fast-${index}-${randomSegment(random)}`, + })) +} + +async function drainMicrotasks() { + await Promise.resolve() + await Promise.resolve() +} + +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 createSetup( + framework: Framework, + mountTestApp: MountTestApp, + resolveAllSlowLoaders: ResolveAllSlowLoaders, + resolveSlowLoader: ResolveSlowLoader, + slowLoaderRegistry: SlowLoaderRegistry, +) { + warnDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let resolveRendered: () => void = () => {} + 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 new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + 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 = document.createElement('div') + document.body.append(container) + + 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 = () => {} + 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?.() + container?.remove() + unsub() + + container = undefined + unmount = undefined + unsub = () => {} + resolveRendered = () => {} + expectedRenderedPath = undefined + navigateFast = uninitialized + startSlowNavigation = uninitializedSettlement + getLatestLoadPromise = () => undefined + } + + async function interrupt( + slowId: string, + fastId: string, + assertShape = false, + ) { + 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 + assertSlowNavigationSettlement(settlement) + await awaitExpectedLoadSettlement(slowLoadPromise) + await drainMicrotasks() + + if (assertShape) { + assertRenderedPage('fast', fastId) + } + } + + 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', true) + } finally { + after() + } + }, + after, + } +} 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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..28deb89580 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts @@ -0,0 +1,20 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} = (await import(/* @vite-ignore */ appModulePath)) as typeof App + +export function setup() { + return createSetup( + '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..47aa9d8a37 --- /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..d1871d287b --- /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..1e645569b7 --- /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/src/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/slow-loaders.ts new file mode 100644 index 0000000000..c70ccac3da --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/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/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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..fad57cfc8c --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts @@ -0,0 +1,20 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +} = (await import(/* @vite-ignore */ appModulePath)) as typeof App + +export function setup() { + return createSetup( + '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..4257601dbd --- /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..ff5e828909 --- /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..5ac5b44d44 --- /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/src/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/slow-loaders.ts new file mode 100644 index 0000000000..c70ccac3da --- /dev/null +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/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/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/react/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts index 934f37c1bf..1f84970d14 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts @@ -1,169 +1,11 @@ import type * as App from './src/app' -import { - createDeterministicRandom, - randomSegment, -} from '#memory-client/bench-utils' +import { createSetup } from '../shared' const appModulePath = './dist/app.js' const { loaderPayloadRecordCount, mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -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 setup() { - if (process.env.NODE_ENV !== 'production') { - console.warn( - 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', - ) - } - - let container: HTMLDivElement | undefined = undefined - let unmount: (() => void) | undefined = undefined - let unsub = () => {} - let resolveRendered: () => void = () => {} - 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 new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) - } - } - - assertRenderedShell() - } - - function waitForNextRender(pathname: string) { - expectedRenderedPath = pathname - - return new Promise((resolve) => { - resolveRendered = resolve - }) - } - - async function before() { - if (container) { - after() - } - - container = document.createElement('div') - document.body.append(container) - - const mounted = mountTestApp(container) - const { router } = mounted - unmount = mounted.unmount - - unsub = router.subscribe('onRendered', (event) => { - if ( - expectedRenderedPath && - event.toLocation.pathname !== expectedRenderedPath - ) { - return - } - - const resolve = resolveRendered - resolveRendered = () => {} - 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?.() - container?.remove() - unsub() - - container = undefined - unmount = undefined - unsub = () => {} - resolveRendered = () => {} - expectedRenderedPath = undefined - navigateTo = uninitialized - } - - return { - name: 'mem loader-data-retention (react)', - before, - navigate: (id: string) => navigateTo(id), - async run() { - for (const id of pageIds) { - await navigateTo(id) - } - }, - async sanity() { - await before() - - try { - await navigateTo('sanity-a') - assertRenderedPage('sanity-a') - await navigateTo('sanity-b') - assertRenderedPage('sanity-b') - } finally { - after() - } - }, - after, - } + return createSetup('react', mountTestApp, loaderPayloadRecordCount) } 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..06c8f6eecd --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts @@ -0,0 +1,205 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' + +type Framework = 'react' | 'solid' | 'vue' + +type MountedApp = { + router: unknown + unmount: () => void +} + +type MountTestApp = (container: HTMLDivElement) => MountedApp + +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 frameworkNames = { + react: 'React', + solid: 'Solid', + vue: 'Vue', +} satisfies Record +const loaderDataRetentionNavigationCount = 20 +const pageIds = createPageIds() + +const uninitialized = () => + Promise.reject( + new Error('loader-data-retention benchmark is not initialized'), + ) + +function warnDevMode(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.`, + ) + } +} + +function createPageIds() { + const random = createDeterministicRandom(11) + + return Array.from( + { length: loaderDataRetentionNavigationCount }, + (_, index) => `${index}-${randomSegment(random)}`, + ) +} + +export function createSetup( + framework: Framework, + mountTestApp: MountTestApp, + loaderPayloadRecordCount: number, +) { + warnDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let resolveRendered: () => void = () => {} + 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 new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + assertRenderedShell() + } + + function waitForNextRender(pathname: string) { + expectedRenderedPath = pathname + + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = document.createElement('div') + document.body.append(container) + + 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 = () => {} + 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?.() + container?.remove() + unsub() + + container = undefined + unmount = undefined + unsub = () => {} + resolveRendered = () => {} + 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 { + await navigateTo('sanity-a') + assertRenderedPage('sanity-a') + await navigateTo('sanity-b') + assertRenderedPage('sanity-b') + } 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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..56a22a4ac5 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts @@ -0,0 +1,11 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { loaderPayloadRecordCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createSetup('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..453f50f330 --- /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/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/loader-data.ts new file mode 100644 index 0000000000..3bd59c82ef --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/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/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..3968848038 --- /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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..a1701d8dbf --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts @@ -0,0 +1,11 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { loaderPayloadRecordCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createSetup('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..e9d54137ae --- /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/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/loader-data.ts new file mode 100644 index 0000000000..3bd59c82ef --- /dev/null +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/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/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..570ea3fb68 --- /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/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts index 203dc39d37..7083cd1432 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts @@ -1,73 +1,11 @@ import type * as App from './src/app' +import { createSetup } from '../shared' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -const mountUnmountIterations = 100 - -function drainMicrotasks() { - return Promise.resolve().then(() => Promise.resolve()) -} - -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 setup() { - if (process.env.NODE_ENV !== 'production') { - console.warn( - 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', - ) - } - - async function cycle() { - const container = document.createElement('div') - document.body.append(container) - - let unmount = () => {} - let unsubscribe = () => {} - - try { - const mounted = mountTestApp(container) - const { router } = mounted - unmount = mounted.unmount - - const rendered = new Promise((resolve) => { - unsubscribe = router.subscribe('onRendered', () => { - resolve() - }) - }) - - await router.load() - await rendered - unsubscribe() - unsubscribe = () => {} - } finally { - unmount() - container.remove() - unsubscribe() - await drainMicrotasks() - } - } - - return { - name: 'mem mount-unmount (react)', - cycle, - async run() { - for (let index = 0; index < mountUnmountIterations; index++) { - await cycle() - } - }, - async sanity() { - assertEmptyBody() - await cycle() - await cycle() - assertEmptyBody() - }, - } + return createSetup('react', mountTestApp) } 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..1cc57712cd --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts @@ -0,0 +1,90 @@ +type Framework = 'react' | 'solid' | 'vue' + +type MountedApp = { + router: unknown + unmount: () => void +} + +type MountTestApp = (container: HTMLDivElement) => MountedApp + +type RenderRouter = { + load: () => Promise + subscribe: (event: 'onRendered', listener: () => void) => () => void +} + +const frameworkNames = { + react: 'React', + solid: 'Solid', + vue: 'Vue', +} satisfies Record +const mountUnmountIterations = 100 + +function warnDevMode(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.`, + ) + } +} + +function drainMicrotasks() { + return Promise.resolve().then(() => Promise.resolve()) +} + +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 createSetup(framework: Framework, mountTestApp: MountTestApp) { + warnDevMode(framework) + + async function cycle() { + const container = document.createElement('div') + document.body.append(container) + + let unmount = () => {} + let unsubscribe = () => {} + + 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 = () => {} + } finally { + unmount() + container.remove() + 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() + 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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..73e5f8949e --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts @@ -0,0 +1,11 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createSetup('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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..0f76d289d4 --- /dev/null +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts @@ -0,0 +1,11 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createSetup('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/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts index deac1d1bbf..7083cd1432 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts @@ -1,124 +1,11 @@ import type * as App from './src/app' - -type Target = '/a' | '/b' +import { createSetup } from '../shared' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -const navigationChurnIterations = 300 - -const uninitialized = () => - Promise.reject(new Error('navigation-churn benchmark is not initialized')) export function setup() { - if (process.env.NODE_ENV !== 'production') { - console.warn( - 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', - ) - } - - let container: HTMLDivElement | undefined = undefined - let unmount: (() => void) | undefined = undefined - let unsub = () => {} - let resolveRendered: () => void = () => {} - 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 new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) - } - } - - assertRenderedPage(target) - } - - function waitForNextRender() { - return new Promise((resolve) => { - resolveRendered = resolve - }) - } - - async function before() { - if (container) { - after() - } - - container = document.createElement('div') - document.body.append(container) - - const mounted = mountTestApp(container) - const { router } = mounted - 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?.() - container?.remove() - unsub() - - container = undefined - unmount = undefined - unsub = () => {} - resolveRendered = () => {} - navigateTo = uninitialized - } - - return { - name: 'mem navigation-churn (react)', - 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, - } + return createSetup('react', mountTestApp) } 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..ab36c48767 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts @@ -0,0 +1,141 @@ +type Framework = 'react' | 'solid' | 'vue' +type Target = '/a' | '/b' + +type MountedApp = { + router: unknown + unmount: () => void +} + +type MountTestApp = (container: HTMLDivElement) => MountedApp + +type NavigationRouter = { + load: () => Promise + navigate: (options: { to: Target; replace: true }) => Promise + subscribe: (event: 'onRendered', listener: () => void) => () => void +} + +const frameworkNames = { + react: 'React', + solid: 'Solid', + vue: 'Vue', +} satisfies Record +const navigationChurnIterations = 300 + +const uninitialized = () => + Promise.reject(new Error('navigation-churn benchmark is not initialized')) + +function warnDevMode(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 createSetup(framework: Framework, mountTestApp: MountTestApp) { + warnDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let resolveRendered: () => void = () => {} + 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 new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + assertRenderedPage(target) + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = document.createElement('div') + document.body.append(container) + + 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?.() + container?.remove() + unsub() + + container = undefined + unmount = undefined + unsub = () => {} + resolveRendered = () => {} + 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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..73e5f8949e --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts @@ -0,0 +1,11 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createSetup('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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..0f76d289d4 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts @@ -0,0 +1,11 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createSetup('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..a1537dc324 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx @@ -0,0 +1,14 @@ +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..ce19d1bd10 --- /dev/null +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx @@ -0,0 +1,14 @@ +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/react/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts index a1737c644e..d7c39e5e01 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts @@ -1,266 +1,11 @@ import type * as App from './src/app' -import { - createDeterministicRandom, - randomSegment, -} from '#memory-client/bench-utils' +import { createSetup } from '../shared' const appModulePath = './dist/app.js' const { getTrackedItemLoaderCount, mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -type MountedApp = ReturnType - -// 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') -} - -async function drainMicrotasks() { - await Promise.resolve() - await Promise.resolve() -} - export function setup() { - if (process.env.NODE_ENV !== 'production') { - console.warn( - 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', - ) - } - - let container: HTMLDivElement | undefined = undefined - let router: MountedApp['router'] | undefined = undefined - let unmount: (() => void) | undefined = undefined - let unsub = () => {} - let resolveRendered: () => void = () => {} - 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}`) - } - } - - function assertRenderedItem(id: string) { - const page = - container?.querySelector('[data-bench-page]')?.dataset - .benchPage - const actualId = - container?.querySelector('[data-bench-id]')?.dataset.benchId - - if (page !== 'item' || actualId !== id) { - throw new Error(`Expected rendered item ${id}, got ${page}:${actualId}`) - } - } - - function hasCachedItemMatch(id: string) { - return Boolean( - router?.stores.cachedMatches - .get() - .some((match) => (match.params as { id?: string }).id === id), - ) - } - - async function waitForRenderedIndex() { - for (let attempt = 0; attempt < 10; attempt++) { - try { - assertRenderedIndex() - return - } catch { - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) - } - } - - assertRenderedIndex() - } - - async function waitForRenderedItem(id: string) { - for (let attempt = 0; attempt < 10; attempt++) { - try { - assertRenderedItem(id) - return - } catch { - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) - } - } - - assertRenderedItem(id) - } - - function waitForNextRender() { - return new Promise((resolve) => { - resolveRendered = resolve - }) - } - - async function before() { - if (container) { - after() - } - - container = document.createElement('div') - document.body.append(container) - - const mounted = mountTestApp(container) - router = mounted.router - 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?.() - container?.remove() - unsub() - - container = undefined - router = undefined - unmount = undefined - unsub = () => {} - resolveRendered = () => {} - 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 (react)', - 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}`, - ) - } - - if (!hasCachedItemMatch(id)) { - throw new Error( - 'Expected preloaded match to sit in router.state.cachedMatches', - ) - } - - // A navigation commit runs clearExpiredCache; with - // defaultPreloadGcTime: 0 it must evict the preloaded match. This is - // the mechanism the bench's flat-floor expectation rests on. - await navigateToItem('sanity-evict-nav') - await waitForRenderedItem('sanity-evict-nav') - - if (hasCachedItemMatch(id)) { - throw new Error( - 'Expected the navigation commit to evict the preloaded match (preloadGcTime 0)', - ) - } - - await preloadItem(id) - - const repreloadedLoaderCount = getTrackedItemLoaderCount(id) - if (repreloadedLoaderCount !== preloadedLoaderCount + 1) { - throw new Error( - 'Expected re-preload after eviction to run the item loader again', - ) - } - } finally { - after() - } - }, - after, - } + return createSetup('react', mountTestApp, getTrackedItemLoaderCount) } 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..09784fc311 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/shared.ts @@ -0,0 +1,308 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' + +type Framework = 'react' | 'solid' | 'vue' + +type MountedApp = { + router: unknown + unmount: () => void +} + +type MountTestApp = (container: HTMLDivElement) => MountedApp +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 + stores: { + cachedMatches: { + get: () => unknown + } + } +} + +const frameworkNames = { + react: 'React', + solid: 'Solid', + vue: 'Vue', +} satisfies Record + +// 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') +} + +function warnDevMode(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.`, + ) + } +} + +async function drainMicrotasks() { + await Promise.resolve() + await Promise.resolve() +} + +export function createSetup( + framework: Framework, + mountTestApp: MountTestApp, + getTrackedItemLoaderCount: GetTrackedItemLoaderCount, +) { + warnDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let router: PreloadRouter | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let resolveRendered: () => void = () => {} + 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}`) + } + } + + function assertRenderedItem(id: string) { + const page = + container?.querySelector('[data-bench-page]')?.dataset + .benchPage + const actualId = + container?.querySelector('[data-bench-id]')?.dataset.benchId + + if (page !== 'item' || actualId !== id) { + throw new Error(`Expected rendered item ${id}, got ${page}:${actualId}`) + } + } + + function hasCachedItemMatch(id: string) { + const cachedMatches = router?.stores.cachedMatches.get() as + | Array<{ params: { id?: string } }> + | undefined + + return Boolean(cachedMatches?.some((match) => match.params.id === id)) + } + + async function waitForRenderedIndex() { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedIndex() + return + } catch { + await new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + assertRenderedIndex() + } + + async function waitForRenderedItem(id: string) { + for (let attempt = 0; attempt < 10; attempt++) { + try { + assertRenderedItem(id) + return + } catch { + await new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + assertRenderedItem(id) + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = document.createElement('div') + document.body.append(container) + + 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?.() + container?.remove() + unsub() + + container = undefined + router = undefined + unmount = undefined + unsub = () => {} + resolveRendered = () => {} + 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}`, + ) + } + + if (!hasCachedItemMatch(id)) { + throw new Error( + 'Expected preloaded match to sit in router.state.cachedMatches', + ) + } + + // A navigation commit runs clearExpiredCache; with + // defaultPreloadGcTime: 0 it must evict the preloaded match. This is + // the mechanism the bench's flat-floor expectation rests on. + await navigateToItem('sanity-evict-nav') + await waitForRenderedItem('sanity-evict-nav') + + if (hasCachedItemMatch(id)) { + throw new Error( + 'Expected the navigation commit to evict the preloaded match (preloadGcTime 0)', + ) + } + + await preloadItem(id) + + const repreloadedLoaderCount = getTrackedItemLoaderCount(id) + if (repreloadedLoaderCount !== preloadedLoaderCount + 1) { + throw new Error( + 'Expected re-preload after eviction to run the item loader again', + ) + } + } 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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..0fdbe25998 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts @@ -0,0 +1,11 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { getTrackedItemLoaderCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createSetup('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..3dfe005e5c --- /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/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/src/item-payload.ts new file mode 100644 index 0000000000..4a86ea7244 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/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/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..158a6213ac --- /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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..ab21eb1030 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts @@ -0,0 +1,11 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { getTrackedItemLoaderCount, mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createSetup('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..8040fc57f9 --- /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/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/src/item-payload.ts new file mode 100644 index 0000000000..4a86ea7244 --- /dev/null +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/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/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..4a39ea7bfd --- /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/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts index 14fbfe7081..7083cd1432 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts @@ -1,141 +1,11 @@ import type * as App from './src/app' -import { - createDeterministicRandom, - randomSegment, -} from '#memory-client/bench-utils' - -type ItemLocation = { - id: string - q: string -} +import { createSetup } from '../shared' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -const benchmarkRandom = createDeterministicRandom(0xdecafbad) -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. -let locationCounter = 0 - -const uninitialized = () => - Promise.reject( - new Error('unique-location-churn benchmark is not initialized'), - ) export function setup() { - if (process.env.NODE_ENV !== 'production') { - console.warn( - 'memory client benchmark is running without NODE_ENV=production; React dev overhead will dominate results.', - ) - } - - let container: HTMLDivElement | undefined = undefined - let unmount: (() => void) | undefined = undefined - let unsub = () => {} - let resolveRendered: () => void = () => {} - 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 new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) - } - } - - assertRenderedId(expected) - } - - function waitForNextRender() { - return new Promise((resolve) => { - resolveRendered = resolve - }) - } - - async function before() { - if (container) { - after() - } - - container = document.createElement('div') - document.body.append(container) - - const mounted = mountTestApp(container) - const { router } = mounted - 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?.() - container?.remove() - unsub() - - container = undefined - unmount = undefined - unsub = () => {} - resolveRendered = () => {} - navigateTo = uninitialized - } - - return { - name: 'mem unique-location-churn (react)', - 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') - await navigateTo({ id: 'sanity-two', q: 'q-sanity-two' }) - assertRenderedId('sanity-two') - } finally { - after() - } - }, - after, - } + return createSetup('react', mountTestApp) } 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..272882540e --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts @@ -0,0 +1,165 @@ +import { + createDeterministicRandom, + randomSegment, +} from '#memory-client/bench-utils' + +type Framework = 'react' | 'solid' | 'vue' + +type ItemLocation = { + id: string + q: string +} + +type MountedApp = { + router: unknown + unmount: () => void +} + +type MountTestApp = (container: HTMLDivElement) => MountedApp + +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 frameworkNames = { + react: 'React', + solid: 'Solid', + vue: 'Vue', +} satisfies Record +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'), + ) + +function warnDevMode(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 createSetup(framework: Framework, mountTestApp: MountTestApp) { + warnDevMode(framework) + + let container: HTMLDivElement | undefined = undefined + let unmount: (() => void) | undefined = undefined + let unsub = () => {} + let resolveRendered: () => void = () => {} + 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 new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + } + } + + assertRenderedId(expected) + } + + function waitForNextRender() { + return new Promise((resolve) => { + resolveRendered = resolve + }) + } + + async function before() { + if (container) { + after() + } + + container = document.createElement('div') + document.body.append(container) + + 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?.() + container?.remove() + unsub() + + container = undefined + unmount = undefined + unsub = () => {} + resolveRendered = () => {} + 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') + await navigateTo({ id: 'sanity-two', q: 'q-sanity-two' }) + assertRenderedId('sanity-two') + } 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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..73e5f8949e --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts @@ -0,0 +1,11 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createSetup('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..5b1c779d71 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts @@ -0,0 +1,9 @@ +import { describe } from 'vitest' +import { registerClientMemoryBench } from '#memory-client/runner' +import { setup } from './setup' + +await setup().sanity() + +describe('memory', () => { + registerClientMemoryBench(setup()) +}) 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..b464c772b0 --- /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 { setup } from './setup.ts' + +await runClientFlameBenchmark(setup) 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..0f76d289d4 --- /dev/null +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts @@ -0,0 +1,11 @@ +import type * as App from './src/app' +import { createSetup } from '../shared' + +const appModulePath = './dist/app.js' +const { mountTestApp } = (await import( + /* @vite-ignore */ appModulePath +)) as typeof App + +export function setup() { + return createSetup('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/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/server/package.json b/benchmarks/memory/server/package.json index 2427549e87..d898755beb 100644 --- a/benchmarks/memory/server/package.json +++ b/benchmarks/memory/server/package.json @@ -13,16 +13,24 @@ "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" + "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": { @@ -45,6 +53,42 @@ } ] }, + "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, @@ -63,6 +107,42 @@ } ] }, + "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, @@ -81,6 +161,42 @@ } ] }, + "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, @@ -93,6 +209,30 @@ "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": [ @@ -104,7 +244,21 @@ "@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-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/react/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts index b3f1fc047b..66aed269b9 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts @@ -1,147 +1,7 @@ -import type { StartRequestHandler } from '#memory-server/bench-utils' +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -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 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 drainCancellationMicrotasks() { - await Promise.resolve() - await Promise.resolve() -} - -async function assertAbortedRequestsSanity(handler: StartRequestHandler) { - 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') - } - - for (const marker of [ - alphaFirstRecord(fullId), - alphaLastRecord(fullId), - betaFirstRecord(fullId), - betaLastRecord(fullId), - ]) { - if (!fullBody.includes(marker)) { - throw new Error(`Expected full sanity response to include ${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, value } = await readFirstChunk( - midStreamResponse, - midStreamRequest, - ) - const text = textDecoder.decode(value) - - if (!text.includes(eagerMarker)) { - throw new Error('Expected first sanity chunk to include the eager marker') - } - - if ( - !text.includes(alphaFallbackMarker) || - !text.includes(betaFallbackMarker) - ) { - throw new Error('Expected first sanity chunk to include deferred fallbacks') - } - - for (const marker of [ - alphaFirstRecord(midStreamId), - alphaLastRecord(midStreamId), - betaFirstRecord(midStreamId), - betaLastRecord(midStreamId), - ]) { - if (text.includes(marker)) { - throw new Error( - `First sanity chunk already included deferred content ${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 reader.cancel() - await drainCancellationMicrotasks() -} - -async function runAbortedRequestLoop(handler: StartRequestHandler) { - 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 readFirstChunk(response, request) - controller.abort() - await reader.cancel() - await drainCancellationMicrotasks() - } -} export async function setup() { const { default: handler } = (await import( @@ -150,15 +10,5 @@ export async function setup() { default: StartRequestHandler } - const run = () => runAbortedRequestLoop(handler) - - return { - sanity: () => assertAbortedRequestsSanity(handler), - benches: [ - { - name: 'mem aborted-requests (react)', - run, - }, - ], - } + return createSetup('react', handler) } 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..efdfd6e139 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts @@ -0,0 +1,309 @@ +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 sanity chunks already included deferred content ${marker}`, + ) + } + } + + if ( + text.includes(eagerMarker) && + text.includes(alphaFallbackMarker) && + text.includes(betaFallbackMarker) + ) { + return { + reader, + text, + } + } + } +} + +async function readSanityStream( + mode: AbortedRequestReadMode, + response: Response, + request: Request, + id: string, +) { + if (mode === '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') + } + + for (const marker of [ + alphaFirstRecord(fullId), + alphaLastRecord(fullId), + betaFirstRecord(fullId), + betaLastRecord(fullId), + ]) { + if (!fullBody.includes(marker)) { + throw new Error(`Expected full sanity response to include ${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( + mode.readMode, + midStreamResponse, + midStreamRequest, + midStreamId, + ) + + if (!text.includes(eagerMarker)) { + throw new Error('Expected first sanity chunk to include the eager marker') + } + + if ( + !text.includes(alphaFallbackMarker) || + !text.includes(betaFallbackMarker) + ) { + throw new Error('Expected first sanity chunk to include deferred fallbacks') + } + + for (const marker of [ + alphaFirstRecord(midStreamId), + alphaLastRecord(midStreamId), + betaFirstRecord(midStreamId), + betaLastRecord(midStreamId), + ]) { + if (text.includes(marker)) { + throw new Error( + `First sanity chunk already included deferred content ${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 createSetup( + framework: Framework, + handler: StartRequestHandler, +) { + const mode = abortedRequestModes[framework] + const run = () => runAbortedRequestLoop(handler, mode) + + return { + sanity: () => assertAbortedRequestsSanity(handler, mode), + benches: [ + { + 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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..0a575854b5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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..0d723098c8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/src/routes/stream.$id.tsx @@ -0,0 +1,133 @@ +import { createFileRoute } from '@tanstack/solid-router' +import { Show, Suspense, createResource } from 'solid-js' + +const recordCount = 20 +const alphaDelayMs = 50 +const betaDelayMs = 75 +const abortProbeAlphaDelayMs = 500 +const abortProbeBetaDelayMs = 750 + +type RecordGroup = 'alpha' | 'beta' + +export interface DeferredRecord { + id: string + label: string +} + +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, + () => makeRecords(id, group), + () => [], + ) +} + +function makeRecords(id: string, group: RecordGroup): Array { + return Array.from({ length: recordCount }, (_, index) => ({ + id: `${group}-${id}-${index}`, + label: `deferred-${group}-${id}-${index}`, + })) +} + +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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..df2908aec9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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..8b2676523d --- /dev/null +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/src/routes/stream.$id.tsx @@ -0,0 +1,130 @@ +import { Await, createFileRoute } from '@tanstack/vue-router' +import { Suspense } from 'vue' + +const recordCount = 20 +const alphaDelayMs = 50 +const betaDelayMs = 75 +const abortProbeAlphaDelayMs = 500 +const abortProbeBetaDelayMs = 750 + +type RecordGroup = 'alpha' | 'beta' + +export interface DeferredRecord { + id: string + label: string +} + +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, + () => makeRecords(id, group), + () => [], + ) +} + +function makeRecords(id: string, group: RecordGroup): Array { + return Array.from({ length: recordCount }, (_, index) => ({ + id: `${group}-${id}-${index}`, + label: `deferred-${group}-${id}-${index}`, + })) +} + +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/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts index 3e5b658ff6..66aed269b9 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts +++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts @@ -1,153 +1,7 @@ -import { - createDeterministicRandom, - randomSegment, - runSequentialRequestLoop, -} from '#memory-server/bench-utils' -import type { StartRequestHandler } from '#memory-server/bench-utils' +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -const errorPathsIterations = 50 -const redirectSeed = 0xdecafbad -const notFoundSeed = 0xdecafb0d -const errorSeed = 0xdecafbed -const unmatchedSeed = 0xdecaf00d -// 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 redirectStatus = 302 -const notFoundStatus = 404 -const errorStatus = 500 -const notFoundMarker = 'data-bench="not-found-boundary"' -const errorMarker = 'data-bench="error-boundary"' - -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}`, - ) - } -} - -function validateNotFoundBody(body: string) { - if (!body.includes(notFoundMarker)) { - throw new Error('Expected error-paths not-found marker in response body') - } -} - -function validateErrorBody(body: string) { - if (!body.includes(errorMarker)) { - throw new Error('Expected error-paths error marker in response body') - } -} - -async function assertStatusSanity( - handler: StartRequestHandler, - request: Request, - validateResponse: (response: Response, request: Request) => void, - validateBody?: (body: string) => void, -) { - const response = await handler.fetch(request) - validateResponse(response, request) - - const body = await response.text() - validateBody?.(body) -} - -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, - validateNotFoundBody, - ) - await assertStatusSanity( - handler, - new Request('http://localhost/boom/sanity-error', requestInit), - validateErrorResponse, - validateErrorBody, - ) - await assertStatusSanity( - handler, - new Request('http://localhost/nope/sanity-unmatched', requestInit), - validateNotFoundResponse, - ) -} export async function setup() { const { default: handler } = (await import( @@ -156,57 +10,5 @@ export async function setup() { default: 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), - benches: [ - { - name: 'mem error-paths redirect (react)', - run: runRedirect, - }, - { - name: 'mem error-paths not-found (react)', - run: runNotFound, - }, - { - name: 'mem error-paths error (react)', - run: runError, - }, - { - name: 'mem error-paths unmatched (react)', - run: runUnmatched, - }, - ], - } + return createSetup('react', handler) } 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..f552d33f2a --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/shared.ts @@ -0,0 +1,211 @@ +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 +const notFoundMarker = 'data-bench="not-found-boundary"' +const errorMarker = 'data-bench="error-boundary"' +// 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}`, + ) + } +} + +function validateNotFoundBody(body: string) { + if (!body.includes(notFoundMarker)) { + throw new Error('Expected error-paths not-found marker in response body') + } +} + +function validateErrorBody(body: string) { + if (!body.includes(errorMarker)) { + throw new Error('Expected error-paths error marker in response body') + } +} + +async function assertStatusSanity( + handler: StartRequestHandler, + request: Request, + validateResponse: (response: Response, request: Request) => void, + validateBody?: (body: string) => void, +) { + const response = await handler.fetch(request) + validateResponse(response, request) + + const body = await response.text() + validateBody?.(body) +} + +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, + validateNotFoundBody, + ) + await assertStatusSanity( + handler, + new Request('http://localhost/boom/sanity-error', requestInit), + validateErrorResponse, + validateErrorBody, + ) + await assertStatusSanity( + handler, + new Request('http://localhost/nope/sanity-unmatched', requestInit), + validateNotFoundResponse, + ) +} + +export function createSetup( + 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), + benches: [ + { + 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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..0a575854b5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..df2908aec9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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/react/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts index 28936ea0c6..66aed269b9 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts @@ -1,52 +1,7 @@ -import { runSequentialRequestLoop } from '#memory-server/bench-utils' -import type { StartRequestHandler } from '#memory-server/bench-utils' +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -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 knownDehydratedRecordName = 'peak-large-page-l8-record-199' - -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') - } - - if (!body.includes(knownDehydratedRecordName)) { - throw new Error( - 'Expected peak-large-page dehydrated record 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 async function setup() { const { default: handler } = (await import( @@ -55,21 +10,5 @@ export async function setup() { default: StartRequestHandler } - const run = () => - runSequentialRequestLoop(handler, { - seed: benchmarkSeed, - iterations: peakLargePageIterations, - buildRequest: buildPeakLargePageRequest, - validateResponse: validatePeakLargePageResponse, - }) - - return { - sanity: () => assertPeakLargePageSanity(handler), - benches: [ - { - name: 'mem peak-large-page (react)', - run, - }, - ], - } + return createSetup('react', handler) } 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..1d020fed22 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts @@ -0,0 +1,75 @@ +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 knownDehydratedRecordName = 'peak-large-page-l8-record-199' + +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') + } + + if (!body.includes(knownDehydratedRecordName)) { + throw new Error( + 'Expected peak-large-page dehydrated record 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 createSetup( + framework: Framework, + handler: StartRequestHandler, +) { + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: peakLargePageIterations, + buildRequest: buildPeakLargePageRequest, + validateResponse: validatePeakLargePageResponse, + }) + + return { + sanity: () => assertPeakLargePageSanity(handler), + benches: [ + { + 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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..0a575854b5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('solid', handler) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/large-page-data.ts new file mode 100644 index 0000000000..8f77c58731 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/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/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..6b8465a552 --- /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,25 @@ +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..bdd3f6c71a --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx @@ -0,0 +1,26 @@ +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..6e3faefe63 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx @@ -0,0 +1,26 @@ +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..2a3ad3fa24 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx @@ -0,0 +1,26 @@ +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..71ec922804 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx @@ -0,0 +1,26 @@ +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..414a93dff8 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx @@ -0,0 +1,26 @@ +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..df2531641e --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.l2.tsx @@ -0,0 +1,26 @@ +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..0dbfc839c1 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/src/routes/l1.tsx @@ -0,0 +1,26 @@ +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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..df2908aec9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('vue', handler) +} diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts new file mode 100644 index 0000000000..2b59e96d72 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/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/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..61b52162f1 --- /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,25 @@ +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..efbcadff76 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx @@ -0,0 +1,26 @@ +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..ff1550045e --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx @@ -0,0 +1,26 @@ +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..c6894302ed --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx @@ -0,0 +1,26 @@ +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..99a4c880f9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx @@ -0,0 +1,26 @@ +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..8e6ac1e36c --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx @@ -0,0 +1,26 @@ +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..074b3465eb --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.l2.tsx @@ -0,0 +1,26 @@ +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..f11410cdbe --- /dev/null +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/routes/l1.tsx @@ -0,0 +1,26 @@ +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/setup.ts b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts index 8da5590818..66aed269b9 100644 --- a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts +++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts @@ -1,65 +1,7 @@ -import { - createDeterministicRandom, - randomSegment, - runSequentialRequestLoop, -} from '#memory-server/bench-utils' -import type { StartRequestHandler } from '#memory-server/bench-utils' +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -const benchmarkSeed = 0xdecafbad -const requestChurnIterations = 200 -const itemPageMarker = 'data-bench="request-churn-item"' -const dehydrationMarker = '$_TSR' -// 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 buildItemRequest(random: () => number) { - const id = `${(requestCounter++).toString(36)}-${randomSegment(random)}` - const q = `q-${randomSegment(random)}` - - return new Request(`http://localhost/items/${id}?q=${q}`, 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) - - if (!body.includes(dehydrationMarker)) { - throw new Error( - 'Expected sanity response to include the dehydration marker', - ) - } -} export async function setup() { const { default: handler } = (await import( @@ -68,21 +10,5 @@ export async function setup() { default: StartRequestHandler } - const run = () => - runSequentialRequestLoop(handler, { - random: benchmarkRandom, - iterations: requestChurnIterations, - buildRequest: buildItemRequest, - validateResponse: validateItemResponse, - }) - - return { - sanity: () => assertRequestChurnSanity(handler), - benches: [ - { - name: 'mem request-churn (react)', - run, - }, - ], - } + return createSetup('react', handler) } 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..d29c3c9685 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/shared.ts @@ -0,0 +1,89 @@ +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"' +const dehydrationMarker = '$_TSR' +// 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) + + if (!body.includes(dehydrationMarker)) { + throw new Error( + 'Expected sanity response to include the dehydration marker', + ) + } +} + +export function createSetup( + 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), + benches: [ + { + 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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..0a575854b5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..df2908aec9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts index b67c0089e4..66aed269b9 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts @@ -1,83 +1,7 @@ -import { - randomSegment, - runSequentialRequestLoop, -} from '#memory-server/bench-utils' -import type { StartRequestHandler } from '#memory-server/bench-utils' +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -const benchmarkSeed = 0x51eaa11 -const serializationPayloadIterations = 20 -const payloadPageMarker = 'data-bench="serialization-payload"' -const dehydrationMarker = '$_TSR' - -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 knownMapKey(id: string) { - return `map-${id}-000` -} - -function getRequestId(request: Request) { - const url = new URL(request.url) - const match = /^\/data\/([^/]+)$/.exec(url.pathname) - const id = match?.[1] - - if (id === undefined) { - throw new Error(`Expected /data/$id request URL, got ${request.url}`) - } - - return decodeURIComponent(id) -} - -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, - _response: Response, - request: Request, -) { - if (!body.includes(payloadPageMarker)) { - throw new Error('Expected serialization-payload marker in response body') - } - - if (!body.includes(dehydrationMarker)) { - throw new Error('Expected serialization-payload dehydration script in body') - } - - const mapKey = knownMapKey(getRequestId(request)) - - if (!body.includes(mapKey)) { - throw new Error(`Expected dehydrated payload to include Map key ${mapKey}`) - } -} - -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, response, request) -} export async function setup() { const { default: handler } = (await import( @@ -86,21 +10,5 @@ export async function setup() { default: StartRequestHandler } - const run = () => - runSequentialRequestLoop(handler, { - seed: benchmarkSeed, - iterations: serializationPayloadIterations, - buildRequest: buildPayloadRequest, - validateResponse: validatePayloadResponse, - }) - - return { - sanity: () => assertSerializationPayloadSanity(handler), - benches: [ - { - name: 'mem serialization-payload (react)', - run, - }, - ], - } + return createSetup('react', handler) } 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..2fa7e4b5e4 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/shared.ts @@ -0,0 +1,106 @@ +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 dehydrationMarker = '$_TSR' + +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 knownMapKey(id: string) { + return `map-${id}-000` +} + +function getRequestId(request: Request) { + const url = new URL(request.url) + const match = /^\/data\/([^/]+)$/.exec(url.pathname) + const id = match?.[1] + + if (id === undefined) { + throw new Error(`Expected /data/$id request URL, got ${request.url}`) + } + + return decodeURIComponent(id) +} + +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, + _response: Response, + request: Request, +) { + if (!body.includes(payloadPageMarker)) { + throw new Error('Expected serialization-payload marker in response body') + } + + if (!body.includes(dehydrationMarker)) { + throw new Error('Expected serialization-payload dehydration script in body') + } + + const mapKey = knownMapKey(getRequestId(request)) + + if (!body.includes(mapKey)) { + throw new Error(`Expected dehydrated payload to include Map key ${mapKey}`) + } +} + +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, response, request) +} + +export function createSetup( + framework: Framework, + handler: StartRequestHandler, +) { + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: serializationPayloadIterations, + buildRequest: buildPayloadRequest, + validateResponse: validatePayloadResponse, + }) + + return { + sanity: () => assertSerializationPayloadSanity(handler), + benches: [ + { + 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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..0a575854b5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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..a57778d7ee --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/src/routes/data.$id.tsx @@ -0,0 +1,132 @@ +import { createFileRoute } from '@tanstack/solid-router' + +const mapEntryCount = 500 +const setEntryCount = 500 +const temporalEntryCount = 500 +const nestedTreeDepth = 5 +const nestedTreeBreadth = 6 +const payloadTextLength = 150 + +interface MapPayloadValue { + index: number + label: string + createdAt: Date + count: bigint + text: string +} + +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 const Route = createFileRoute('/data/$id')({ + loader: ({ params }) => makeSerializationPayload(params.id), + component: DataComponent, +}) + +function DataComponent() { + const data = Route.useLoaderData() + + return ( +
+ Map size: {data().lookup.size} +
+ ) +} + +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/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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..df2908aec9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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..e32e12db61 --- /dev/null +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/data.$id.tsx @@ -0,0 +1,132 @@ +import { createFileRoute } from '@tanstack/vue-router' + +const mapEntryCount = 500 +const setEntryCount = 500 +const temporalEntryCount = 500 +const nestedTreeDepth = 5 +const nestedTreeBreadth = 6 +const payloadTextLength = 150 + +interface MapPayloadValue { + index: number + label: string + createdAt: Date + count: bigint + text: string +} + +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 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} +
+ ) +} + +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/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/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts index 0469c70909..66aed269b9 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts @@ -1,202 +1,7 @@ -import { - createDeterministicRandom, - randomSegment, - runSequentialRequestLoop, -} from '#memory-server/bench-utils' -import type { StartRequestHandler } from '#memory-server/bench-utils' - -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 - } +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -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}`, - ) - } - - if (!body.includes(`${expectedId}-4`)) { - throw new Error(`Expected final payload record for ${expectedId}`) - } -} - -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 setup() { const { default: handler } = (await import( @@ -204,33 +9,6 @@ export async function setup() { )) as { default: 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), - benches: [ - { - name: 'mem server-fn-churn (react)', - run, - }, - ], - } + return createSetup('react', handler) } 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..1666bc5e7d --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts @@ -0,0 +1,236 @@ +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}`, + ) + } + + if (!body.includes(`${expectedId}-4`)) { + throw new Error(`Expected final payload record for ${expectedId}`) + } +} + +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 createSetup( + 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), + benches: [ + { + 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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..0a575854b5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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..86a79393b9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts @@ -0,0 +1,47 @@ +import { createMiddleware, createServerFn } from '@tanstack/solid-start' + +type ServerFnInput = { + id: string +} + +const recordIndexes = Array.from({ length: 5 }, (_, index) => index) + +const contextMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => + next({ + context: { + ctx: 'ctx-server-fn-churn', + }, + }), +) + +function validateInput(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 } +} + +function echoPayload(data: ServerFnInput, context: { ctx: string }) { + return { + id: data.id, + ctx: context.ctx, + payload: recordIndexes.map((index) => ({ + id: `${data.id}-${index}`, + label: `record-${index}`, + })), + } +} + +export const churnGet = createServerFn({ method: 'GET' }) + .middleware([contextMiddleware]) + .validator(validateInput) + .handler(({ data, context }) => echoPayload(data, context)) + +export const churnPost = createServerFn({ method: 'POST' }) + .middleware([contextMiddleware]) + .validator(validateInput) + .handler(({ data, context }) => echoPayload(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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..df2908aec9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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..c3d8fbe0ed --- /dev/null +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts @@ -0,0 +1,47 @@ +import { createMiddleware, createServerFn } from '@tanstack/vue-start' + +type ServerFnInput = { + id: string +} + +const recordIndexes = Array.from({ length: 5 }, (_, index) => index) + +const contextMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => + next({ + context: { + ctx: 'ctx-server-fn-churn', + }, + }), +) + +function validateInput(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 } +} + +function echoPayload(data: ServerFnInput, context: { ctx: string }) { + return { + id: data.id, + ctx: context.ctx, + payload: recordIndexes.map((index) => ({ + id: `${data.id}-${index}`, + label: `record-${index}`, + })), + } +} + +export const churnGet = createServerFn({ method: 'GET' }) + .middleware([contextMiddleware]) + .validator(validateInput) + .handler(({ data, context }) => echoPayload(data, context)) + +export const churnPost = createServerFn({ method: 'POST' }) + .middleware([contextMiddleware]) + .validator(validateInput) + .handler(({ data, context }) => echoPayload(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/react/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts index 895520d45f..66aed269b9 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts @@ -1,123 +1,7 @@ -import { - randomSegment, - runSequentialRequestLoop, -} from '#memory-server/bench-utils' -import type { StartRequestHandler } from '#memory-server/bench-utils' +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href -const benchmarkSeed = 0xdecafbad -const streamingPeakIterations = 20 -const fallbackMarkers = [ - 'streaming-peak-fallback-0', - 'streaming-peak-fallback-1', - 'streaming-peak-fallback-2', - 'streaming-peak-fallback-3', -] as const -const deferredSectionMarkers = [ - 'streaming-peak-deferred-0', - 'streaming-peak-deferred-1', - 'streaming-peak-deferred-2', - 'streaming-peak-deferred-3', -] as const - -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 } -} - -function assertFallbacksPrecedeDeferredContent(body: string) { - for (let index = 0; index < fallbackMarkers.length; index++) { - const fallbackIndex = body.indexOf(fallbackMarkers[index]!) - const deferredIndex = body.indexOf(deferredSectionMarkers[index]!) - - if (fallbackIndex === -1) { - throw new Error( - `Expected fallback marker ${fallbackMarkers[index]} in body`, - ) - } - - if (deferredIndex === -1) { - throw new Error( - `Expected deferred section marker ${deferredSectionMarkers[index]} in body`, - ) - } - - if (fallbackIndex > deferredIndex) { - throw new Error( - `Expected ${fallbackMarkers[index]} to precede ${deferredSectionMarkers[index]}`, - ) - } - } -} - -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}`, - ) - } - - assertFallbacksPrecedeDeferredContent(chunked.body) -} export async function setup() { const { default: handler } = (await import( @@ -126,21 +10,5 @@ export async function setup() { default: StartRequestHandler } - const run = () => - runSequentialRequestLoop(handler, { - seed: benchmarkSeed, - iterations: streamingPeakIterations, - buildRequest: buildStreamingRequest, - validateResponse: validateStreamingResponse, - }) - - return { - sanity: () => assertStreamingPeakSanity(handler), - benches: [ - { - name: 'mem streaming-peak chunked (react)', - run, - }, - ], - } + return createSetup('react', handler) } 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..627eea277f --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/shared.ts @@ -0,0 +1,146 @@ +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 fallbackMarkers = [ + 'streaming-peak-fallback-0', + 'streaming-peak-fallback-1', + 'streaming-peak-fallback-2', + 'streaming-peak-fallback-3', +] as const +const deferredSectionMarkers = [ + 'streaming-peak-deferred-0', + 'streaming-peak-deferred-1', + 'streaming-peak-deferred-2', + 'streaming-peak-deferred-3', +] as const + +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 } +} + +function assertFallbacksPrecedeDeferredContent(body: string) { + for (let index = 0; index < fallbackMarkers.length; index++) { + const fallbackIndex = body.indexOf(fallbackMarkers[index]!) + const deferredIndex = body.indexOf(deferredSectionMarkers[index]!) + + if (fallbackIndex === -1) { + throw new Error( + `Expected fallback marker ${fallbackMarkers[index]} in body`, + ) + } + + if (deferredIndex === -1) { + throw new Error( + `Expected deferred section marker ${deferredSectionMarkers[index]} in body`, + ) + } + + if (fallbackIndex > deferredIndex) { + throw new Error( + `Expected ${fallbackMarkers[index]} to precede ${deferredSectionMarkers[index]}`, + ) + } + } +} + +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}`, + ) + } + + assertFallbacksPrecedeDeferredContent(chunked.body) +} + +export function createSetup( + framework: Framework, + handler: StartRequestHandler, +) { + const run = () => + runSequentialRequestLoop(handler, { + seed: benchmarkSeed, + iterations: streamingPeakIterations, + buildRequest: buildStreamingRequest, + validateResponse: validateStreamingResponse, + }) + + return { + sanity: () => assertStreamingPeakSanity(handler), + benches: [ + { + 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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..0a575854b5 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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..f77d04a04f --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/src/routes/stream.$id.tsx @@ -0,0 +1,99 @@ +import { Await, createFileRoute } from '@tanstack/solid-router' +import { Suspense } from 'solid-js' + +const deferredRecordCount = 250 +const recordValueLength = 128 +const fallbackFlushDelayMs = 25 + +interface DeferredRecord { + id: string + value: string +} + +export interface DeferredSectionPayload { + index: number + records: Array +} + +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 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) +} + +function makeDeferredSection(id: string, sectionIndex: number) { + return afterFallbackFlush(sectionIndex).then(() => ({ + index: sectionIndex, + records: Array.from({ length: deferredRecordCount }, (_, recordIndex) => ({ + id: `${id}-${sectionIndex}-${recordIndex}`, + value: makeRecordValue(id, sectionIndex, recordIndex), + })), + })) +} + +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..1dc2e0da86 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts @@ -0,0 +1,10 @@ +import { describe } from 'vitest' +import { registerServerMemoryBenches } from '#memory-server/runner' +import { setup } from './setup' + +const test = await setup() +await test.sanity() + +describe('memory', () => { + registerServerMemoryBenches(test) +}) 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..882050f597 --- /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 { setup } from './setup.ts' + +await runServerFlameBenchmark(setup) 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..df2908aec9 --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts @@ -0,0 +1,14 @@ +import { createSetup } from '../shared' +import type { StartRequestHandler } from '../shared' + +const appModuleUrl = new URL('./dist/server/server.js', import.meta.url).href + +export async function setup() { + const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl + )) as { + default: StartRequestHandler + } + + return createSetup('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..191a8f9d4e --- /dev/null +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/src/routes/stream.$id.tsx @@ -0,0 +1,106 @@ +import { Await, createFileRoute } from '@tanstack/vue-router' +import { Suspense } from 'vue' + +const deferredRecordCount = 250 +const recordValueLength = 128 +const fallbackFlushDelayMs = 1 + +interface DeferredRecord { + id: string + value: string +} + +export interface DeferredSectionPayload { + index: number + records: Array +} + +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 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) +} + +function makeDeferredSection(id: string, sectionIndex: number) { + return afterFallbackFlush(sectionIndex).then(() => ({ + index: sectionIndex, + records: Array.from({ length: deferredRecordCount }, (_, recordIndex) => ({ + id: `${id}-${sectionIndex}-${recordIndex}`, + value: makeRecordValue(id, sectionIndex, recordIndex), + })), + })) +} + +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/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 4868afde6a..f544549330 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,13 @@ "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..92ec955be7 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,54 @@ 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 2b2d43c119..4917d0514f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -321,12 +321,24 @@ importers: '@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 @@ -346,6 +358,12 @@ importers: '@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) @@ -355,6 +373,9 @@ importers: 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)) @@ -367,12 +388,30 @@ importers: '@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 @@ -386,12 +425,18 @@ importers: '@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)) @@ -2107,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 @@ -31701,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 @@ -31894,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 @@ -31902,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 @@ -33781,7 +33839,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.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)) + vitest: 4.1.4(@types/node@25.0.9)(@vitest/ui@4.1.4)(jsdom@25.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)) '@vitest/utils@4.1.4': dependencies: From ab7fb18f7acabd7c9162038521ca5141af1ddb8b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 20:34:34 +0000 Subject: [PATCH 20/24] ci: apply automated fixes --- .../scenarios/navigation-churn/vue/src/routes/a.tsx | 4 +++- .../scenarios/navigation-churn/vue/src/routes/b.tsx | 4 +++- .../aborted-requests/solid/src/routes/stream.$id.tsx | 5 ++++- .../aborted-requests/vue/src/routes/stream.$id.tsx | 8 ++++---- packages/vue-router/src/ssr/renderRouterToStream.tsx | 7 +++++-- 5 files changed, 19 insertions(+), 9 deletions(-) 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 index a1537dc324..183228e748 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/a.tsx @@ -10,5 +10,7 @@ export const Route = createFileRoute('/a')({ function AComponent() { const data = Route.useLoaderData() - return
{`${data.value.name}:${data.value.ts}`}
+ 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 index ce19d1bd10..3a18ec97a9 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/src/routes/b.tsx @@ -10,5 +10,7 @@ export const Route = createFileRoute('/b')({ function BComponent() { const data = Route.useLoaderData() - return
{`${data.value.name}:${data.value.ts}`}
+ return ( +
{`${data.value.name}:${data.value.ts}`}
+ ) } 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 index 0d723098c8..60a9e8e82b 100644 --- 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 @@ -117,7 +117,10 @@ function DeferredRecords(props: { promise: Promise> dataBench: string }) { - const [records] = createResource(() => props.promise, (promise) => promise) + const [records] = createResource( + () => props.promise, + (promise) => promise, + ) return ( 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 index 8b2676523d..2c11ab569a 100644 --- 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 @@ -103,8 +103,8 @@ function StreamComponent() { ))} )} - /> - ), + /> + ), fallback: () => null, }} @@ -120,8 +120,8 @@ function StreamComponent() { ))} )} - /> - ), + /> + ), fallback: () => null, }} diff --git a/packages/vue-router/src/ssr/renderRouterToStream.tsx b/packages/vue-router/src/ssr/renderRouterToStream.tsx index 92ec955be7..efad4fc2eb 100644 --- a/packages/vue-router/src/ssr/renderRouterToStream.tsx +++ b/packages/vue-router/src/ssr/renderRouterToStream.tsx @@ -12,7 +12,7 @@ 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') + (error as any)?.code === 'ABORT_ERR' function prependDoctype( readable: globalThis.ReadableStream, @@ -178,7 +178,10 @@ export const renderRouterToStream = async ({ } writerDone = true - return innerWriter.abort(reason).catch(handleWriterError).finally(releaseWriter) + return innerWriter + .abort(reason) + .catch(handleWriterError) + .finally(releaseWriter) }, }) From 8d25db98f3419566b718013d5c030cc075ca1029 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sat, 13 Jun 2026 22:56:23 +0200 Subject: [PATCH 21/24] simplify dual architecture --- benchmarks/memory/README.md | 5 ++-- benchmarks/memory/client/benchmark.ts | 2 +- benchmarks/memory/client/flame-runner.ts | 16 +++++------- benchmarks/memory/client/package.json | 15 +++-------- benchmarks/memory/client/runner.ts | 25 ------------------- .../react/memory.bench.ts | 22 ++++++++++++---- .../react/memory.flame.ts | 4 +-- .../interrupted-navigations/react/setup.ts | 19 +++++++------- .../interrupted-navigations/shared.ts | 2 +- .../solid/memory.bench.ts | 22 ++++++++++++---- .../solid/memory.flame.ts | 4 +-- .../interrupted-navigations/solid/setup.ts | 19 +++++++------- .../vue/memory.bench.ts | 22 ++++++++++++---- .../vue/memory.flame.ts | 4 +-- .../interrupted-navigations/vue/setup.ts | 19 +++++++------- .../react/memory.bench.ts | 22 ++++++++++++---- .../react/memory.flame.ts | 4 +-- .../loader-data-retention/react/setup.ts | 11 +++++--- .../scenarios/loader-data-retention/shared.ts | 2 +- .../solid/memory.bench.ts | 22 ++++++++++++---- .../solid/memory.flame.ts | 4 +-- .../loader-data-retention/solid/setup.ts | 11 +++++--- .../loader-data-retention/vue/memory.bench.ts | 22 ++++++++++++---- .../loader-data-retention/vue/memory.flame.ts | 4 +-- .../loader-data-retention/vue/setup.ts | 11 +++++--- .../mount-unmount/react/memory.bench.ts | 22 ++++++++++++---- .../mount-unmount/react/memory.flame.ts | 4 +-- .../scenarios/mount-unmount/react/setup.ts | 10 +++++--- .../client/scenarios/mount-unmount/shared.ts | 5 +++- .../mount-unmount/solid/memory.bench.ts | 22 ++++++++++++---- .../mount-unmount/solid/memory.flame.ts | 4 +-- .../scenarios/mount-unmount/solid/setup.ts | 10 +++++--- .../mount-unmount/vue/memory.bench.ts | 22 ++++++++++++---- .../mount-unmount/vue/memory.flame.ts | 4 +-- .../scenarios/mount-unmount/vue/setup.ts | 10 +++++--- .../navigation-churn/react/memory.bench.ts | 22 ++++++++++++---- .../navigation-churn/react/memory.flame.ts | 4 +-- .../scenarios/navigation-churn/react/setup.ts | 10 +++++--- .../scenarios/navigation-churn/shared.ts | 5 +++- .../navigation-churn/solid/memory.bench.ts | 22 ++++++++++++---- .../navigation-churn/solid/memory.flame.ts | 4 +-- .../scenarios/navigation-churn/solid/setup.ts | 10 +++++--- .../navigation-churn/vue/memory.bench.ts | 22 ++++++++++++---- .../navigation-churn/vue/memory.flame.ts | 4 +-- .../scenarios/navigation-churn/vue/setup.ts | 10 +++++--- .../preload-churn/react/memory.bench.ts | 22 ++++++++++++---- .../preload-churn/react/memory.flame.ts | 4 +-- .../scenarios/preload-churn/react/setup.ts | 11 +++++--- .../client/scenarios/preload-churn/shared.ts | 2 +- .../preload-churn/solid/memory.bench.ts | 22 ++++++++++++---- .../preload-churn/solid/memory.flame.ts | 4 +-- .../scenarios/preload-churn/solid/setup.ts | 11 +++++--- .../preload-churn/vue/memory.bench.ts | 22 ++++++++++++---- .../preload-churn/vue/memory.flame.ts | 4 +-- .../scenarios/preload-churn/vue/setup.ts | 11 +++++--- .../react/memory.bench.ts | 22 ++++++++++++---- .../react/memory.flame.ts | 4 +-- .../unique-location-churn/react/setup.ts | 10 +++++--- .../scenarios/unique-location-churn/shared.ts | 5 +++- .../solid/memory.bench.ts | 22 ++++++++++++---- .../solid/memory.flame.ts | 4 +-- .../unique-location-churn/solid/setup.ts | 10 +++++--- .../unique-location-churn/vue/memory.bench.ts | 22 ++++++++++++---- .../unique-location-churn/vue/memory.flame.ts | 4 +-- .../unique-location-churn/vue/setup.ts | 10 +++++--- benchmarks/memory/server/benchmark.ts | 6 ++--- benchmarks/memory/server/flame-runner.ts | 12 ++++----- benchmarks/memory/server/package.json | 4 +-- benchmarks/memory/server/runner.ts | 9 ------- .../aborted-requests/react/memory.bench.ts | 13 +++++----- .../aborted-requests/react/memory.flame.ts | 4 +-- .../scenarios/aborted-requests/react/setup.ts | 20 +++++++-------- .../scenarios/aborted-requests/shared.ts | 4 +-- .../aborted-requests/solid/memory.bench.ts | 13 +++++----- .../aborted-requests/solid/memory.flame.ts | 4 +-- .../scenarios/aborted-requests/solid/setup.ts | 20 +++++++-------- .../aborted-requests/vue/memory.bench.ts | 13 +++++----- .../aborted-requests/vue/memory.flame.ts | 4 +-- .../scenarios/aborted-requests/vue/setup.ts | 20 +++++++-------- .../error-paths/react/memory.bench.ts | 13 +++++----- .../error-paths/react/memory.flame.ts | 4 +-- .../scenarios/error-paths/react/setup.ts | 20 +++++++-------- .../server/scenarios/error-paths/shared.ts | 4 +-- .../error-paths/solid/memory.bench.ts | 13 +++++----- .../error-paths/solid/memory.flame.ts | 4 +-- .../scenarios/error-paths/solid/setup.ts | 20 +++++++-------- .../scenarios/error-paths/vue/memory.bench.ts | 13 +++++----- .../scenarios/error-paths/vue/memory.flame.ts | 4 +-- .../server/scenarios/error-paths/vue/setup.ts | 20 +++++++-------- .../peak-large-page/react/memory.bench.ts | 13 +++++----- .../peak-large-page/react/memory.flame.ts | 4 +-- .../scenarios/peak-large-page/react/setup.ts | 20 +++++++-------- .../scenarios/peak-large-page/shared.ts | 4 +-- .../peak-large-page/solid/memory.bench.ts | 13 +++++----- .../peak-large-page/solid/memory.flame.ts | 4 +-- .../scenarios/peak-large-page/solid/setup.ts | 20 +++++++-------- .../peak-large-page/vue/memory.bench.ts | 13 +++++----- .../peak-large-page/vue/memory.flame.ts | 4 +-- .../scenarios/peak-large-page/vue/setup.ts | 20 +++++++-------- .../request-churn/react/memory.bench.ts | 13 +++++----- .../request-churn/react/memory.flame.ts | 4 +-- .../scenarios/request-churn/react/setup.ts | 20 +++++++-------- .../server/scenarios/request-churn/shared.ts | 4 +-- .../request-churn/solid/memory.bench.ts | 13 +++++----- .../request-churn/solid/memory.flame.ts | 4 +-- .../scenarios/request-churn/solid/setup.ts | 20 +++++++-------- .../request-churn/vue/memory.bench.ts | 13 +++++----- .../request-churn/vue/memory.flame.ts | 4 +-- .../scenarios/request-churn/vue/setup.ts | 20 +++++++-------- .../react/memory.bench.ts | 13 +++++----- .../react/memory.flame.ts | 4 +-- .../serialization-payload/react/setup.ts | 20 +++++++-------- .../scenarios/serialization-payload/shared.ts | 4 +-- .../solid/memory.bench.ts | 13 +++++----- .../solid/memory.flame.ts | 4 +-- .../serialization-payload/solid/setup.ts | 20 +++++++-------- .../serialization-payload/vue/memory.bench.ts | 13 +++++----- .../serialization-payload/vue/memory.flame.ts | 4 +-- .../serialization-payload/vue/setup.ts | 20 +++++++-------- .../server-fn-churn/react/memory.bench.ts | 13 +++++----- .../server-fn-churn/react/memory.flame.ts | 4 +-- .../scenarios/server-fn-churn/react/setup.ts | 20 +++++++-------- .../scenarios/server-fn-churn/shared.ts | 4 +-- .../server-fn-churn/solid/memory.bench.ts | 13 +++++----- .../server-fn-churn/solid/memory.flame.ts | 4 +-- .../scenarios/server-fn-churn/solid/setup.ts | 20 +++++++-------- .../server-fn-churn/vue/memory.bench.ts | 13 +++++----- .../server-fn-churn/vue/memory.flame.ts | 4 +-- .../scenarios/server-fn-churn/vue/setup.ts | 20 +++++++-------- .../streaming-peak/react/memory.bench.ts | 13 +++++----- .../streaming-peak/react/memory.flame.ts | 4 +-- .../scenarios/streaming-peak/react/setup.ts | 20 +++++++-------- .../server/scenarios/streaming-peak/shared.ts | 4 +-- .../streaming-peak/solid/memory.bench.ts | 13 +++++----- .../streaming-peak/solid/memory.flame.ts | 4 +-- .../scenarios/streaming-peak/solid/setup.ts | 20 +++++++-------- .../streaming-peak/vue/memory.bench.ts | 13 +++++----- .../streaming-peak/vue/memory.flame.ts | 4 +-- .../scenarios/streaming-peak/vue/setup.ts | 20 +++++++-------- 139 files changed, 916 insertions(+), 685 deletions(-) delete mode 100644 benchmarks/memory/client/runner.ts delete mode 100644 benchmarks/memory/server/runner.ts diff --git a/benchmarks/memory/README.md b/benchmarks/memory/README.md index 511acc6693..d9fb24f752 100644 --- a/benchmarks/memory/README.md +++ b/benchmarks/memory/README.md @@ -27,8 +27,9 @@ benchmarks/memory// 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` owns the scenario workload, sanity checks, and any deterministic id -generation; `memory.bench.ts` and `memory.flame.ts` are thin runners only. +`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 diff --git a/benchmarks/memory/client/benchmark.ts b/benchmarks/memory/client/benchmark.ts index bcf043c953..8b877b0740 100644 --- a/benchmarks/memory/client/benchmark.ts +++ b/benchmarks/memory/client/benchmark.ts @@ -1,4 +1,4 @@ -export interface ClientMemoryBenchmark { +export interface ClientMemoryWorkload { name: string before?: () => Promise | void run: () => Promise | void diff --git a/benchmarks/memory/client/flame-runner.ts b/benchmarks/memory/client/flame-runner.ts index 93abe6cf13..04941fda67 100644 --- a/benchmarks/memory/client/flame-runner.ts +++ b/benchmarks/memory/client/flame-runner.ts @@ -1,18 +1,14 @@ import { profileFlameWorkload } from '../flame-control.ts' import { window } from './jsdom.ts' -import type { ClientMemoryBenchmark } from './benchmark.ts' - -export async function runClientFlameBenchmark( - setup: () => ClientMemoryBenchmark, -) { - const test = setup() +import type { ClientMemoryWorkload } from './benchmark.ts' +export async function runClientFlameBenchmark(workload: ClientMemoryWorkload) { try { - await test.sanity() - await test.before?.() - await profileFlameWorkload(test.run) + await workload.sanity() + await workload.before?.() + await profileFlameWorkload(workload.run, workload.name) } finally { - await test.after?.() + await workload.after?.() window.close() } } diff --git a/benchmarks/memory/client/package.json b/benchmarks/memory/client/package.json index d7fd463bde..231ad6af58 100644 --- a/benchmarks/memory/client/package.json +++ b/benchmarks/memory/client/package.json @@ -6,18 +6,9 @@ "clean:profiles": "rm -rf scenarios/*/*/.profiles" }, "imports": { - "#memory-client/bench-utils": { - "types": "./bench-utils.ts", - "default": "./bench-utils.ts" - }, - "#memory-client/flame-runner": { - "types": "./flame-runner.ts", - "default": "./flame-runner.ts" - }, - "#memory-client/runner": { - "types": "./runner.ts", - "default": "./runner.ts" - } + "#memory-client/benchmark": "./benchmark.ts", + "#memory-client/bench-utils": "./bench-utils.ts", + "#memory-client/flame-runner": "./flame-runner.ts" }, "dependencies": { "@tanstack/react-router": "workspace:*", diff --git a/benchmarks/memory/client/runner.ts b/benchmarks/memory/client/runner.ts deleted file mode 100644 index a3f29d732a..0000000000 --- a/benchmarks/memory/client/runner.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { afterAll, beforeAll, bench } from 'vitest' -import { memoryBenchOptions } from './bench-utils' -import type { ClientMemoryBenchmark } from './benchmark' - -export function registerClientMemoryBench(test: ClientMemoryBenchmark) { - if (test.before && test.after) { - /** - * Plain `vitest bench` never runs suite hooks (beforeAll/afterAll) and only - * honors tinybench's setup/teardown options; the CodSpeed runner does the - * exact opposite. Both registrations are load-bearing: exactly one pair - * runs in any given mode. - */ - beforeAll(test.before) - afterAll(test.after) - - bench(test.name, test.run, { - ...memoryBenchOptions, - setup: test.before, - teardown: test.after, - }) - return - } - - bench(test.name, test.run, memoryBenchOptions) -} diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts index 5a61a00e61..6e072ad5f3 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/setup.ts @@ -1,5 +1,6 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { @@ -9,12 +10,10 @@ const { slowLoaderRegistry, } = (await import(/* @vite-ignore */ appModulePath)) as typeof App -export function setup() { - return createSetup( - 'react', - mountTestApp, - resolveAllSlowLoaders, - resolveSlowLoader, - slowLoaderRegistry, - ) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts index ede8ad2bc0..87c91d82bf 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts @@ -144,7 +144,7 @@ function reasonHasCancellationShape(reason: unknown) { ) } -export function createSetup( +export function createWorkload( framework: Framework, mountTestApp: MountTestApp, resolveAllSlowLoaders: ResolveAllSlowLoaders, diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts index 28deb89580..e736111663 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/setup.ts @@ -1,5 +1,6 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { @@ -9,12 +10,10 @@ const { slowLoaderRegistry, } = (await import(/* @vite-ignore */ appModulePath)) as typeof App -export function setup() { - return createSetup( - 'solid', - mountTestApp, - resolveAllSlowLoaders, - resolveSlowLoader, - slowLoaderRegistry, - ) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts index fad57cfc8c..260e45b64b 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/setup.ts @@ -1,5 +1,6 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { @@ -9,12 +10,10 @@ const { slowLoaderRegistry, } = (await import(/* @vite-ignore */ appModulePath)) as typeof App -export function setup() { - return createSetup( - 'vue', - mountTestApp, - resolveAllSlowLoaders, - resolveSlowLoader, - slowLoaderRegistry, - ) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, + resolveAllSlowLoaders, + resolveSlowLoader, + slowLoaderRegistry, +) 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 index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts index 1f84970d14..fafaf13e6f 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/setup.ts @@ -1,11 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { loaderPayloadRecordCount, mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('react', mountTestApp, loaderPayloadRecordCount) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, + loaderPayloadRecordCount, +) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts index 06c8f6eecd..84f9b71bc1 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts @@ -61,7 +61,7 @@ function createPageIds() { ) } -export function createSetup( +export function createWorkload( framework: Framework, mountTestApp: MountTestApp, loaderPayloadRecordCount: number, 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 index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts index 56a22a4ac5..b7c80a3e49 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/setup.ts @@ -1,11 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { loaderPayloadRecordCount, mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('solid', mountTestApp, loaderPayloadRecordCount) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, + loaderPayloadRecordCount, +) 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 index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts index a1701d8dbf..a83a1f70d1 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/setup.ts @@ -1,11 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { loaderPayloadRecordCount, mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('vue', mountTestApp, loaderPayloadRecordCount) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, + loaderPayloadRecordCount, +) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts index 7083cd1432..70603b5c34 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/react/setup.ts @@ -1,11 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('react', mountTestApp) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/shared.ts b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts index 1cc57712cd..ed3702f876 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/shared.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts @@ -39,7 +39,10 @@ function assertEmptyBody() { } } -export function createSetup(framework: Framework, mountTestApp: MountTestApp) { +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, +) { warnDevMode(framework) async function cycle() { diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts index 73e5f8949e..493425c6a2 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/solid/setup.ts @@ -1,11 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('solid', mountTestApp) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts b/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts index 0f76d289d4..a38df04788 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/vue/setup.ts @@ -1,11 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('vue', mountTestApp) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts index 7083cd1432..70603b5c34 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/react/setup.ts @@ -1,11 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('react', mountTestApp) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/shared.ts b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts index ab36c48767..595cfd4bf3 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/shared.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts @@ -32,7 +32,10 @@ function warnDevMode(framework: Framework) { } } -export function createSetup(framework: Framework, mountTestApp: MountTestApp) { +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, +) { warnDevMode(framework) let container: HTMLDivElement | undefined = undefined diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts index 73e5f8949e..493425c6a2 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/solid/setup.ts @@ -1,11 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('solid', mountTestApp) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts index 0f76d289d4..a38df04788 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/vue/setup.ts @@ -1,11 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('vue', mountTestApp) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts index d7c39e5e01..31cd97d135 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/react/setup.ts @@ -1,11 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { getTrackedItemLoaderCount, mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('react', mountTestApp, getTrackedItemLoaderCount) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, + getTrackedItemLoaderCount, +) diff --git a/benchmarks/memory/client/scenarios/preload-churn/shared.ts b/benchmarks/memory/client/scenarios/preload-churn/shared.ts index 09784fc311..ab428943f3 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/shared.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/shared.ts @@ -77,7 +77,7 @@ async function drainMicrotasks() { await Promise.resolve() } -export function createSetup( +export function createWorkload( framework: Framework, mountTestApp: MountTestApp, getTrackedItemLoaderCount: GetTrackedItemLoaderCount, diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts index 0fdbe25998..4db457f561 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/setup.ts @@ -1,11 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { getTrackedItemLoaderCount, mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('solid', mountTestApp, getTrackedItemLoaderCount) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, + getTrackedItemLoaderCount, +) diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts index ab21eb1030..4aad4af31e 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/setup.ts @@ -1,11 +1,14 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { getTrackedItemLoaderCount, mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('vue', mountTestApp, getTrackedItemLoaderCount) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, + getTrackedItemLoaderCount, +) 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 index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts index 7083cd1432..70603b5c34 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/react/setup.ts @@ -1,11 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('react', mountTestApp) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'react', + mountTestApp, +) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts index 272882540e..43f85b3369 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts @@ -52,7 +52,10 @@ function warnDevMode(framework: Framework) { } } -export function createSetup(framework: Framework, mountTestApp: MountTestApp) { +export function createWorkload( + framework: Framework, + mountTestApp: MountTestApp, +) { warnDevMode(framework) let container: HTMLDivElement | undefined = undefined 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 index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts index 73e5f8949e..493425c6a2 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/solid/setup.ts @@ -1,11 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('solid', mountTestApp) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'solid', + mountTestApp, +) 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 index 5b1c779d71..e645ab38f1 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.bench.ts @@ -1,9 +1,21 @@ -import { describe } from 'vitest' -import { registerClientMemoryBench } from '#memory-client/runner' -import { setup } from './setup' +import { afterAll, beforeAll, bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-client/bench-utils' +import { workload } from './setup' -await setup().sanity() +await workload.sanity() describe('memory', () => { - registerClientMemoryBench(setup()) + 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 index b464c772b0..952fd9a62c 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runClientFlameBenchmark } from '#memory-client/flame-runner' -import { setup } from './setup.ts' +import { workload } from './setup.ts' -await runClientFlameBenchmark(setup) +await runClientFlameBenchmark(workload) diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts b/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts index 0f76d289d4..a38df04788 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/vue/setup.ts @@ -1,11 +1,13 @@ +import type { ClientMemoryWorkload } from '#memory-client/benchmark' import type * as App from './src/app' -import { createSetup } from '../shared' +import { createWorkload } from '../shared.ts' const appModulePath = './dist/app.js' const { mountTestApp } = (await import( /* @vite-ignore */ appModulePath )) as typeof App -export function setup() { - return createSetup('vue', mountTestApp) -} +export const workload: ClientMemoryWorkload = createWorkload( + 'vue', + mountTestApp, +) diff --git a/benchmarks/memory/server/benchmark.ts b/benchmarks/memory/server/benchmark.ts index 02379a8ecf..301a9f956d 100644 --- a/benchmarks/memory/server/benchmark.ts +++ b/benchmarks/memory/server/benchmark.ts @@ -1,9 +1,9 @@ -export interface ServerMemoryBench { +export interface ServerMemoryWorkload { name: string run: () => Promise | void } -export interface ServerMemoryBenchmark { +export interface ServerMemoryWorkloadGroup { sanity: () => Promise | void - benches: Array + workloads: Array } diff --git a/benchmarks/memory/server/flame-runner.ts b/benchmarks/memory/server/flame-runner.ts index d1892815c9..1930085aad 100644 --- a/benchmarks/memory/server/flame-runner.ts +++ b/benchmarks/memory/server/flame-runner.ts @@ -1,14 +1,12 @@ import { profileFlameWorkload } from '../flame-control.ts' -import type { ServerMemoryBenchmark } from './benchmark.ts' +import type { ServerMemoryWorkloadGroup } from './benchmark.ts' export async function runServerFlameBenchmark( - setup: () => Promise, + workloadGroup: ServerMemoryWorkloadGroup, ) { - const test = await setup() + await workloadGroup.sanity() - await test.sanity() - - for (const bench of test.benches) { - await profileFlameWorkload(bench.run, bench.name) + 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 index d898755beb..68ba6b3c32 100644 --- a/benchmarks/memory/server/package.json +++ b/benchmarks/memory/server/package.json @@ -6,9 +6,9 @@ "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", - "#memory-server/runner": "./runner.ts" + "#memory-server/flame-runner": "./flame-runner.ts" }, "dependencies": { "@tanstack/react-router": "workspace:*", diff --git a/benchmarks/memory/server/runner.ts b/benchmarks/memory/server/runner.ts deleted file mode 100644 index 783facbf3b..0000000000 --- a/benchmarks/memory/server/runner.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { bench } from 'vitest' -import { memoryBenchOptions } from './bench-utils' -import type { ServerMemoryBenchmark } from './benchmark' - -export function registerServerMemoryBenches(test: ServerMemoryBenchmark) { - for (const memoryBench of test.benches) { - bench(memoryBench.name, memoryBench.run, memoryBenchOptions) - } -} diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts index 66aed269b9..0c98f54ddd 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/react/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('react', handler) +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/shared.ts b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts index efdfd6e139..7c4e506ed2 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/shared.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts @@ -290,7 +290,7 @@ async function runAbortedRequestLoop( } } -export function createSetup( +export function createWorkloadGroup( framework: Framework, handler: StartRequestHandler, ) { @@ -299,7 +299,7 @@ export function createSetup( return { sanity: () => assertAbortedRequestsSanity(handler, mode), - benches: [ + 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 index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts index 0a575854b5..4fe85c3f00 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/solid/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('solid', handler) +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/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts b/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts index df2908aec9..6683dafe6b 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/vue/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('vue', handler) +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/react/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/error-paths/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts index 66aed269b9..0c98f54ddd 100644 --- a/benchmarks/memory/server/scenarios/error-paths/react/setup.ts +++ b/benchmarks/memory/server/scenarios/error-paths/react/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('react', handler) +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/shared.ts b/benchmarks/memory/server/scenarios/error-paths/shared.ts index f552d33f2a..677ff661b9 100644 --- a/benchmarks/memory/server/scenarios/error-paths/shared.ts +++ b/benchmarks/memory/server/scenarios/error-paths/shared.ts @@ -151,7 +151,7 @@ async function assertErrorPathsSanity(handler: StartRequestHandler) { ) } -export function createSetup( +export function createWorkloadGroup( framework: Framework, handler: StartRequestHandler, ) { @@ -189,7 +189,7 @@ export function createSetup( return { sanity: () => assertErrorPathsSanity(handler), - benches: [ + workloads: [ { name: `mem error-paths redirect (${framework})`, run: runRedirect, diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/error-paths/solid/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/error-paths/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts b/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts index 0a575854b5..4fe85c3f00 100644 --- a/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts +++ b/benchmarks/memory/server/scenarios/error-paths/solid/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('solid', handler) +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/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/error-paths/vue/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/error-paths/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts b/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts index df2908aec9..6683dafe6b 100644 --- a/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts +++ b/benchmarks/memory/server/scenarios/error-paths/vue/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('vue', handler) +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/react/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts index 66aed269b9..0c98f54ddd 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/react/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('react', handler) +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/shared.ts b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts index 1d020fed22..7ee03caa3d 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/shared.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts @@ -51,7 +51,7 @@ async function assertPeakLargePageSanity(handler: StartRequestHandler) { validatePeakLargePageBody(body) } -export function createSetup( +export function createWorkloadGroup( framework: Framework, handler: StartRequestHandler, ) { @@ -65,7 +65,7 @@ export function createSetup( return { sanity: () => assertPeakLargePageSanity(handler), - benches: [ + 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 index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts index 0a575854b5..4fe85c3f00 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/solid/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('solid', handler) +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/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts index df2908aec9..6683dafe6b 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/vue/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('vue', handler) +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/react/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/request-churn/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts index 66aed269b9..0c98f54ddd 100644 --- a/benchmarks/memory/server/scenarios/request-churn/react/setup.ts +++ b/benchmarks/memory/server/scenarios/request-churn/react/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('react', handler) +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/shared.ts b/benchmarks/memory/server/scenarios/request-churn/shared.ts index d29c3c9685..174aa76dca 100644 --- a/benchmarks/memory/server/scenarios/request-churn/shared.ts +++ b/benchmarks/memory/server/scenarios/request-churn/shared.ts @@ -57,7 +57,7 @@ async function assertRequestChurnSanity(handler: StartRequestHandler) { } } -export function createSetup( +export function createWorkloadGroup( framework: Framework, handler: StartRequestHandler, ) { @@ -79,7 +79,7 @@ export function createSetup( return { sanity: () => assertRequestChurnSanity(handler), - benches: [ + 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 index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/request-churn/solid/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/request-churn/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts b/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts index 0a575854b5..4fe85c3f00 100644 --- a/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts +++ b/benchmarks/memory/server/scenarios/request-churn/solid/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('solid', handler) +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/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/request-churn/vue/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/request-churn/vue/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/request-churn/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts b/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts index df2908aec9..6683dafe6b 100644 --- a/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts +++ b/benchmarks/memory/server/scenarios/request-churn/vue/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('vue', handler) +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/react/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts index 66aed269b9..0c98f54ddd 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/react/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('react', handler) +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/shared.ts b/benchmarks/memory/server/scenarios/serialization-payload/shared.ts index 2fa7e4b5e4..cf6c86f1f9 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/shared.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/shared.ts @@ -82,7 +82,7 @@ async function assertSerializationPayloadSanity(handler: StartRequestHandler) { validatePayloadBody(body, response, request) } -export function createSetup( +export function createWorkloadGroup( framework: Framework, handler: StartRequestHandler, ) { @@ -96,7 +96,7 @@ export function createSetup( return { sanity: () => assertSerializationPayloadSanity(handler), - benches: [ + 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 index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts index 0a575854b5..4fe85c3f00 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/solid/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('solid', handler) +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/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts b/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts index df2908aec9..6683dafe6b 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/vue/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('vue', handler) +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/react/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts index 66aed269b9..0c98f54ddd 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('react', handler) +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/shared.ts b/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts index 1666bc5e7d..8a424672af 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts @@ -201,7 +201,7 @@ async function assertServerFnChurnSanity( validateEchoedBody(postBody, postRequest, postFixture.id) } -export async function createSetup( +export async function createWorkloadGroup( framework: Framework, handler: StartRequestHandler, ) { @@ -226,7 +226,7 @@ export async function createSetup( return { sanity: () => assertServerFnChurnSanity(handler, urls), - benches: [ + 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 index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts index 0a575854b5..4fe85c3f00 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('solid', handler) +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/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts b/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts index df2908aec9..6683dafe6b 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('vue', handler) +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/react/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts index 66aed269b9..0c98f54ddd 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/react/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('react', handler) +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/shared.ts b/benchmarks/memory/server/scenarios/streaming-peak/shared.ts index 627eea277f..cde9a7c51c 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/shared.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/shared.ts @@ -122,7 +122,7 @@ async function assertStreamingPeakSanity(handler: StartRequestHandler) { assertFallbacksPrecedeDeferredContent(chunked.body) } -export function createSetup( +export function createWorkloadGroup( framework: Framework, handler: StartRequestHandler, ) { @@ -136,7 +136,7 @@ export function createSetup( return { sanity: () => assertStreamingPeakSanity(handler), - benches: [ + 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 index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts index 0a575854b5..4fe85c3f00 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/solid/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('solid', handler) +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/vue/memory.bench.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts index 1dc2e0da86..9a5c211fee 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.bench.ts @@ -1,10 +1,11 @@ -import { describe } from 'vitest' -import { registerServerMemoryBenches } from '#memory-server/runner' -import { setup } from './setup' +import { bench, describe } from 'vitest' +import { memoryBenchOptions } from '#memory-server/bench-utils' +import { workloadGroup } from './setup' -const test = await setup() -await test.sanity() +await workloadGroup.sanity() describe('memory', () => { - registerServerMemoryBenches(test) + 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 index 882050f597..0182c472a8 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.flame.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/memory.flame.ts @@ -1,4 +1,4 @@ import { runServerFlameBenchmark } from '#memory-server/flame-runner' -import { setup } from './setup.ts' +import { workloadGroup } from './setup.ts' -await runServerFlameBenchmark(setup) +await runServerFlameBenchmark(workloadGroup) diff --git a/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts b/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts index df2908aec9..6683dafe6b 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/vue/setup.ts @@ -1,14 +1,14 @@ -import { createSetup } from '../shared' -import type { StartRequestHandler } from '../shared' +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 -export async function setup() { - const { default: handler } = (await import( - /* @vite-ignore */ appModuleUrl - )) as { - default: StartRequestHandler - } - - return createSetup('vue', handler) +const { default: handler } = (await import( + /* @vite-ignore */ appModuleUrl +)) as { + default: StartRequestHandler } + +export const workloadGroup: ServerMemoryWorkloadGroup = + await createWorkloadGroup('vue', handler) From b15de72c1d1a8d0001a0daefd1f32f24aa757e1d Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 14 Jun 2026 00:28:51 +0200 Subject: [PATCH 22/24] cleanup --- benchmarks/memory/client/lifecycle.ts | 48 +++++++ benchmarks/memory/client/package.json | 3 +- .../interrupted-navigations/react/src/app.tsx | 2 +- .../react/src/routes/fast.$id.tsx | 2 +- .../react/src/routes/slow.$id.tsx | 2 +- .../interrupted-navigations/shared.ts | 77 ++++------- .../{react/src => }/slow-loaders.ts | 0 .../interrupted-navigations/solid/src/app.tsx | 2 +- .../solid/src/routes/fast.$id.tsx | 2 +- .../solid/src/routes/slow.$id.tsx | 2 +- .../solid/src/slow-loaders.ts | 54 -------- .../interrupted-navigations/vue/src/app.tsx | 2 +- .../vue/src/routes/fast.$id.tsx | 2 +- .../vue/src/routes/slow.$id.tsx | 2 +- .../vue/src/slow-loaders.ts | 54 -------- .../{react/src => }/loader-data.ts | 0 .../loader-data-retention/react/src/app.tsx | 2 +- .../react/src/routes/page.$id.tsx | 2 +- .../scenarios/loader-data-retention/shared.ts | 60 +++----- .../loader-data-retention/solid/src/app.tsx | 2 +- .../solid/src/loader-data.ts | 48 ------- .../solid/src/routes/page.$id.tsx | 2 +- .../loader-data-retention/vue/src/app.tsx | 2 +- .../vue/src/loader-data.ts | 48 ------- .../vue/src/routes/page.$id.tsx | 2 +- .../client/scenarios/mount-unmount/shared.ts | 47 ++----- .../scenarios/navigation-churn/shared.ts | 55 +++----- .../{react/src => }/item-payload.ts | 0 .../scenarios/preload-churn/react/src/app.tsx | 2 +- .../react/src/routes/items.$id.tsx | 2 +- .../client/scenarios/preload-churn/shared.ts | 128 +++--------------- .../scenarios/preload-churn/solid/src/app.tsx | 2 +- .../preload-churn/solid/src/item-payload.ts | 51 ------- .../solid/src/routes/items.$id.tsx | 2 +- .../scenarios/preload-churn/vue/src/app.tsx | 2 +- .../preload-churn/vue/src/item-payload.ts | 51 ------- .../vue/src/routes/items.$id.tsx | 2 +- .../scenarios/unique-location-churn/shared.ts | 57 +++----- benchmarks/memory/client/tsconfig.json | 2 +- .../aborted-requests/deferred-records.ts | 18 +++ .../react/src/routes/stream.$id.tsx | 25 +--- .../scenarios/aborted-requests/shared.ts | 46 +------ .../solid/src/routes/stream.$id.tsx | 22 +-- .../vue/src/routes/stream.$id.tsx | 22 +-- .../server/scenarios/error-paths/shared.ts | 21 +-- .../{solid/src => }/large-page-data.ts | 0 .../react/src/large-page-data.ts | 111 --------------- .../src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 2 +- .../react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 2 +- .../react/src/routes/l1.l2.l3.l4.l5.l6.tsx | 2 +- .../react/src/routes/l1.l2.l3.l4.l5.tsx | 2 +- .../react/src/routes/l1.l2.l3.l4.tsx | 2 +- .../react/src/routes/l1.l2.l3.tsx | 2 +- .../react/src/routes/l1.l2.tsx | 2 +- .../peak-large-page/react/src/routes/l1.tsx | 2 +- .../scenarios/peak-large-page/shared.ts | 7 - .../src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 2 +- .../solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 2 +- .../solid/src/routes/l1.l2.l3.l4.l5.l6.tsx | 2 +- .../solid/src/routes/l1.l2.l3.l4.l5.tsx | 2 +- .../solid/src/routes/l1.l2.l3.l4.tsx | 2 +- .../solid/src/routes/l1.l2.l3.tsx | 2 +- .../solid/src/routes/l1.l2.tsx | 2 +- .../peak-large-page/solid/src/routes/l1.tsx | 2 +- .../vue/src/large-page-data.ts | 111 --------------- .../src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 2 +- .../vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 2 +- .../vue/src/routes/l1.l2.l3.l4.l5.l6.tsx | 2 +- .../vue/src/routes/l1.l2.l3.l4.l5.tsx | 2 +- .../vue/src/routes/l1.l2.l3.l4.tsx | 2 +- .../vue/src/routes/l1.l2.l3.tsx | 2 +- .../peak-large-page/vue/src/routes/l1.l2.tsx | 2 +- .../peak-large-page/vue/src/routes/l1.tsx | 2 +- .../server/scenarios/request-churn/shared.ts | 7 - .../react/src/routes/data.$id.tsx | 117 +--------------- .../serialization-payload.ts | 115 ++++++++++++++++ .../scenarios/serialization-payload/shared.ts | 35 +---- .../solid/src/routes/data.$id.tsx | 117 +--------------- .../vue/src/routes/data.$id.tsx | 117 +--------------- .../server-fn-churn/react/src/fns.ts | 39 ++---- .../server-fn-churn/server-fn-payload.ts | 33 +++++ .../scenarios/server-fn-churn/shared.ts | 4 - .../server-fn-churn/solid/src/fns.ts | 39 ++---- .../scenarios/server-fn-churn/vue/src/fns.ts | 39 ++---- .../streaming-peak/deferred-section-data.ts | 37 +++++ .../react/src/routes/stream.$id.tsx | 38 +----- .../server/scenarios/streaming-peak/shared.ts | 42 +----- .../solid/src/routes/stream.$id.tsx | 38 +----- .../vue/src/routes/stream.$id.tsx | 38 +----- 89 files changed, 498 insertions(+), 1613 deletions(-) create mode 100644 benchmarks/memory/client/lifecycle.ts rename benchmarks/memory/client/scenarios/interrupted-navigations/{react/src => }/slow-loaders.ts (100%) delete mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/slow-loaders.ts delete mode 100644 benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/slow-loaders.ts rename benchmarks/memory/client/scenarios/loader-data-retention/{react/src => }/loader-data.ts (100%) delete mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/solid/src/loader-data.ts delete mode 100644 benchmarks/memory/client/scenarios/loader-data-retention/vue/src/loader-data.ts rename benchmarks/memory/client/scenarios/preload-churn/{react/src => }/item-payload.ts (100%) delete mode 100644 benchmarks/memory/client/scenarios/preload-churn/solid/src/item-payload.ts delete mode 100644 benchmarks/memory/client/scenarios/preload-churn/vue/src/item-payload.ts create mode 100644 benchmarks/memory/server/scenarios/aborted-requests/deferred-records.ts rename benchmarks/memory/server/scenarios/peak-large-page/{solid/src => }/large-page-data.ts (100%) delete mode 100644 benchmarks/memory/server/scenarios/peak-large-page/react/src/large-page-data.ts delete mode 100644 benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts create mode 100644 benchmarks/memory/server/scenarios/serialization-payload/serialization-payload.ts create mode 100644 benchmarks/memory/server/scenarios/server-fn-churn/server-fn-payload.ts create mode 100644 benchmarks/memory/server/scenarios/streaming-peak/deferred-section-data.ts diff --git a/benchmarks/memory/client/lifecycle.ts b/benchmarks/memory/client/lifecycle.ts new file mode 100644 index 0000000000..78850e81b2 --- /dev/null +++ b/benchmarks/memory/client/lifecycle.ts @@ -0,0 +1,48 @@ +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 index 231ad6af58..2624b9e4e5 100644 --- a/benchmarks/memory/client/package.json +++ b/benchmarks/memory/client/package.json @@ -8,7 +8,8 @@ "imports": { "#memory-client/benchmark": "./benchmark.ts", "#memory-client/bench-utils": "./bench-utils.ts", - "#memory-client/flame-runner": "./flame-runner.ts" + "#memory-client/flame-runner": "./flame-runner.ts", + "#memory-client/lifecycle": "./lifecycle.ts" }, "dependencies": { "@tanstack/react-router": "workspace:*", diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx index c77230007a..bd605a78ee 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/app.tsx @@ -6,7 +6,7 @@ export { resolveAllSlowLoaders, resolveSlowLoader, slowLoaderRegistry, -} from './slow-loaders' +} from '../../slow-loaders' export function mountTestApp(container: Element) { const router = getRouter() 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 index 146b266f1b..d5fd7ade46 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { fixedTimestamp } from '../slow-loaders' +import { fixedTimestamp } from '../../../slow-loaders' export const Route = createFileRoute('/fast/$id')({ loader: ({ params }) => ({ 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 index 15c7d996dc..61bbf328b7 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { getSlowLoaderDeferred } from '../slow-loaders' +import { getSlowLoaderDeferred } from '../../../slow-loaders' export const Route = createFileRoute('/slow/$id')({ loader: async ({ params }) => { diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts index 87c91d82bf..0e01e92e08 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/shared.ts @@ -2,8 +2,15 @@ import { createDeterministicRandom, randomSegment, } from '#memory-client/bench-utils' - -type Framework = 'react' | 'solid' | 'vue' +import { + createBenchContainer, + drainMicrotasks, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' type NavigationSettlement = | { @@ -15,12 +22,6 @@ type NavigationSettlement = reason: unknown } -type MountedApp = { - router: unknown - unmount: () => void -} - -type MountTestApp = (container: HTMLDivElement) => MountedApp type ResolveAllSlowLoaders = () => void type ResolveSlowLoader = (id: string) => void type SlowLoaderRegistry = { @@ -47,11 +48,6 @@ type InterruptedNavigationRouter = { ) => () => void } -const frameworkNames = { - react: 'React', - solid: 'Solid', - vue: 'Vue', -} satisfies Record const interruptedNavigationIterations = 150 const interruptedNavigationPairs = createInterruptedNavigationPairs( interruptedNavigationIterations, @@ -68,14 +64,6 @@ const uninitializedSettlement = () => reason: new Error('interrupted-navigations benchmark is not initialized'), }) -function warnDevMode(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.`, - ) - } -} - function createInterruptedNavigationPairs(iterations: number) { const random = createDeterministicRandom(13) @@ -85,11 +73,6 @@ function createInterruptedNavigationPairs(iterations: number) { })) } -async function drainMicrotasks() { - await Promise.resolve() - await Promise.resolve() -} - function formatReason(reason: unknown) { if (reason instanceof Error) { return `${reason.name}: ${reason.message}` @@ -151,12 +134,12 @@ export function createWorkload( resolveSlowLoader: ResolveSlowLoader, slowLoaderRegistry: SlowLoaderRegistry, ) { - warnDevMode(framework) + warnClientMemoryDevMode(framework) let container: HTMLDivElement | undefined = undefined - let unmount: (() => void) | undefined = undefined - let unsub = () => {} - let resolveRendered: () => void = () => {} + 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 = @@ -183,9 +166,7 @@ export function createWorkload( assertRenderedPage(page, id) return } catch { - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) + await nextAnimationFrame() } } @@ -217,8 +198,7 @@ export function createWorkload( after() } - container = document.createElement('div') - document.body.append(container) + container = createBenchContainer() const mounted = mountTestApp(container) const router = mounted.router as InterruptedNavigationRouter @@ -234,7 +214,7 @@ export function createWorkload( } const resolve = resolveRendered - resolveRendered = () => {} + resolveRendered = noop expectedRenderedPath = undefined resolve() }) @@ -274,14 +254,14 @@ export function createWorkload( function after() { resolveAllSlowLoaders() - unmount?.() - container?.remove() + unmount() + removeBenchContainer(container) unsub() container = undefined - unmount = undefined - unsub = () => {} - resolveRendered = () => {} + unmount = noop + unsub = noop + resolveRendered = noop expectedRenderedPath = undefined navigateFast = uninitialized startSlowNavigation = uninitializedSettlement @@ -291,7 +271,7 @@ export function createWorkload( async function interrupt( slowId: string, fastId: string, - assertShape = false, + assertSettlement = true, ) { const slowNavigation = startSlowNavigation(slowId) @@ -306,13 +286,13 @@ export function createWorkload( resolveSlowLoader(slowId) const settlement = await slowNavigation - assertSlowNavigationSettlement(settlement) - await awaitExpectedLoadSettlement(slowLoadPromise) - await drainMicrotasks() - if (assertShape) { - assertRenderedPage('fast', fastId) + if (assertSettlement) { + assertSlowNavigationSettlement(settlement) } + + await awaitExpectedLoadSettlement(slowLoadPromise) + await drainMicrotasks() } return { @@ -329,7 +309,8 @@ export function createWorkload( try { assertRenderedPage('shell') - await interrupt('sanity-slow', 'sanity-fast', true) + await interrupt('sanity-slow', 'sanity-fast', false) + assertRenderedPage('fast', 'sanity-fast') } finally { after() } diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/react/src/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/slow-loaders.ts similarity index 100% rename from benchmarks/memory/client/scenarios/interrupted-navigations/react/src/slow-loaders.ts rename to benchmarks/memory/client/scenarios/interrupted-navigations/slow-loaders.ts diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx index 47aa9d8a37..6288b5fab1 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/app.tsx @@ -6,7 +6,7 @@ export { resolveAllSlowLoaders, resolveSlowLoader, slowLoaderRegistry, -} from './slow-loaders' +} from '../../slow-loaders' export function mountTestApp(container: Element) { const router = getRouter() 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 index d1871d287b..511e4712b7 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/solid-router' -import { fixedTimestamp } from '../slow-loaders' +import { fixedTimestamp } from '../../../slow-loaders' export const Route = createFileRoute('/fast/$id')({ loader: ({ params }) => ({ 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 index 1e645569b7..9aaaef5568 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/solid-router' -import { getSlowLoaderDeferred } from '../slow-loaders' +import { getSlowLoaderDeferred } from '../../../slow-loaders' export const Route = createFileRoute('/slow/$id')({ loader: async ({ params }) => { diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/slow-loaders.ts deleted file mode 100644 index c70ccac3da..0000000000 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/solid/src/slow-loaders.ts +++ /dev/null @@ -1,54 +0,0 @@ -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/vue/src/app.tsx b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx index 4257601dbd..1abb4410dc 100644 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx +++ b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/app.tsx @@ -7,7 +7,7 @@ export { resolveAllSlowLoaders, resolveSlowLoader, slowLoaderRegistry, -} from './slow-loaders' +} from '../../slow-loaders' export function mountTestApp(container: Element) { const router = getRouter() 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 index ff5e828909..3bd820bd07 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/vue-router' -import { fixedTimestamp } from '../slow-loaders' +import { fixedTimestamp } from '../../../slow-loaders' export const Route = createFileRoute('/fast/$id')({ loader: ({ params }: { params: { id: string } }) => ({ 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 index 5ac5b44d44..68885fb918 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/vue-router' -import { getSlowLoaderDeferred } from '../slow-loaders' +import { getSlowLoaderDeferred } from '../../../slow-loaders' export const Route = createFileRoute('/slow/$id')({ loader: async ({ params }: { params: { id: string } }) => { diff --git a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/slow-loaders.ts b/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/slow-loaders.ts deleted file mode 100644 index c70ccac3da..0000000000 --- a/benchmarks/memory/client/scenarios/interrupted-navigations/vue/src/slow-loaders.ts +++ /dev/null @@ -1,54 +0,0 @@ -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/loader-data-retention/react/src/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/loader-data.ts similarity index 100% rename from benchmarks/memory/client/scenarios/loader-data-retention/react/src/loader-data.ts rename to benchmarks/memory/client/scenarios/loader-data-retention/loader-data.ts 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 index 8e09296b9c..5eef150c2c 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx +++ b/benchmarks/memory/client/scenarios/loader-data-retention/react/src/app.tsx @@ -2,7 +2,7 @@ import { RouterProvider } from '@tanstack/react-router' import { createRoot } from 'react-dom/client' import { getRouter } from './router' -export { loaderPayloadRecordCount } from './loader-data' +export { loaderPayloadRecordCount } from '../../loader-data' export function mountTestApp(container: Element) { const router = getRouter() 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 index bb387a7034..63af9ed429 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { createLoaderData } from '../loader-data' +import { createLoaderData } from '../../../loader-data' export const Route = createFileRoute('/page/$id')({ loader: ({ params }) => createLoaderData(params.id), diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts index 84f9b71bc1..a5c496cb81 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts +++ b/benchmarks/memory/client/scenarios/loader-data-retention/shared.ts @@ -2,15 +2,14 @@ import { createDeterministicRandom, randomSegment, } from '#memory-client/bench-utils' - -type Framework = 'react' | 'solid' | 'vue' - -type MountedApp = { - router: unknown - unmount: () => void -} - -type MountTestApp = (container: HTMLDivElement) => MountedApp +import { + createBenchContainer, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' type RenderEvent = { toLocation: { @@ -31,11 +30,6 @@ type LoaderDataRouter = { ) => () => void } -const frameworkNames = { - react: 'React', - solid: 'Solid', - vue: 'Vue', -} satisfies Record const loaderDataRetentionNavigationCount = 20 const pageIds = createPageIds() @@ -44,14 +38,6 @@ const uninitialized = () => new Error('loader-data-retention benchmark is not initialized'), ) -function warnDevMode(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.`, - ) - } -} - function createPageIds() { const random = createDeterministicRandom(11) @@ -66,12 +52,12 @@ export function createWorkload( mountTestApp: MountTestApp, loaderPayloadRecordCount: number, ) { - warnDevMode(framework) + warnClientMemoryDevMode(framework) let container: HTMLDivElement | undefined = undefined - let unmount: (() => void) | undefined = undefined - let unsub = () => {} - let resolveRendered: () => void = () => {} + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop let expectedRenderedPath: string | undefined = undefined let navigateTo: (id: string) => Promise = uninitialized @@ -106,9 +92,7 @@ export function createWorkload( assertRenderedShell() return } catch { - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) + await nextAnimationFrame() } } @@ -128,8 +112,7 @@ export function createWorkload( after() } - container = document.createElement('div') - document.body.append(container) + container = createBenchContainer() const mounted = mountTestApp(container) const router = mounted.router as LoaderDataRouter @@ -144,7 +127,7 @@ export function createWorkload( } const resolve = resolveRendered - resolveRendered = () => {} + resolveRendered = noop expectedRenderedPath = undefined resolve() }) @@ -167,14 +150,14 @@ export function createWorkload( } function after() { - unmount?.() - container?.remove() + unmount() + removeBenchContainer(container) unsub() container = undefined - unmount = undefined - unsub = () => {} - resolveRendered = () => {} + unmount = noop + unsub = noop + resolveRendered = noop expectedRenderedPath = undefined navigateTo = uninitialized } @@ -192,10 +175,9 @@ export function createWorkload( await before() try { + assertRenderedShell() await navigateTo('sanity-a') assertRenderedPage('sanity-a') - await navigateTo('sanity-b') - assertRenderedPage('sanity-b') } finally { after() } 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 index 453f50f330..4315a4b15c 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx +++ b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/app.tsx @@ -2,7 +2,7 @@ import { RouterProvider } from '@tanstack/solid-router' import { render } from 'solid-js/web' import { getRouter } from './router' -export { loaderPayloadRecordCount } from './loader-data' +export { loaderPayloadRecordCount } from '../../loader-data' export function mountTestApp(container: Element) { const router = getRouter() diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/loader-data.ts deleted file mode 100644 index 3bd59c82ef..0000000000 --- a/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/loader-data.ts +++ /dev/null @@ -1,48 +0,0 @@ -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/solid/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/solid/src/routes/page.$id.tsx index 3968848038..fb6c438fa5 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/solid-router' -import { createLoaderData } from '../loader-data' +import { createLoaderData } from '../../../loader-data' export const Route = createFileRoute('/page/$id')({ loader: ({ params }) => createLoaderData(params.id), 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 index e9d54137ae..c843b4feb5 100644 --- a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx +++ b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/app.tsx @@ -3,7 +3,7 @@ import { createApp } from 'vue' import { getRouter } from './router' import type {} from '@tanstack/router-core' -export { loaderPayloadRecordCount } from './loader-data' +export { loaderPayloadRecordCount } from '../../loader-data' export function mountTestApp(container: Element) { const router = getRouter() diff --git a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/loader-data.ts b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/loader-data.ts deleted file mode 100644 index 3bd59c82ef..0000000000 --- a/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/loader-data.ts +++ /dev/null @@ -1,48 +0,0 @@ -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/vue/src/routes/page.$id.tsx b/benchmarks/memory/client/scenarios/loader-data-retention/vue/src/routes/page.$id.tsx index 570ea3fb68..9f4b3c4470 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/vue-router' -import { createLoaderData } from '../loader-data' +import { createLoaderData } from '../../../loader-data' export const Route = createFileRoute('/page/$id')({ loader: ({ params }: { params: { id: string } }) => diff --git a/benchmarks/memory/client/scenarios/mount-unmount/shared.ts b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts index ed3702f876..4f9b9f430f 100644 --- a/benchmarks/memory/client/scenarios/mount-unmount/shared.ts +++ b/benchmarks/memory/client/scenarios/mount-unmount/shared.ts @@ -1,36 +1,19 @@ -type Framework = 'react' | 'solid' | 'vue' - -type MountedApp = { - router: unknown - unmount: () => void -} - -type MountTestApp = (container: HTMLDivElement) => MountedApp +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 frameworkNames = { - react: 'React', - solid: 'Solid', - vue: 'Vue', -} satisfies Record const mountUnmountIterations = 100 -function warnDevMode(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.`, - ) - } -} - -function drainMicrotasks() { - return Promise.resolve().then(() => Promise.resolve()) -} - function assertEmptyBody() { if (document.body.childNodes.length !== 0) { throw new Error( @@ -43,14 +26,13 @@ export function createWorkload( framework: Framework, mountTestApp: MountTestApp, ) { - warnDevMode(framework) + warnClientMemoryDevMode(framework) async function cycle() { - const container = document.createElement('div') - document.body.append(container) + const container = createBenchContainer() - let unmount = () => {} - let unsubscribe = () => {} + let unmount = noop + let unsubscribe = noop try { const mounted = mountTestApp(container) @@ -66,10 +48,10 @@ export function createWorkload( await router.load() await rendered unsubscribe() - unsubscribe = () => {} + unsubscribe = noop } finally { unmount() - container.remove() + removeBenchContainer(container) unsubscribe() await drainMicrotasks() } @@ -86,7 +68,6 @@ export function createWorkload( async sanity() { assertEmptyBody() await cycle() - await cycle() assertEmptyBody() }, } diff --git a/benchmarks/memory/client/scenarios/navigation-churn/shared.ts b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts index 595cfd4bf3..0745de2116 100644 --- a/benchmarks/memory/client/scenarios/navigation-churn/shared.ts +++ b/benchmarks/memory/client/scenarios/navigation-churn/shared.ts @@ -1,12 +1,13 @@ -type Framework = 'react' | 'solid' | 'vue' -type Target = '/a' | '/b' - -type MountedApp = { - router: unknown - unmount: () => void -} +import { + createBenchContainer, + nextAnimationFrame, + noop, + removeBenchContainer, + warnClientMemoryDevMode, +} from '#memory-client/lifecycle' +import type { Framework, MountTestApp } from '#memory-client/lifecycle' -type MountTestApp = (container: HTMLDivElement) => MountedApp +type Target = '/a' | '/b' type NavigationRouter = { load: () => Promise @@ -14,34 +15,21 @@ type NavigationRouter = { subscribe: (event: 'onRendered', listener: () => void) => () => void } -const frameworkNames = { - react: 'React', - solid: 'Solid', - vue: 'Vue', -} satisfies Record const navigationChurnIterations = 300 const uninitialized = () => Promise.reject(new Error('navigation-churn benchmark is not initialized')) -function warnDevMode(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 createWorkload( framework: Framework, mountTestApp: MountTestApp, ) { - warnDevMode(framework) + warnClientMemoryDevMode(framework) let container: HTMLDivElement | undefined = undefined - let unmount: (() => void) | undefined = undefined - let unsub = () => {} - let resolveRendered: () => void = () => {} + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop let navigateTo: (target: Target) => Promise = uninitialized function assertRenderedPage(target: Target) { @@ -61,9 +49,7 @@ export function createWorkload( assertRenderedPage(target) return } catch { - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) + await nextAnimationFrame() } } @@ -81,8 +67,7 @@ export function createWorkload( after() } - container = document.createElement('div') - document.body.append(container) + container = createBenchContainer() const mounted = mountTestApp(container) const router = mounted.router as NavigationRouter @@ -108,14 +93,14 @@ export function createWorkload( } function after() { - unmount?.() - container?.remove() + unmount() + removeBenchContainer(container) unsub() container = undefined - unmount = undefined - unsub = () => {} - resolveRendered = () => {} + unmount = noop + unsub = noop + resolveRendered = noop navigateTo = uninitialized } diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/item-payload.ts similarity index 100% rename from benchmarks/memory/client/scenarios/preload-churn/react/src/item-payload.ts rename to benchmarks/memory/client/scenarios/preload-churn/item-payload.ts diff --git a/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx index 8e60bad71d..b75d1fe34d 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx +++ b/benchmarks/memory/client/scenarios/preload-churn/react/src/app.tsx @@ -2,7 +2,7 @@ import { RouterProvider } from '@tanstack/react-router' import { createRoot } from 'react-dom/client' import { getRouter } from './router' -export { getTrackedItemLoaderCount } from './item-payload' +export { getTrackedItemLoaderCount } from '../../item-payload' export function mountTestApp(container: Element) { const router = getRouter() 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 index 36c8579ddf..2f52acd528 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { createItemPayload, trackItemLoaderCall } from '../item-payload' +import { createItemPayload, trackItemLoaderCall } from '../../../item-payload' export const Route = createFileRoute('/items/$id')({ loader: ({ params }) => { diff --git a/benchmarks/memory/client/scenarios/preload-churn/shared.ts b/benchmarks/memory/client/scenarios/preload-churn/shared.ts index ab428943f3..78e35c42ad 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/shared.ts +++ b/benchmarks/memory/client/scenarios/preload-churn/shared.ts @@ -2,15 +2,16 @@ 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 Framework = 'react' | 'solid' | 'vue' - -type MountedApp = { - router: unknown - unmount: () => void -} - -type MountTestApp = (container: HTMLDivElement) => MountedApp type GetTrackedItemLoaderCount = (id: string) => number type PreloadRouter = { @@ -32,19 +33,8 @@ type PreloadRouter = { }, ) => Promise subscribe: (event: 'onRendered', listener: () => void) => () => void - stores: { - cachedMatches: { - get: () => unknown - } - } } -const frameworkNames = { - react: 'React', - solid: 'Solid', - vue: 'Vue', -} satisfies Record - // 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' @@ -64,31 +54,18 @@ const uninitialized = async (_id: string) => { throw new Error('preload-churn benchmark is not initialized') } -function warnDevMode(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.`, - ) - } -} - -async function drainMicrotasks() { - await Promise.resolve() - await Promise.resolve() -} - export function createWorkload( framework: Framework, mountTestApp: MountTestApp, getTrackedItemLoaderCount: GetTrackedItemLoaderCount, ) { - warnDevMode(framework) + warnClientMemoryDevMode(framework) let container: HTMLDivElement | undefined = undefined let router: PreloadRouter | undefined = undefined - let unmount: (() => void) | undefined = undefined - let unsub = () => {} - let resolveRendered: () => void = () => {} + let unmount = noop + let unsub = noop + let resolveRendered = noop let evictionParity = 0 let preloadItem: (id: string) => Promise = uninitialized let navigateToItem: (id: string) => Promise = uninitialized @@ -105,56 +82,19 @@ export function createWorkload( } } - function assertRenderedItem(id: string) { - const page = - container?.querySelector('[data-bench-page]')?.dataset - .benchPage - const actualId = - container?.querySelector('[data-bench-id]')?.dataset.benchId - - if (page !== 'item' || actualId !== id) { - throw new Error(`Expected rendered item ${id}, got ${page}:${actualId}`) - } - } - - function hasCachedItemMatch(id: string) { - const cachedMatches = router?.stores.cachedMatches.get() as - | Array<{ params: { id?: string } }> - | undefined - - return Boolean(cachedMatches?.some((match) => match.params.id === id)) - } - async function waitForRenderedIndex() { for (let attempt = 0; attempt < 10; attempt++) { try { assertRenderedIndex() return } catch { - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) + await nextAnimationFrame() } } assertRenderedIndex() } - async function waitForRenderedItem(id: string) { - for (let attempt = 0; attempt < 10; attempt++) { - try { - assertRenderedItem(id) - return - } catch { - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) - } - } - - assertRenderedItem(id) - } - function waitForNextRender() { return new Promise((resolve) => { resolveRendered = resolve @@ -166,8 +106,7 @@ export function createWorkload( after() } - container = document.createElement('div') - document.body.append(container) + container = createBenchContainer() const mounted = mountTestApp(container) router = mounted.router as PreloadRouter @@ -213,15 +152,15 @@ export function createWorkload( } function after() { - unmount?.() - container?.remove() + unmount() + removeBenchContainer(container) unsub() container = undefined router = undefined - unmount = undefined - unsub = () => {} - resolveRendered = () => {} + unmount = noop + unsub = noop + resolveRendered = noop evictionParity = 0 preloadItem = uninitialized navigateToItem = uninitialized @@ -272,33 +211,6 @@ export function createWorkload( `Expected preload to run item loader once, got ${preloadedLoaderCount - initialLoaderCount}`, ) } - - if (!hasCachedItemMatch(id)) { - throw new Error( - 'Expected preloaded match to sit in router.state.cachedMatches', - ) - } - - // A navigation commit runs clearExpiredCache; with - // defaultPreloadGcTime: 0 it must evict the preloaded match. This is - // the mechanism the bench's flat-floor expectation rests on. - await navigateToItem('sanity-evict-nav') - await waitForRenderedItem('sanity-evict-nav') - - if (hasCachedItemMatch(id)) { - throw new Error( - 'Expected the navigation commit to evict the preloaded match (preloadGcTime 0)', - ) - } - - await preloadItem(id) - - const repreloadedLoaderCount = getTrackedItemLoaderCount(id) - if (repreloadedLoaderCount !== preloadedLoaderCount + 1) { - throw new Error( - 'Expected re-preload after eviction to run the item loader again', - ) - } } finally { after() } diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx index 3dfe005e5c..18ad5b97e4 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx +++ b/benchmarks/memory/client/scenarios/preload-churn/solid/src/app.tsx @@ -2,7 +2,7 @@ import { RouterProvider } from '@tanstack/solid-router' import { render } from 'solid-js/web' import { getRouter } from './router' -export { getTrackedItemLoaderCount } from './item-payload' +export { getTrackedItemLoaderCount } from '../../item-payload' export function mountTestApp(container: Element) { const router = getRouter() diff --git a/benchmarks/memory/client/scenarios/preload-churn/solid/src/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/solid/src/item-payload.ts deleted file mode 100644 index 4a86ea7244..0000000000 --- a/benchmarks/memory/client/scenarios/preload-churn/solid/src/item-payload.ts +++ /dev/null @@ -1,51 +0,0 @@ -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/solid/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/solid/src/routes/items.$id.tsx index 158a6213ac..15fa2a34ab 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/solid-router' -import { createItemPayload, trackItemLoaderCall } from '../item-payload' +import { createItemPayload, trackItemLoaderCall } from '../../../item-payload' export const Route = createFileRoute('/items/$id')({ loader: ({ params }) => { diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx index 8040fc57f9..890447d88b 100644 --- a/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx +++ b/benchmarks/memory/client/scenarios/preload-churn/vue/src/app.tsx @@ -3,7 +3,7 @@ import { createApp } from 'vue' import { getRouter } from './router' import type {} from '@tanstack/router-core' -export { getTrackedItemLoaderCount } from './item-payload' +export { getTrackedItemLoaderCount } from '../../item-payload' export function mountTestApp(container: Element) { const router = getRouter() diff --git a/benchmarks/memory/client/scenarios/preload-churn/vue/src/item-payload.ts b/benchmarks/memory/client/scenarios/preload-churn/vue/src/item-payload.ts deleted file mode 100644 index 4a86ea7244..0000000000 --- a/benchmarks/memory/client/scenarios/preload-churn/vue/src/item-payload.ts +++ /dev/null @@ -1,51 +0,0 @@ -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/vue/src/routes/items.$id.tsx b/benchmarks/memory/client/scenarios/preload-churn/vue/src/routes/items.$id.tsx index 4a39ea7bfd..2a29245926 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/vue-router' -import { createItemPayload, trackItemLoaderCall } from '../item-payload' +import { createItemPayload, trackItemLoaderCall } from '../../../item-payload' export const Route = createFileRoute('/items/$id')({ loader: ({ params }: { params: { id: string } }) => { diff --git a/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts index 43f85b3369..95b382269d 100644 --- a/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts +++ b/benchmarks/memory/client/scenarios/unique-location-churn/shared.ts @@ -2,21 +2,20 @@ import { createDeterministicRandom, randomSegment, } from '#memory-client/bench-utils' - -type Framework = 'react' | 'solid' | 'vue' +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 MountedApp = { - router: unknown - unmount: () => void -} - -type MountTestApp = (container: HTMLDivElement) => MountedApp - type NavigationRouter = { load: () => Promise navigate: (options: { @@ -28,11 +27,6 @@ type NavigationRouter = { subscribe: (event: 'onRendered', listener: () => void) => () => void } -const frameworkNames = { - react: 'React', - solid: 'Solid', - vue: 'Vue', -} satisfies Record 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. @@ -44,24 +38,16 @@ const uninitialized = () => new Error('unique-location-churn benchmark is not initialized'), ) -function warnDevMode(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 createWorkload( framework: Framework, mountTestApp: MountTestApp, ) { - warnDevMode(framework) + warnClientMemoryDevMode(framework) let container: HTMLDivElement | undefined = undefined - let unmount: (() => void) | undefined = undefined - let unsub = () => {} - let resolveRendered: () => void = () => {} + let unmount = noop + let unsub = noop + let resolveRendered: () => void = noop let navigateTo: (location: ItemLocation) => Promise = uninitialized function assertRenderedId(expected: string) { @@ -79,9 +65,7 @@ export function createWorkload( assertRenderedId(expected) return } catch { - await new Promise((resolve) => { - requestAnimationFrame(() => resolve()) - }) + await nextAnimationFrame() } } @@ -99,8 +83,7 @@ export function createWorkload( after() } - container = document.createElement('div') - document.body.append(container) + container = createBenchContainer() const mounted = mountTestApp(container) const router = mounted.router as NavigationRouter @@ -128,14 +111,14 @@ export function createWorkload( } function after() { - unmount?.() - container?.remove() + unmount() + removeBenchContainer(container) unsub() container = undefined - unmount = undefined - unsub = () => {} - resolveRendered = () => {} + unmount = noop + unsub = noop + resolveRendered = noop navigateTo = uninitialized } @@ -157,8 +140,6 @@ export function createWorkload( try { await navigateTo({ id: 'sanity-one', q: 'q-sanity-one' }) assertRenderedId('sanity-one') - await navigateTo({ id: 'sanity-two', q: 'q-sanity-two' }) - assertRenderedId('sanity-two') } finally { after() } diff --git a/benchmarks/memory/client/tsconfig.json b/benchmarks/memory/client/tsconfig.json index 5350368145..9280d5c42f 100644 --- a/benchmarks/memory/client/tsconfig.json +++ b/benchmarks/memory/client/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "types": ["node", "vite/client", "vitest/globals"] }, - "include": ["bench-utils.ts", "vitest.setup.ts"] + "include": ["bench-utils.ts", "lifecycle.ts", "vitest.setup.ts"] } 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/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/aborted-requests/react/src/routes/stream.$id.tsx index 8772428338..ecd9ef524d 100644 --- 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 @@ -1,14 +1,6 @@ import { Await, createFileRoute } from '@tanstack/react-router' import { Suspense } from 'react' - -const recordCount = 20 - -type RecordGroup = 'alpha' | 'beta' - -export interface DeferredRecord { - id: string - label: string -} +import { makeAbortedRequestRecords } from '../../../deferred-records' function resolveAfterMicrotasks(microtasks: number, value: () => T) { let promise = Promise.resolve() @@ -20,18 +12,15 @@ function resolveAfterMicrotasks(microtasks: number, value: () => T) { return promise.then(value) } -function makeRecords(id: string, group: RecordGroup): Array { - return Array.from({ length: recordCount }, (_, index) => ({ - id: `${group}-${id}-${index}`, - label: `deferred-${group}-${id}-${index}`, - })) -} - export const Route = createFileRoute('/stream/$id')({ loader: ({ params }) => ({ eager: `eager-${params.id}`, - alpha: resolveAfterMicrotasks(32, () => makeRecords(params.id, 'alpha')), - beta: resolveAfterMicrotasks(64, () => makeRecords(params.id, 'beta')), + alpha: resolveAfterMicrotasks(32, () => + makeAbortedRequestRecords(params.id, 'alpha'), + ), + beta: resolveAfterMicrotasks(64, () => + makeAbortedRequestRecords(params.id, 'beta'), + ), }), component: StreamComponent, }) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/shared.ts b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts index 7c4e506ed2..ee80d62933 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/shared.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts @@ -120,7 +120,7 @@ async function readShellBeforeDeferred( if (text.includes(marker)) { await reader.cancel() throw new Error( - `Shell sanity chunks already included deferred content ${marker}`, + `Shell chunks already included deferred content ${marker}`, ) } } @@ -138,16 +138,7 @@ async function readShellBeforeDeferred( } } -async function readSanityStream( - mode: AbortedRequestReadMode, - response: Response, - request: Request, - id: string, -) { - if (mode === 'shell-before-deferred') { - return readShellBeforeDeferred(response, request, id) - } - +async function readSanityStream(response: Response, request: Request) { const { reader, value } = await readFirstChunk(response, request) return { @@ -212,17 +203,6 @@ async function assertAbortedRequestsSanity( throw new Error('Expected full sanity response to include the eager marker') } - for (const marker of [ - alphaFirstRecord(fullId), - alphaLastRecord(fullId), - betaFirstRecord(fullId), - betaLastRecord(fullId), - ]) { - if (!fullBody.includes(marker)) { - throw new Error(`Expected full sanity response to include ${marker}`) - } - } - const midStreamId = 'sanity-mid-stream' const controller = new AbortController() const midStreamRequest = buildStreamRequest(midStreamId, controller.signal) @@ -230,36 +210,14 @@ async function assertAbortedRequestsSanity( validateDocumentResponse(midStreamResponse, midStreamRequest) const { reader, text } = await readSanityStream( - mode.readMode, midStreamResponse, midStreamRequest, - midStreamId, ) if (!text.includes(eagerMarker)) { throw new Error('Expected first sanity chunk to include the eager marker') } - if ( - !text.includes(alphaFallbackMarker) || - !text.includes(betaFallbackMarker) - ) { - throw new Error('Expected first sanity chunk to include deferred fallbacks') - } - - for (const marker of [ - alphaFirstRecord(midStreamId), - alphaLastRecord(midStreamId), - betaFirstRecord(midStreamId), - betaLastRecord(midStreamId), - ]) { - if (text.includes(marker)) { - throw new Error( - `First sanity chunk already included deferred content ${marker}`, - ) - } - } - // reader.cancel() is the response-stream cancellation path if the handler // does not observe Request.signal for this in-process request. controller.abort() 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 index 60a9e8e82b..95d69ad91d 100644 --- 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 @@ -1,19 +1,16 @@ import { createFileRoute } from '@tanstack/solid-router' import { Show, Suspense, createResource } from 'solid-js' +import { + makeAbortedRequestRecords, + type DeferredRecord, + type RecordGroup, +} from '../../../deferred-records' -const recordCount = 20 const alphaDelayMs = 50 const betaDelayMs = 75 const abortProbeAlphaDelayMs = 500 const abortProbeBetaDelayMs = 750 -type RecordGroup = 'alpha' | 'beta' - -export interface DeferredRecord { - id: string - label: string -} - function isAbortProbeId(id: string) { return id === 'sanity-mid-stream' || id.startsWith('abort-') } @@ -62,18 +59,11 @@ function makeDeferredRecords( return resolveAfterDelay( delayMs, signal, - () => makeRecords(id, group), + () => makeAbortedRequestRecords(id, group), () => [], ) } -function makeRecords(id: string, group: RecordGroup): Array { - return Array.from({ length: recordCount }, (_, index) => ({ - id: `${group}-${id}-${index}`, - label: `deferred-${group}-${id}-${index}`, - })) -} - export const Route = createFileRoute('/stream/$id')({ loader: ({ params, abortController }) => ({ eager: `eager-${params.id}`, 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 index 2c11ab569a..d064bf75e9 100644 --- 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 @@ -1,19 +1,16 @@ import { Await, createFileRoute } from '@tanstack/vue-router' import { Suspense } from 'vue' +import { + makeAbortedRequestRecords, + type DeferredRecord, + type RecordGroup, +} from '../../../deferred-records' -const recordCount = 20 const alphaDelayMs = 50 const betaDelayMs = 75 const abortProbeAlphaDelayMs = 500 const abortProbeBetaDelayMs = 750 -type RecordGroup = 'alpha' | 'beta' - -export interface DeferredRecord { - id: string - label: string -} - function isAbortProbeId(id: string) { return id === 'sanity-mid-stream' || id.startsWith('abort-') } @@ -62,18 +59,11 @@ function makeDeferredRecords( return resolveAfterDelay( delayMs, signal, - () => makeRecords(id, group), + () => makeAbortedRequestRecords(id, group), () => [], ) } -function makeRecords(id: string, group: RecordGroup): Array { - return Array.from({ length: recordCount }, (_, index) => ({ - id: `${group}-${id}-${index}`, - label: `deferred-${group}-${id}-${index}`, - })) -} - export const Route = createFileRoute('/stream/$id')({ loader: ({ params, abortController }) => ({ eager: `eager-${params.id}`, diff --git a/benchmarks/memory/server/scenarios/error-paths/shared.ts b/benchmarks/memory/server/scenarios/error-paths/shared.ts index 677ff661b9..d4c1877465 100644 --- a/benchmarks/memory/server/scenarios/error-paths/shared.ts +++ b/benchmarks/memory/server/scenarios/error-paths/shared.ts @@ -17,8 +17,6 @@ const unmatchedSeed = 0xdecaf00d const redirectStatus = 302 const notFoundStatus = 404 const errorStatus = 500 -const notFoundMarker = 'data-bench="not-found-boundary"' -const errorMarker = 'data-bench="error-boundary"' // Module-level so each error-path bench keeps advancing across runner invocations. const redirectRandom = createDeterministicRandom(redirectSeed) const notFoundRandom = createDeterministicRandom(notFoundSeed) @@ -101,29 +99,14 @@ function validateErrorResponse(response: Response, request: Request) { } } -function validateNotFoundBody(body: string) { - if (!body.includes(notFoundMarker)) { - throw new Error('Expected error-paths not-found marker in response body') - } -} - -function validateErrorBody(body: string) { - if (!body.includes(errorMarker)) { - throw new Error('Expected error-paths error marker in response body') - } -} - async function assertStatusSanity( handler: StartRequestHandler, request: Request, validateResponse: (response: Response, request: Request) => void, - validateBody?: (body: string) => void, ) { const response = await handler.fetch(request) validateResponse(response, request) - - const body = await response.text() - validateBody?.(body) + await response.text() } async function assertErrorPathsSanity(handler: StartRequestHandler) { @@ -136,13 +119,11 @@ async function assertErrorPathsSanity(handler: StartRequestHandler) { handler, new Request('http://localhost/missing/sanity-missing', requestInit), validateNotFoundResponse, - validateNotFoundBody, ) await assertStatusSanity( handler, new Request('http://localhost/boom/sanity-error', requestInit), validateErrorResponse, - validateErrorBody, ) await assertStatusSanity( handler, diff --git a/benchmarks/memory/server/scenarios/peak-large-page/solid/src/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/large-page-data.ts similarity index 100% rename from benchmarks/memory/server/scenarios/peak-large-page/solid/src/large-page-data.ts rename to benchmarks/memory/server/scenarios/peak-large-page/large-page-data.ts diff --git a/benchmarks/memory/server/scenarios/peak-large-page/react/src/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/react/src/large-page-data.ts deleted file mode 100644 index a0cd3b4975..0000000000 --- a/benchmarks/memory/server/scenarios/peak-large-page/react/src/large-page-data.ts +++ /dev/null @@ -1,111 +0,0 @@ -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/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 index 036a2a1911..98e6fd2aa4 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ loader: () => makeLargePageLevelData(8, 0x5eed_1008), 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 index 0aceecf51b..dda157406c 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ loader: () => makeLargePageLevelData(7, 0x5eed_1007), 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 index 1bc6588434..aeaf96cf36 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ loader: () => makeLargePageLevelData(6, 0x5eed_1006), 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 index bbece2972c..74838de0a3 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ loader: () => makeLargePageLevelData(5, 0x5eed_1005), 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 index 62822e75ea..7dcaf8e417 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4')({ loader: () => makeLargePageLevelData(4, 0x5eed_1004), 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 index 4a74984f35..4e59288fd1 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3')({ loader: () => makeLargePageLevelData(3, 0x5eed_1003), 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 index a283fe04cb..d3f39a5f87 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2')({ loader: () => makeLargePageLevelData(2, 0x5eed_1002), 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 index 469a129b37..26f9d00281 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1')({ loader: () => makeLargePageLevelData(1, 0x5eed_1001), diff --git a/benchmarks/memory/server/scenarios/peak-large-page/shared.ts b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts index 7ee03caa3d..36a91fa49d 100644 --- a/benchmarks/memory/server/scenarios/peak-large-page/shared.ts +++ b/benchmarks/memory/server/scenarios/peak-large-page/shared.ts @@ -9,7 +9,6 @@ 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 knownDehydratedRecordName = 'peak-large-page-l8-record-199' const requestInit = { method: 'GET', @@ -34,12 +33,6 @@ function validatePeakLargePageBody(body: string) { if (!body.includes(levelEightMarker)) { throw new Error('Expected peak-large-page level-8 marker in response body') } - - if (!body.includes(knownDehydratedRecordName)) { - throw new Error( - 'Expected peak-large-page dehydrated record in response body', - ) - } } async function assertPeakLargePageSanity(handler: StartRequestHandler) { 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 index 6b8465a552..88682c409a 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ loader: () => makeLargePageLevelData(8, 0x5eed_1008), 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 index bdd3f6c71a..973c6789cf 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ loader: () => makeLargePageLevelData(7, 0x5eed_1007), 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 index 6e3faefe63..75913b8a7f 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ loader: () => makeLargePageLevelData(6, 0x5eed_1006), 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 index 2a3ad3fa24..2fb68f1633 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ loader: () => makeLargePageLevelData(5, 0x5eed_1005), 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 index 71ec922804..57b145e93f 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4')({ loader: () => makeLargePageLevelData(4, 0x5eed_1004), 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 index 414a93dff8..74b2a79d51 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3')({ loader: () => makeLargePageLevelData(3, 0x5eed_1003), 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 index df2531641e..29d75fe01e 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2')({ loader: () => makeLargePageLevelData(2, 0x5eed_1002), 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 index 0dbfc839c1..99ddc68820 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1')({ loader: () => makeLargePageLevelData(1, 0x5eed_1001), diff --git a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts b/benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts deleted file mode 100644 index 2b59e96d72..0000000000 --- a/benchmarks/memory/server/scenarios/peak-large-page/vue/src/large-page-data.ts +++ /dev/null @@ -1,111 +0,0 @@ -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/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 index 61b52162f1..c234a4fb4c 100644 --- 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 @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ loader: () => makeLargePageLevelData(8, 0x5eed_1008), 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 index efbcadff76..b0ef71adf0 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ loader: () => makeLargePageLevelData(7, 0x5eed_1007), 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 index ff1550045e..3a54b55ee7 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ loader: () => makeLargePageLevelData(6, 0x5eed_1006), 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 index c6894302ed..ad046ee842 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ loader: () => makeLargePageLevelData(5, 0x5eed_1005), 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 index 99a4c880f9..cf20a05e59 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4')({ loader: () => makeLargePageLevelData(4, 0x5eed_1004), 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 index 8e6ac1e36c..fc9be2668d 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3')({ loader: () => makeLargePageLevelData(3, 0x5eed_1003), 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 index 074b3465eb..9328fae07b 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1/l2')({ loader: () => makeLargePageLevelData(2, 0x5eed_1002), 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 index f11410cdbe..d87511040c 100644 --- 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 @@ -1,5 +1,5 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../large-page-data' +import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' export const Route = createFileRoute('/l1')({ loader: () => makeLargePageLevelData(1, 0x5eed_1001), diff --git a/benchmarks/memory/server/scenarios/request-churn/shared.ts b/benchmarks/memory/server/scenarios/request-churn/shared.ts index 174aa76dca..1739e4cc59 100644 --- a/benchmarks/memory/server/scenarios/request-churn/shared.ts +++ b/benchmarks/memory/server/scenarios/request-churn/shared.ts @@ -12,7 +12,6 @@ type Framework = 'react' | 'solid' | 'vue' const benchmarkSeed = 0xdecafbad const requestChurnIterations = 200 const itemPageMarker = 'data-bench="request-churn-item"' -const dehydrationMarker = '$_TSR' // Module-level so CodSpeed warmups and measurement never replay URLs. const benchmarkRandom = createDeterministicRandom(benchmarkSeed) let requestCounter = 0 @@ -49,12 +48,6 @@ async function assertRequestChurnSanity(handler: StartRequestHandler) { } validateItemBody(body) - - if (!body.includes(dehydrationMarker)) { - throw new Error( - 'Expected sanity response to include the dehydration marker', - ) - } } export function createWorkloadGroup( 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 index a131991bc6..cfba0dbd62 100644 --- 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 @@ -1,36 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' - -const mapEntryCount = 500 -const setEntryCount = 500 -const temporalEntryCount = 500 -const nestedTreeDepth = 5 -const nestedTreeBreadth = 6 -const payloadTextLength = 150 - -interface MapPayloadValue { - index: number - label: string - createdAt: Date - count: bigint - text: string -} - -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 -} +import { makeSerializationPayload } from '../../../serialization-payload' export const Route = createFileRoute('/data/$id')({ loader: ({ params }) => makeSerializationPayload(params.id), @@ -44,87 +13,3 @@ function DataComponent() {
Map size: {data.lookup.size}
) } - -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/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 index cf6c86f1f9..c02990bbf9 100644 --- a/benchmarks/memory/server/scenarios/serialization-payload/shared.ts +++ b/benchmarks/memory/server/scenarios/serialization-payload/shared.ts @@ -11,7 +11,6 @@ type Framework = 'react' | 'solid' | 'vue' const benchmarkSeed = 0x51eaa11 const serializationPayloadIterations = 20 const payloadPageMarker = 'data-bench="serialization-payload"' -const dehydrationMarker = '$_TSR' const requestInit = { method: 'GET', @@ -26,22 +25,6 @@ function buildPayloadRequest(random: () => number, index: number) { return new Request(`http://localhost/data/${id}`, requestInit) } -function knownMapKey(id: string) { - return `map-${id}-000` -} - -function getRequestId(request: Request) { - const url = new URL(request.url) - const match = /^\/data\/([^/]+)$/.exec(url.pathname) - const id = match?.[1] - - if (id === undefined) { - throw new Error(`Expected /data/$id request URL, got ${request.url}`) - } - - return decodeURIComponent(id) -} - function validatePayloadResponse(response: Response, request: Request) { if (response.status !== 200) { throw new Error( @@ -50,24 +33,10 @@ function validatePayloadResponse(response: Response, request: Request) { } } -function validatePayloadBody( - body: string, - _response: Response, - request: Request, -) { +function validatePayloadBody(body: string) { if (!body.includes(payloadPageMarker)) { throw new Error('Expected serialization-payload marker in response body') } - - if (!body.includes(dehydrationMarker)) { - throw new Error('Expected serialization-payload dehydration script in body') - } - - const mapKey = knownMapKey(getRequestId(request)) - - if (!body.includes(mapKey)) { - throw new Error(`Expected dehydrated payload to include Map key ${mapKey}`) - } } async function assertSerializationPayloadSanity(handler: StartRequestHandler) { @@ -79,7 +48,7 @@ async function assertSerializationPayloadSanity(handler: StartRequestHandler) { const body = await response.text() validatePayloadResponse(response, request) - validatePayloadBody(body, response, request) + validatePayloadBody(body) } export function createWorkloadGroup( 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 index a57778d7ee..8cdbdb9046 100644 --- 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 @@ -1,36 +1,5 @@ import { createFileRoute } from '@tanstack/solid-router' - -const mapEntryCount = 500 -const setEntryCount = 500 -const temporalEntryCount = 500 -const nestedTreeDepth = 5 -const nestedTreeBreadth = 6 -const payloadTextLength = 150 - -interface MapPayloadValue { - index: number - label: string - createdAt: Date - count: bigint - text: string -} - -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 -} +import { makeSerializationPayload } from '../../../serialization-payload' export const Route = createFileRoute('/data/$id')({ loader: ({ params }) => makeSerializationPayload(params.id), @@ -46,87 +15,3 @@ function DataComponent() { ) } - -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/vue/src/routes/data.$id.tsx b/benchmarks/memory/server/scenarios/serialization-payload/vue/src/routes/data.$id.tsx index e32e12db61..e6b4cfadfb 100644 --- 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 @@ -1,36 +1,5 @@ import { createFileRoute } from '@tanstack/vue-router' - -const mapEntryCount = 500 -const setEntryCount = 500 -const temporalEntryCount = 500 -const nestedTreeDepth = 5 -const nestedTreeBreadth = 6 -const payloadTextLength = 150 - -interface MapPayloadValue { - index: number - label: string - createdAt: Date - count: bigint - text: string -} - -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 -} +import { makeSerializationPayload } from '../../../serialization-payload' export const Route = createFileRoute('/data/$id')({ loader: ({ params }) => makeSerializationPayload(params.id), @@ -46,87 +15,3 @@ function DataComponent() { ) } - -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/server-fn-churn/react/src/fns.ts b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts index 2ea8643da1..22770278cd 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/react/src/fns.ts @@ -1,10 +1,8 @@ import { createMiddleware, createServerFn } from '@tanstack/react-start' - -type ServerFnInput = { - id: string -} - -const recordIndexes = Array.from({ length: 5 }, (_, index) => index) +import { + makeServerFnChurnPayload, + validateServerFnInput, +} from '../../server-fn-payload' const contextMiddleware = createMiddleware({ type: 'function' }).server( ({ next }) => @@ -15,33 +13,12 @@ const contextMiddleware = createMiddleware({ type: 'function' }).server( }), ) -function validateInput(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 } -} - -function echoPayload(data: ServerFnInput, context: { ctx: string }) { - return { - id: data.id, - ctx: context.ctx, - payload: recordIndexes.map((index) => ({ - id: `${data.id}-${index}`, - label: `record-${index}`, - })), - } -} - export const churnGet = createServerFn({ method: 'GET' }) .middleware([contextMiddleware]) - .validator(validateInput) - .handler(({ data, context }) => echoPayload(data, context)) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) export const churnPost = createServerFn({ method: 'POST' }) .middleware([contextMiddleware]) - .validator(validateInput) - .handler(({ data, context }) => echoPayload(data, context)) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) 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 index 8a424672af..ad1022c836 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/shared.ts @@ -174,10 +174,6 @@ function validateEchoedBody( `Expected context marker ${contextMarker} in ${request.url}`, ) } - - if (!body.includes(`${expectedId}-4`)) { - throw new Error(`Expected final payload record for ${expectedId}`) - } } async function assertServerFnChurnSanity( 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 index 86a79393b9..31669dff50 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/solid/src/fns.ts @@ -1,10 +1,8 @@ import { createMiddleware, createServerFn } from '@tanstack/solid-start' - -type ServerFnInput = { - id: string -} - -const recordIndexes = Array.from({ length: 5 }, (_, index) => index) +import { + makeServerFnChurnPayload, + validateServerFnInput, +} from '../../server-fn-payload' const contextMiddleware = createMiddleware({ type: 'function' }).server( ({ next }) => @@ -15,33 +13,12 @@ const contextMiddleware = createMiddleware({ type: 'function' }).server( }), ) -function validateInput(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 } -} - -function echoPayload(data: ServerFnInput, context: { ctx: string }) { - return { - id: data.id, - ctx: context.ctx, - payload: recordIndexes.map((index) => ({ - id: `${data.id}-${index}`, - label: `record-${index}`, - })), - } -} - export const churnGet = createServerFn({ method: 'GET' }) .middleware([contextMiddleware]) - .validator(validateInput) - .handler(({ data, context }) => echoPayload(data, context)) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) export const churnPost = createServerFn({ method: 'POST' }) .middleware([contextMiddleware]) - .validator(validateInput) - .handler(({ data, context }) => echoPayload(data, context)) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) 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 index c3d8fbe0ed..0fa679f76f 100644 --- a/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts +++ b/benchmarks/memory/server/scenarios/server-fn-churn/vue/src/fns.ts @@ -1,10 +1,8 @@ import { createMiddleware, createServerFn } from '@tanstack/vue-start' - -type ServerFnInput = { - id: string -} - -const recordIndexes = Array.from({ length: 5 }, (_, index) => index) +import { + makeServerFnChurnPayload, + validateServerFnInput, +} from '../../server-fn-payload' const contextMiddleware = createMiddleware({ type: 'function' }).server( ({ next }) => @@ -15,33 +13,12 @@ const contextMiddleware = createMiddleware({ type: 'function' }).server( }), ) -function validateInput(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 } -} - -function echoPayload(data: ServerFnInput, context: { ctx: string }) { - return { - id: data.id, - ctx: context.ctx, - payload: recordIndexes.map((index) => ({ - id: `${data.id}-${index}`, - label: `record-${index}`, - })), - } -} - export const churnGet = createServerFn({ method: 'GET' }) .middleware([contextMiddleware]) - .validator(validateInput) - .handler(({ data, context }) => echoPayload(data, context)) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) export const churnPost = createServerFn({ method: 'POST' }) .middleware([contextMiddleware]) - .validator(validateInput) - .handler(({ data, context }) => echoPayload(data, context)) + .validator(validateServerFnInput) + .handler(({ data, context }) => makeServerFnChurnPayload(data, context)) 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/src/routes/stream.$id.tsx b/benchmarks/memory/server/scenarios/streaming-peak/react/src/routes/stream.$id.tsx index dc93600b47..aa8ba5e865 100644 --- 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 @@ -1,20 +1,12 @@ import { Await, createFileRoute } from '@tanstack/react-router' import { Suspense } from 'react' +import { + makeDeferredSectionPayload, + type DeferredSectionPayload, +} from '../../../deferred-section-data' -const deferredRecordCount = 250 -const recordValueLength = 128 const fallbackFlushDelayMs = 1 -interface DeferredRecord { - id: string - value: string -} - -export interface DeferredSectionPayload { - index: number - records: Array -} - export const Route = createFileRoute('/stream/$id')({ loader: ({ params }) => ({ eager: `streaming-peak-eager-${params.id}`, @@ -41,26 +33,10 @@ function afterFallbackFlush(sectionIndex: number) { }) } -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) -} - function makeDeferredSection(id: string, sectionIndex: number) { - return afterFallbackFlush(sectionIndex).then(() => ({ - index: sectionIndex, - records: Array.from({ length: deferredRecordCount }, (_, recordIndex) => ({ - id: `${id}-${sectionIndex}-${recordIndex}`, - value: makeRecordValue(id, sectionIndex, recordIndex), - })), - })) + return afterFallbackFlush(sectionIndex).then(() => + makeDeferredSectionPayload(id, sectionIndex), + ) } function StreamComponent() { diff --git a/benchmarks/memory/server/scenarios/streaming-peak/shared.ts b/benchmarks/memory/server/scenarios/streaming-peak/shared.ts index cde9a7c51c..a82c43b5b4 100644 --- a/benchmarks/memory/server/scenarios/streaming-peak/shared.ts +++ b/benchmarks/memory/server/scenarios/streaming-peak/shared.ts @@ -10,18 +10,7 @@ type Framework = 'react' | 'solid' | 'vue' const benchmarkSeed = 0xdecafbad const streamingPeakIterations = 20 -const fallbackMarkers = [ - 'streaming-peak-fallback-0', - 'streaming-peak-fallback-1', - 'streaming-peak-fallback-2', - 'streaming-peak-fallback-3', -] as const -const deferredSectionMarkers = [ - 'streaming-peak-deferred-0', - 'streaming-peak-deferred-1', - 'streaming-peak-deferred-2', - 'streaming-peak-deferred-3', -] as const +const fallbackMarker = 'streaming-peak-fallback-0' const requestInit = { method: 'GET', @@ -77,31 +66,6 @@ async function readStreamingBody(response: Response) { return { body, chunkCount } } -function assertFallbacksPrecedeDeferredContent(body: string) { - for (let index = 0; index < fallbackMarkers.length; index++) { - const fallbackIndex = body.indexOf(fallbackMarkers[index]!) - const deferredIndex = body.indexOf(deferredSectionMarkers[index]!) - - if (fallbackIndex === -1) { - throw new Error( - `Expected fallback marker ${fallbackMarkers[index]} in body`, - ) - } - - if (deferredIndex === -1) { - throw new Error( - `Expected deferred section marker ${deferredSectionMarkers[index]} in body`, - ) - } - - if (fallbackIndex > deferredIndex) { - throw new Error( - `Expected ${fallbackMarkers[index]} to precede ${deferredSectionMarkers[index]}`, - ) - } - } -} - async function assertStreamingPeakSanity(handler: StartRequestHandler) { const chunkedRequest = new Request( 'http://localhost/stream/sanity-chunked', @@ -119,7 +83,9 @@ async function assertStreamingPeakSanity(handler: StartRequestHandler) { ) } - assertFallbacksPrecedeDeferredContent(chunked.body) + if (!chunked.body.includes(fallbackMarker)) { + throw new Error('Expected streaming-peak fallback marker in response body') + } } export function createWorkloadGroup( 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 index f77d04a04f..0a3f64ffd4 100644 --- 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 @@ -1,20 +1,12 @@ import { Await, createFileRoute } from '@tanstack/solid-router' import { Suspense } from 'solid-js' +import { + makeDeferredSectionPayload, + type DeferredSectionPayload, +} from '../../../deferred-section-data' -const deferredRecordCount = 250 -const recordValueLength = 128 const fallbackFlushDelayMs = 25 -interface DeferredRecord { - id: string - value: string -} - -export interface DeferredSectionPayload { - index: number - records: Array -} - export const Route = createFileRoute('/stream/$id')({ loader: ({ params }) => ({ eager: `streaming-peak-eager-${params.id}`, @@ -34,26 +26,10 @@ function afterFallbackFlush(sectionIndex: number) { }) } -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) -} - function makeDeferredSection(id: string, sectionIndex: number) { - return afterFallbackFlush(sectionIndex).then(() => ({ - index: sectionIndex, - records: Array.from({ length: deferredRecordCount }, (_, recordIndex) => ({ - id: `${id}-${sectionIndex}-${recordIndex}`, - value: makeRecordValue(id, sectionIndex, recordIndex), - })), - })) + return afterFallbackFlush(sectionIndex).then(() => + makeDeferredSectionPayload(id, sectionIndex), + ) } function StreamComponent() { 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 index 191a8f9d4e..963782403b 100644 --- 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 @@ -1,20 +1,12 @@ import { Await, createFileRoute } from '@tanstack/vue-router' import { Suspense } from 'vue' +import { + makeDeferredSectionPayload, + type DeferredSectionPayload, +} from '../../../deferred-section-data' -const deferredRecordCount = 250 -const recordValueLength = 128 const fallbackFlushDelayMs = 1 -interface DeferredRecord { - id: string - value: string -} - -export interface DeferredSectionPayload { - index: number - records: Array -} - export const Route = createFileRoute('/stream/$id')({ loader: ({ params }) => ({ eager: `streaming-peak-eager-${params.id}`, @@ -34,26 +26,10 @@ function afterFallbackFlush(sectionIndex: number) { }) } -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) -} - function makeDeferredSection(id: string, sectionIndex: number) { - return afterFallbackFlush(sectionIndex).then(() => ({ - index: sectionIndex, - records: Array.from({ length: deferredRecordCount }, (_, recordIndex) => ({ - id: `${id}-${sectionIndex}-${recordIndex}`, - value: makeRecordValue(id, sectionIndex, recordIndex), - })), - })) + return afterFallbackFlush(sectionIndex).then(() => + makeDeferredSectionPayload(id, sectionIndex), + ) } function StreamComponent() { From c922c2a58eb2db01d10bf7a844491a9d7f32fedd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:30:15 +0000 Subject: [PATCH 23/24] ci: apply automated fixes --- benchmarks/memory/client/lifecycle.ts | 4 +--- .../react/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 5 ++++- .../react/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 5 ++++- .../peak-large-page/react/src/routes/l1.l2.l3.l4.l5.l6.tsx | 5 ++++- .../peak-large-page/react/src/routes/l1.l2.l3.l4.l5.tsx | 5 ++++- .../peak-large-page/react/src/routes/l1.l2.l3.l4.tsx | 5 ++++- .../scenarios/peak-large-page/react/src/routes/l1.l2.l3.tsx | 5 ++++- .../scenarios/peak-large-page/react/src/routes/l1.l2.tsx | 5 ++++- .../server/scenarios/peak-large-page/react/src/routes/l1.tsx | 5 ++++- .../solid/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 5 ++++- .../solid/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 5 ++++- .../peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.l6.tsx | 5 ++++- .../peak-large-page/solid/src/routes/l1.l2.l3.l4.l5.tsx | 5 ++++- .../peak-large-page/solid/src/routes/l1.l2.l3.l4.tsx | 5 ++++- .../scenarios/peak-large-page/solid/src/routes/l1.l2.l3.tsx | 5 ++++- .../scenarios/peak-large-page/solid/src/routes/l1.l2.tsx | 5 ++++- .../server/scenarios/peak-large-page/solid/src/routes/l1.tsx | 5 ++++- .../vue/src/routes/l1.l2.l3.l4.l5.l6.l7.l8.tsx | 5 ++++- .../peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.l7.tsx | 5 ++++- .../peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.l6.tsx | 5 ++++- .../peak-large-page/vue/src/routes/l1.l2.l3.l4.l5.tsx | 5 ++++- .../scenarios/peak-large-page/vue/src/routes/l1.l2.l3.l4.tsx | 5 ++++- .../scenarios/peak-large-page/vue/src/routes/l1.l2.l3.tsx | 5 ++++- .../scenarios/peak-large-page/vue/src/routes/l1.l2.tsx | 5 ++++- .../server/scenarios/peak-large-page/vue/src/routes/l1.tsx | 5 ++++- 25 files changed, 97 insertions(+), 27 deletions(-) diff --git a/benchmarks/memory/client/lifecycle.ts b/benchmarks/memory/client/lifecycle.ts index 78850e81b2..c4a62f0f96 100644 --- a/benchmarks/memory/client/lifecycle.ts +++ b/benchmarks/memory/client/lifecycle.ts @@ -30,9 +30,7 @@ export function createBenchContainer() { return container } -export function removeBenchContainer( - container: HTMLDivElement | undefined, -) { +export function removeBenchContainer(container: HTMLDivElement | undefined) { container?.remove() } 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 index 98e6fd2aa4..0651ba886d 100644 --- 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 @@ -1,5 +1,8 @@ import { createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ loader: () => makeLargePageLevelData(8, 0x5eed_1008), 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 index dda157406c..1f89e6f454 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ loader: () => makeLargePageLevelData(7, 0x5eed_1007), 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 index aeaf96cf36..1a8c98ceb2 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ loader: () => makeLargePageLevelData(6, 0x5eed_1006), 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 index 74838de0a3..3e9afad0b9 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ loader: () => makeLargePageLevelData(5, 0x5eed_1005), 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 index 7dcaf8e417..73c29bc402 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4')({ loader: () => makeLargePageLevelData(4, 0x5eed_1004), 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 index 4e59288fd1..e5b57463ad 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3')({ loader: () => makeLargePageLevelData(3, 0x5eed_1003), 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 index d3f39a5f87..18999233cc 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2')({ loader: () => makeLargePageLevelData(2, 0x5eed_1002), 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 index 26f9d00281..8fa7a5e0dc 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/react-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1')({ loader: () => makeLargePageLevelData(1, 0x5eed_1001), 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 index 88682c409a..b0f94893fb 100644 --- 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 @@ -1,5 +1,8 @@ import { createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ loader: () => makeLargePageLevelData(8, 0x5eed_1008), 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 index 973c6789cf..f8a3819c85 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ loader: () => makeLargePageLevelData(7, 0x5eed_1007), 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 index 75913b8a7f..7a438ddbd2 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ loader: () => makeLargePageLevelData(6, 0x5eed_1006), 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 index 2fb68f1633..2e05985ac5 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ loader: () => makeLargePageLevelData(5, 0x5eed_1005), 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 index 57b145e93f..4f38f0cb76 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4')({ loader: () => makeLargePageLevelData(4, 0x5eed_1004), 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 index 74b2a79d51..3f1f113fe3 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3')({ loader: () => makeLargePageLevelData(3, 0x5eed_1003), 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 index 29d75fe01e..bd338fd53f 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2')({ loader: () => makeLargePageLevelData(2, 0x5eed_1002), 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 index 99ddc68820..9dcfd7448a 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/solid-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1')({ loader: () => makeLargePageLevelData(1, 0x5eed_1001), 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 index c234a4fb4c..bd079aa1a0 100644 --- 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 @@ -1,5 +1,8 @@ import { createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7/l8')({ loader: () => makeLargePageLevelData(8, 0x5eed_1008), 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 index b0ef71adf0..7f69f6f438 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6/l7')({ loader: () => makeLargePageLevelData(7, 0x5eed_1007), 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 index 3a54b55ee7..75a7c717e3 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5/l6')({ loader: () => makeLargePageLevelData(6, 0x5eed_1006), 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 index ad046ee842..f390ae3db3 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4/l5')({ loader: () => makeLargePageLevelData(5, 0x5eed_1005), 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 index cf20a05e59..6b30bd50dc 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3/l4')({ loader: () => makeLargePageLevelData(4, 0x5eed_1004), 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 index fc9be2668d..6ec6f32fea 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2/l3')({ loader: () => makeLargePageLevelData(3, 0x5eed_1003), 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 index 9328fae07b..e5b4cf0de1 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1/l2')({ loader: () => makeLargePageLevelData(2, 0x5eed_1002), 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 index d87511040c..a29e465cd4 100644 --- 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 @@ -1,5 +1,8 @@ import { Outlet, createFileRoute } from '@tanstack/vue-router' -import { makeLargePageHead, makeLargePageLevelData } from '../../../large-page-data' +import { + makeLargePageHead, + makeLargePageLevelData, +} from '../../../large-page-data' export const Route = createFileRoute('/l1')({ loader: () => makeLargePageLevelData(1, 0x5eed_1001), From e4fbeb173d26f09e2d103ef02951dfa526058ee6 Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 14 Jun 2026 01:12:52 +0200 Subject: [PATCH 24/24] fix vue benchmark --- .../server/scenarios/aborted-requests/shared.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/benchmarks/memory/server/scenarios/aborted-requests/shared.ts b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts index ee80d62933..9b71c37617 100644 --- a/benchmarks/memory/server/scenarios/aborted-requests/shared.ts +++ b/benchmarks/memory/server/scenarios/aborted-requests/shared.ts @@ -138,7 +138,16 @@ async function readShellBeforeDeferred( } } -async function readSanityStream(response: Response, request: Request) { +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 { @@ -212,10 +221,12 @@ async function assertAbortedRequestsSanity( const { reader, text } = await readSanityStream( midStreamResponse, midStreamRequest, + mode, + midStreamId, ) if (!text.includes(eagerMarker)) { - throw new Error('Expected first sanity chunk to include the eager marker') + throw new Error('Expected sanity stream to include the eager marker') } // reader.cancel() is the response-stream cancellation path if the handler