├── .dockerignore ├── .gitignore ├── .storybook ├── main.ts └── preview.ts ├── Dockerfile ├── Dockerfile.bun ├── Dockerfile.pnpm ├── LICENSE ├── README.md ├── components.json ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.ico ├── react-router.config.ts ├── src ├── components │ ├── ExportModal │ │ ├── ExportModal.stories.tsx │ │ ├── ExportModal.tsx │ │ └── index.ts │ ├── common │ │ ├── FileTree │ │ │ ├── FileTree.stories.tsx │ │ │ ├── FileTree.tsx │ │ │ └── index.ts │ │ ├── Logo │ │ │ ├── Logo.stories.tsx │ │ │ ├── Logo.tsx │ │ │ └── index.ts │ │ ├── MetricCard │ │ │ ├── MetricCard.stories.tsx │ │ │ ├── MetricCard.tsx │ │ │ └── index.ts │ │ ├── Monaco │ │ │ ├── Monaco.stories.tsx │ │ │ ├── Monaco.tsx │ │ │ ├── index.ts │ │ │ └── themes │ │ │ │ ├── vs-dark.json │ │ │ │ └── vs-light.json │ │ └── MonacoTab │ │ │ ├── MonacoTab.stories.tsx │ │ │ ├── MonacoTab.tsx │ │ │ ├── TabContextMenu.tsx │ │ │ └── index.ts │ ├── layout │ │ ├── Header │ │ │ ├── Header.stories.tsx │ │ │ ├── Header.tsx │ │ │ └── index.ts │ │ └── ThemeSwitcher.tsx │ ├── playground │ │ ├── ShareDialog │ │ │ ├── ShareDialog.stories.tsx │ │ │ ├── ShareDialog.tsx │ │ │ └── index.ts │ │ ├── Sidebar │ │ │ ├── Sidebar.stories.tsx │ │ │ ├── Sidebar.tsx │ │ │ └── index.ts │ │ ├── SidebarIcon │ │ │ ├── SidebarIcon.stories.tsx │ │ │ ├── SidebarIcon.tsx │ │ │ └── index.ts │ │ ├── code │ │ │ └── RunPanel │ │ │ │ ├── RunPanel.stories.tsx │ │ │ │ ├── RunPanel.tsx │ │ │ │ ├── index.ts │ │ │ │ └── tabs │ │ │ │ ├── ConsoleTab │ │ │ │ ├── ConsoleTab.stories.tsx │ │ │ │ ├── ConsoleTab.tsx │ │ │ │ └── index.ts │ │ │ │ └── RunTab │ │ │ │ ├── RunTab.stories.tsx │ │ │ │ ├── RunTab.tsx │ │ │ │ └── index.ts │ │ └── compare │ │ │ ├── ComparisonChart │ │ │ ├── ComparisonChart.stories.tsx │ │ │ ├── ComparisonChart.tsx │ │ │ └── index.ts │ │ │ └── ComparisonTable │ │ │ ├── ComparisonTable.stories.tsx │ │ │ ├── ComparisonTable.tsx │ │ │ └── index.ts │ └── ui │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── progress.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ └── tooltip.tsx ├── config.ts ├── constants.ts ├── global.css ├── hooks │ └── useMonacoTabs.ts ├── lib │ ├── formatters.ts │ └── utils.ts ├── root.tsx ├── routes.ts ├── routes │ ├── home.tsx │ └── playground │ │ ├── root.tsx │ │ └── views │ │ ├── code │ │ └── index.tsx │ │ ├── compare │ │ └── index.tsx │ │ └── settings │ │ └── index.tsx ├── services │ ├── benchmark │ │ ├── benchmark-service.ts │ │ ├── performance.d.ts │ │ ├── types.ts │ │ └── worker.ts │ ├── code-processor │ │ ├── babel.test.ts │ │ ├── babel.ts │ │ ├── bundle-benchmark-code.test.ts │ │ ├── bundle-benchmark-code.ts │ │ ├── index.ts │ │ └── prettier.ts │ └── dependencies │ │ ├── DependencyService.ts │ │ ├── ata.ts │ │ ├── cache.ts │ │ ├── cachedFetch.ts │ │ └── index.ts └── stores │ ├── benchmarkStore.ts │ ├── dependenciesStore.ts │ ├── persistentStore.ts │ └── userStore.ts ├── tailwind.config.ts ├── tsconfig.json ├── vite.config.ts ├── vitest.config.ts └── vitest.setup.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | .react-router 2 | build 3 | node_modules 4 | README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | 4 | # React Router 5 | /.react-router/ 6 | /build/ 7 | /public/monaco-editor/ 8 | 9 | *storybook.log 10 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 5 | addons: [ 6 | "@storybook/addon-essentials", 7 | "@storybook/addon-interactions", 8 | // https://github.com/JesusTheHun/storybook-addon-remix-react-router 9 | "storybook-addon-remix-react-router", 10 | ], 11 | framework: { 12 | name: "@storybook/react-vite", 13 | options: {}, 14 | }, 15 | }; 16 | export default config; 17 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | import { withRouter, reactRouterParameters } from "storybook-addon-remix-react-router"; 3 | import "../src/global.css"; 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/i, 11 | }, 12 | }, 13 | reactRouter: reactRouterParameters({}), 14 | }, 15 | decorators: [withRouter], 16 | }; 17 | 18 | export default preview; 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS development-dependencies-env 2 | COPY . /app 3 | WORKDIR /app 4 | RUN npm ci 5 | 6 | FROM node:20-alpine AS production-dependencies-env 7 | COPY ./package.json package-lock.json /app/ 8 | WORKDIR /app 9 | RUN npm ci --omit=dev 10 | 11 | FROM node:20-alpine AS build-env 12 | COPY . /app/ 13 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 14 | WORKDIR /app 15 | RUN npm run build 16 | 17 | FROM node:20-alpine 18 | COPY ./package.json package-lock.json /app/ 19 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 20 | COPY --from=build-env /app/build /app/build 21 | WORKDIR /app 22 | CMD ["npm", "run", "start"] 23 | -------------------------------------------------------------------------------- /Dockerfile.bun: -------------------------------------------------------------------------------- 1 | FROM oven/bun:1 AS dependencies-env 2 | COPY . /app 3 | 4 | FROM dependencies-env AS development-dependencies-env 5 | COPY ./package.json bun.lockb /app/ 6 | WORKDIR /app 7 | RUN bun i --frozen-lockfile 8 | 9 | FROM dependencies-env AS production-dependencies-env 10 | COPY ./package.json bun.lockb /app/ 11 | WORKDIR /app 12 | RUN bun i --production 13 | 14 | FROM dependencies-env AS build-env 15 | COPY ./package.json bun.lockb /app/ 16 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 17 | WORKDIR /app 18 | RUN bun run build 19 | 20 | FROM dependencies-env 21 | COPY ./package.json bun.lockb /app/ 22 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 23 | COPY --from=build-env /app/build /app/build 24 | WORKDIR /app 25 | CMD ["bun", "run", "start"] 26 | -------------------------------------------------------------------------------- /Dockerfile.pnpm: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS dependencies-env 2 | RUN npm i -g pnpm 3 | COPY . /app 4 | 5 | FROM dependencies-env AS development-dependencies-env 6 | COPY ./package.json pnpm-lock.yaml /app/ 7 | WORKDIR /app 8 | RUN pnpm i --frozen-lockfile 9 | 10 | FROM dependencies-env AS production-dependencies-env 11 | COPY ./package.json pnpm-lock.yaml /app/ 12 | WORKDIR /app 13 | RUN pnpm i --prod --frozen-lockfile 14 | 15 | FROM dependencies-env AS build-env 16 | COPY ./package.json pnpm-lock.yaml /app/ 17 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 18 | WORKDIR /app 19 | RUN pnpm build 20 | 21 | FROM dependencies-env 22 | COPY ./package.json pnpm-lock.yaml /app/ 23 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 24 | COPY --from=build-env /app/build /app/build 25 | WORKDIR /app 26 | CMD ["pnpm", "start"] 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BenchJS 2 | 3 | A browser-based JavaScript benchmarking tool: https://benchjs.com 4 | \ 5 | Currently powered by [benchmate](https://github.com/3rd/benchmate). 6 | 7 | ![screenshot](https://root.b-cdn.net/benchjs/Social.png) 8 | 9 | ## Features 10 | 11 | - 🚀 **Zero Setup Required** - Write and run benchmarks directly in your browser 12 | - 📊 **Real-time Metrics** - Watch your benchmarks run with detailed performance statistics 13 | - 🔄 **Easy Comparison** - Compare multiple implementations at once 14 | - 📋 **Modern experience** - TypeScript, Monaco, esbuild, all the goodies 15 | - 📦 **ESM Package Support** - Import packages directly via [esm.sh](https://esm.sh) 16 | - 🔗 **Shareable Results** - Share an URL to your benchmarks or an image of the results 17 | 18 | ## Writing Benchmarks 19 | 20 | ### File Structure 21 | 22 | - `setup.ts`: Setup code and shared utilities 23 | - `implementations/*.ts`: Implementation files containing benchmark code 24 | 25 | ### Setup File 26 | 27 | The setup file (`setup.ts`) is where you prepare data and create shared helpers. Everything you export will be available as a global for each implementation. 28 | 29 | ```typescript 30 | // Generate test data 31 | export const generateData = (size: number) => { 32 | return Array.from({ length: size }, (_, i) => i); 33 | }; 34 | 35 | // Export data and helpers 36 | export const data = generateData(1000); 37 | export const sum = (a: number, b: number) => a + b; 38 | ``` 39 | 40 | ### Implementation Files 41 | 42 | Each implementation file must export a `run` function that contains the code you want to benchmark. 43 | 44 | ```typescript 45 | export const run = () => { 46 | // Your implementation here 47 | return data.reduce(sum, 0); 48 | }; 49 | ``` 50 | 51 | ### Using External Packages 52 | 53 | BenchJS supports importing packages from [esm.sh](https://esm.sh). Configure them on the settings page and then import them: 54 | 55 | ```typescript 56 | import { pick } from "lodash-es"; 57 | 58 | export const run = () => { 59 | return pick(data, ["id", "name"]); 60 | }; 61 | ``` 62 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/global.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/" 4 | status = 200 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benchjs", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "cross-env NODE_ENV=production react-router build", 7 | "dev": "react-router dev", 8 | "start": "cross-env NODE_ENV=production react-router-serve ./build/server/index.js", 9 | "typecheck": "react-router typegen && tsc", 10 | "storybook": "storybook dev -p 6006", 11 | "build-storybook": "storybook build", 12 | "test": "vitest run", 13 | "test:watch": "vitest watch", 14 | "test:ui": "vitest --ui", 15 | "test:coverage": "vitest run --coverage" 16 | }, 17 | "dependencies": { 18 | "@babel/core": "7.26.0", 19 | "@babel/standalone": "^7.26.4", 20 | "@dnd-kit/core": "^6.3.1", 21 | "@dnd-kit/modifiers": "^9.0.0", 22 | "@dnd-kit/sortable": "^10.0.0", 23 | "@dnd-kit/utilities": "^3.2.2", 24 | "@hookform/resolvers": "^3.9.1", 25 | "@icons-pack/react-simple-icons": "^10.2.0", 26 | "@monaco-editor/react": "^4.6.0", 27 | "@radix-ui/react-checkbox": "^1.1.3", 28 | "@radix-ui/react-context-menu": "^2.2.4", 29 | "@radix-ui/react-dialog": "^1.1.4", 30 | "@radix-ui/react-dropdown-menu": "^2.1.4", 31 | "@radix-ui/react-label": "^2.1.1", 32 | "@radix-ui/react-progress": "^1.1.1", 33 | "@radix-ui/react-scroll-area": "^1.2.2", 34 | "@radix-ui/react-select": "^2.1.4", 35 | "@radix-ui/react-slot": "^1.1.1", 36 | "@radix-ui/react-switch": "^1.1.2", 37 | "@radix-ui/react-tabs": "^1.1.2", 38 | "@radix-ui/react-tooltip": "^1.1.6", 39 | "@react-router/node": "^7.1.0", 40 | "@react-router/serve": "^7.1.0", 41 | "@typescript/ata": "^0.9.7", 42 | "benchmate": "^1.12.0", 43 | "class-variance-authority": "^0.7.1", 44 | "clsx": "^2.1.1", 45 | "date-fns": "^4.1.0", 46 | "es-toolkit": "^1.30.1", 47 | "esbuild-wasm": "^0.24.2", 48 | "framer-motion": "^11.15.0", 49 | "html-to-image": "^1.11.11", 50 | "idb": "^8.0.1", 51 | "isbot": "^5.1.18", 52 | "lucide-react": "^0.469.0", 53 | "lz-string": "^1.5.0", 54 | "monaco-editor": "^0.52.2", 55 | "nanoid": "^5.0.9", 56 | "prettier": "^3.4.2", 57 | "react": "^19.0.0", 58 | "react-dom": "^19.0.0", 59 | "react-hook-form": "^7.54.2", 60 | "react-intersection-observer": "^9.14.0", 61 | "react-resizable-panels": "^2.1.7", 62 | "react-router": "^7.1.0", 63 | "recharts": "^2.15.0", 64 | "serialize-error": "^11.0.3", 65 | "tailwind-merge": "^2.5.5", 66 | "tailwindcss-animate": "^1.0.7", 67 | "typescript": "^5.7.2", 68 | "zod": "^3.24.1", 69 | "zustand": "^5.0.2" 70 | }, 71 | "devDependencies": { 72 | "@react-router/dev": "^7.1.0", 73 | "@storybook/addon-essentials": "^8.4.7", 74 | "@storybook/addon-interactions": "^8.4.7", 75 | "@storybook/blocks": "^8.4.7", 76 | "@storybook/react": "^8.4.7", 77 | "@storybook/react-vite": "^8.4.7", 78 | "@storybook/test": "^8.4.7", 79 | "@types/babel__core": "^7.20.5", 80 | "@types/babel__standalone": "^7.1.9", 81 | "@types/babel__traverse": "^7.20.6", 82 | "@types/fs-extra": "^11.0.4", 83 | "@types/node": "^22.10.2", 84 | "@types/react": "^19.0.2", 85 | "@types/react-dom": "^19.0.2", 86 | "@vitest/coverage-v8": "^2.1.8", 87 | "@vitest/ui": "^2.1.8", 88 | "autoprefixer": "^10.4.20", 89 | "cross-env": "^7.0.3", 90 | "fs-extra": "^11.2.0", 91 | "postcss": "^8.4.49", 92 | "storybook": "^8.4.7", 93 | "storybook-addon-remix-react-router": "3.0.3--canary.85.7d8672d.0", 94 | "tailwindcss": "^3.4.17", 95 | "vite": "^6.0.5", 96 | "vite-tsconfig-paths": "^5.1.4", 97 | "vitest": "^2.1.8" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/3rd/benchjs/d716adc01d7038a04410320f91a12acd537d0dd6/public/favicon.ico -------------------------------------------------------------------------------- /react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | appDirectory: "src", 5 | ssr: false, 6 | } satisfies Config; 7 | -------------------------------------------------------------------------------- /src/components/ExportModal/ExportModal.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ExportModal } from "./ExportModal"; 3 | 4 | const meta = { 5 | title: "Components/ExportModal", 6 | component: ExportModal, 7 | parameters: { 8 | layout: "centered", 9 | }, 10 | } satisfies Meta; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | const sampleJson = JSON.stringify( 16 | { 17 | results: [ 18 | { 19 | name: "Implementation A", 20 | totalTime: 1234, 21 | opsPerSec: 98_765, 22 | }, 23 | { 24 | name: "Implementation B", 25 | totalTime: 2345, 26 | opsPerSec: 87_654, 27 | }, 28 | ], 29 | }, 30 | null, 31 | 2, 32 | ); 33 | 34 | export const Default: Story = { 35 | args: { 36 | open: true, 37 | value: sampleJson, 38 | onOpenChange: () => {}, 39 | }, 40 | }; 41 | 42 | export const Closed: Story = { 43 | args: { 44 | open: false, 45 | value: sampleJson, 46 | onOpenChange: () => {}, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/ExportModal/ExportModal.tsx: -------------------------------------------------------------------------------- 1 | import { Monaco } from "@/components/common/Monaco"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; 4 | 5 | interface ExportModalProps { 6 | open: boolean; 7 | onOpenChange: (open: boolean) => void; 8 | value: string; 9 | } 10 | 11 | export function ExportModal({ open, onOpenChange, value }: ExportModalProps) { 12 | return ( 13 | 14 | 15 | 16 | Export Results 17 | 18 |
19 | 28 |
29 | 30 | 31 | 32 |
33 |
34 | ); 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/components/ExportModal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ExportModal"; -------------------------------------------------------------------------------- /src/components/common/FileTree/FileTree.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { FileTree } from "./FileTree"; 3 | 4 | const meta: Meta = { 5 | title: "Common/FileTree", 6 | component: FileTree, 7 | }; 8 | export default meta; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | args: { 14 | item: { 15 | id: "root", 16 | name: "root", 17 | type: "root", 18 | children: [ 19 | { 20 | id: "dist", 21 | name: "dist", 22 | type: "folder", 23 | children: [], 24 | }, 25 | { 26 | id: "node_modules", 27 | name: "node_modules", 28 | type: "folder", 29 | children: [], 30 | count: 42, 31 | }, 32 | { 33 | id: "src", 34 | name: "src", 35 | type: "folder", 36 | children: [ 37 | { 38 | id: "main.ts", 39 | name: "main.ts", 40 | type: "file", 41 | }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | }, 47 | }; 48 | 49 | export const WithActiveFile: Story = { 50 | args: { 51 | ...Default.args, 52 | activeFileId: "main.ts", 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/common/FileTree/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./FileTree"; 2 | -------------------------------------------------------------------------------- /src/components/common/Logo/Logo.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Logo } from "./Logo"; 3 | 4 | const meta: Meta = { 5 | title: "Common/Logo", 6 | component: Logo, 7 | }; 8 | export default meta; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | args: {}, 14 | }; 15 | 16 | export const Large: Story = { 17 | args: { 18 | size: "large", 19 | }, 20 | }; 21 | 22 | export const Huge: Story = { 23 | args: { 24 | size: "huge", 25 | }, 26 | }; 27 | 28 | export const NoIcon: Story = { 29 | args: { 30 | noIcon: true, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/common/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { ZapIcon } from "lucide-react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | interface LogoProps { 5 | size?: "default" | "huge" | "large"; 6 | noIcon?: boolean; 7 | } 8 | 9 | export const Logo = ({ noIcon, size = "default" }: LogoProps) => { 10 | return ( 11 |
12 | {!noIcon && ( 13 | 20 | )} 21 | 28 | Bench 29 | JS 30 | 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/common/Logo/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Logo"; 2 | -------------------------------------------------------------------------------- /src/components/common/MetricCard/MetricCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ClockIcon } from "lucide-react"; 3 | import { MetricCard } from "./MetricCard"; 4 | 5 | const meta: Meta = { 6 | title: "Common/MetricCard", 7 | component: MetricCard, 8 | }; 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: { 15 | title: "Elapsed Time", 16 | value: "0ms", 17 | icon: ClockIcon, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/common/MetricCard/MetricCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent } from "@/components/ui/card"; 2 | 3 | interface MetricCardProps { 4 | title: string; 5 | value: string; 6 | icon?: React.ElementType; 7 | } 8 | 9 | export const MetricCard = ({ title, value, icon: Icon }: MetricCardProps) => { 10 | return ( 11 | 12 | 13 | {Icon && ( 14 |
15 | 16 |
17 | )} 18 |
19 |

{title}

20 |

{value}

21 |
22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/common/MetricCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MetricCard"; 2 | -------------------------------------------------------------------------------- /src/components/common/Monaco/Monaco.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Monaco } from "./Monaco"; 3 | 4 | const meta: Meta = { 5 | title: "Common/Monaco", 6 | component: Monaco, 7 | render: (args) => ( 8 |
9 | 10 |
11 | ), 12 | }; 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = { 18 | args: {}, 19 | }; 20 | 21 | export const WithTabs: Story = { 22 | args: { 23 | tabs: [ 24 | { 25 | id: "main.ts", 26 | name: "main.ts", 27 | active: true, 28 | }, 29 | { 30 | id: "setup.ts", 31 | name: "setup.ts", 32 | active: false, 33 | }, 34 | ], 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/common/Monaco/Monaco.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from "react"; 2 | import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; 3 | import { restrictToHorizontalAxis } from "@dnd-kit/modifiers"; 4 | import { horizontalListSortingStrategy, SortableContext } from "@dnd-kit/sortable"; 5 | import Editor, { loader, Monaco as MonacoEditor, useMonaco } from "@monaco-editor/react"; 6 | import type { editor } from "monaco-editor"; 7 | import { cn } from "@/lib/utils"; 8 | import { MonacoTab } from "@/components/common/MonacoTab"; 9 | 10 | import vsDark from "./themes/vs-dark.json"; 11 | import vsLight from "./themes/vs-light.json"; 12 | 13 | export const themes = { 14 | light: vsLight, 15 | dark: vsDark, 16 | }; 17 | 18 | const transformToGlobalDeclarations = (dts: string) => { 19 | return `declare global { 20 | ${dts 21 | .replace(/export declare/g, "declare") 22 | .replace(/export interface/g, "interface") 23 | .replace(/export type/g, "type") 24 | .split("\n") 25 | .map((line) => ` ${line}`) 26 | .join("\n")} 27 | } 28 | 29 | export {};`; 30 | }; 31 | 32 | export interface MonacoProps { 33 | height?: string; 34 | defaultValue?: string; 35 | value?: string; 36 | language?: string; 37 | options?: editor.IStandaloneEditorConstructionOptions; 38 | className?: string; 39 | tabs?: MonacoTab[]; 40 | extraLibs?: { content: string; filename: string }[]; 41 | theme?: keyof typeof themes; 42 | onChange?: (value: string | undefined) => void; 43 | onDTSChange?: (value: string) => void; 44 | onChangeTab?: (tab: MonacoTab) => void; 45 | onCloseTab?: (tab: MonacoTab) => void; 46 | onCloseOtherTabs?: (tab: MonacoTab) => void; 47 | onCloseTabsToLeft?: (tab: MonacoTab) => void; 48 | onCloseTabsToRight?: (tab: MonacoTab) => void; 49 | onSetTabs?: (tabs: MonacoTab[]) => void; 50 | onMount?: (editor: editor.IStandaloneCodeEditor, monaco: MonacoEditor) => void; 51 | } 52 | 53 | export const Monaco = ({ 54 | className, 55 | tabs, 56 | extraLibs, 57 | theme = "light", 58 | onChangeTab, 59 | onCloseTab, 60 | onCloseOtherTabs, 61 | onCloseTabsToLeft, 62 | onCloseTabsToRight, 63 | onSetTabs, 64 | onDTSChange, 65 | onMount, 66 | ...props 67 | }: MonacoProps) => { 68 | const monacoHelper = useMonaco(); 69 | const activeFile = tabs?.find((f) => f.active); 70 | 71 | const onDTSChangeRef = useRef<((value: string) => void) | null>(null); 72 | onDTSChangeRef.current = onDTSChange ?? null; 73 | 74 | const sensors = useSensors( 75 | useSensor(PointerSensor, { 76 | activationConstraint: { 77 | distance: 8, 78 | }, 79 | }), 80 | ); 81 | 82 | const handleDragEnd = (event: DragEndEvent) => { 83 | if (!tabs) return; 84 | const { active, over } = event; 85 | if (!over || active.id === over.id) return; 86 | 87 | const oldIndex = tabs.findIndex((item) => item.name === active.id); 88 | const newIndex = tabs.findIndex((item) => item.name === over.id); 89 | 90 | if (oldIndex !== -1 && newIndex !== -1) { 91 | const newOrder = [...tabs]; 92 | const [moved] = newOrder.splice(oldIndex, 1); 93 | newOrder.splice(newIndex, 0, moved); 94 | onSetTabs?.(newOrder); 95 | } 96 | }; 97 | 98 | const handleBeforeMount = (monaco: MonacoEditor) => { 99 | loader.config({ 100 | paths: { 101 | vs: `${location.origin}/monaco-editor/min/vs`, 102 | }, 103 | }); 104 | 105 | monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ 106 | moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, 107 | target: monaco.languages.typescript.ScriptTarget.ESNext, 108 | allowNonTsExtensions: true, 109 | declaration: true, 110 | emitDeclarationOnly: true, 111 | esModuleInterop: true, 112 | noEmit: false, 113 | noEmitOnError: false, 114 | noEmitHelpers: false, 115 | skipLibCheck: true, 116 | }); 117 | 118 | monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ 119 | noSemanticValidation: false, 120 | noSyntaxValidation: false, 121 | diagnosticCodesToIgnore: [], 122 | }); 123 | 124 | // init libs 125 | monaco.languages.typescript.typescriptDefaults.setExtraLibs([]); 126 | for (const lib of extraLibs ?? []) { 127 | monaco.languages.typescript.typescriptDefaults.addExtraLib(lib.content, lib.filename); 128 | } 129 | }; 130 | 131 | const handleMount = useCallback((editor: editor.IStandaloneCodeEditor, monaco: MonacoEditor) => { 132 | editor.onDidChangeModelContent(async () => { 133 | const value = editor.getValue(); 134 | props.onChange?.(value); 135 | 136 | if (onDTSChangeRef.current) { 137 | const model = editor.getModel(); 138 | if (!model) return; 139 | const tsWorker = await monaco.languages.typescript.getTypeScriptWorker(); 140 | const worker = await tsWorker(model.uri); 141 | const outputs = await worker.getEmitOutput(model.uri.toString(), true, true); 142 | const dts = outputs.outputFiles.find((file) => file.name.endsWith(".d.ts"))?.text; 143 | if (!dts) return; 144 | 145 | const transformedDTS = dts ? transformToGlobalDeclarations(dts) : ""; 146 | onDTSChangeRef.current?.(transformedDTS); 147 | } 148 | }); 149 | 150 | editor.updateOptions({ 151 | automaticLayout: true, 152 | fixedOverflowWidgets: true, 153 | glyphMargin: false, 154 | folding: false, 155 | padding: { 156 | top: 8, 157 | bottom: 8, 158 | }, 159 | lineNumbers: "on", 160 | minimap: { 161 | enabled: false, 162 | }, 163 | insertSpaces: true, 164 | tabSize: 2, 165 | scrollBeyondLastLine: false, 166 | renderLineHighlightOnlyWhenFocus: true, 167 | overviewRulerBorder: false, 168 | ...props.options, 169 | }); 170 | 171 | onMount?.(editor, monaco); 172 | 173 | // eslint-disable-next-line react-hooks/exhaustive-deps 174 | }, []); 175 | 176 | // sync theme 177 | useEffect(() => { 178 | if (!monacoHelper) return; 179 | const themeConfig = themes[(theme as keyof typeof themes) ?? "vsLight"] as Parameters< 180 | typeof monacoHelper.editor.defineTheme 181 | >[1]; 182 | monacoHelper.editor.defineTheme("theme", themeConfig); 183 | monacoHelper.editor.setTheme("theme"); 184 | }, [monacoHelper, theme]); 185 | 186 | return ( 187 |
188 | {/* tabs */} 189 | {tabs && tabs.length > 0 && ( 190 | 191 |
192 | f.name)} strategy={horizontalListSortingStrategy}> 193 |
194 | {tabs.map((file) => ( 195 | 205 | ))} 206 |
207 |
208 |
209 |
210 | )} 211 | 212 | {/* editor */} 213 |
214 | 223 |
224 |
225 | ); 226 | }; 227 | -------------------------------------------------------------------------------- /src/components/common/Monaco/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Monaco"; 2 | -------------------------------------------------------------------------------- /src/components/common/Monaco/themes/vs-dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VS Dark", 3 | "base": "vs-dark", 4 | "inherit": true, 5 | "rules": [], 6 | "colors": {} 7 | } 8 | -------------------------------------------------------------------------------- /src/components/common/Monaco/themes/vs-light.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VS Light", 3 | "base": "vs", 4 | "inherit": true, 5 | "rules": [], 6 | "colors": {} 7 | } 8 | -------------------------------------------------------------------------------- /src/components/common/MonacoTab/MonacoTab.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { MonacoTab } from "./MonacoTab"; 3 | 4 | const meta: Meta = { 5 | title: "Common/MonacoTab", 6 | component: MonacoTab, 7 | }; 8 | export default meta; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | args: { 14 | tab: { 15 | id: "main.ts", 16 | name: "main.ts", 17 | active: false, 18 | }, 19 | tabs: [ 20 | { id: "main.ts", name: "main.ts", active: false }, 21 | { id: "setup.ts", name: "setup.ts", active: false }, 22 | ], 23 | }, 24 | }; 25 | 26 | export const Active: Story = { 27 | args: { 28 | tab: { 29 | id: "main.ts", 30 | name: "main.ts", 31 | active: true, 32 | }, 33 | tabs: [ 34 | { id: "main.ts", name: "main.ts", active: false }, 35 | { id: "setup.ts", name: "setup.ts", active: false }, 36 | ], 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/common/MonacoTab/MonacoTab.tsx: -------------------------------------------------------------------------------- 1 | import { useSortable } from "@dnd-kit/sortable"; 2 | import { CSS } from "@dnd-kit/utilities"; 3 | import { XIcon } from "lucide-react"; 4 | import { cn } from "@/lib/utils"; 5 | import { Button } from "@/components/ui/button"; 6 | import { TabContextMenu } from "./TabContextMenu"; 7 | 8 | export type MonacoTab = { 9 | id: string; 10 | name: string; 11 | active: boolean; 12 | }; 13 | 14 | interface TabProps { 15 | tab: MonacoTab; 16 | tabs: MonacoTab[]; 17 | onClose?: (file: MonacoTab) => void; 18 | onCloseOthers?: (file: MonacoTab) => void; 19 | onCloseLeft?: (file: MonacoTab) => void; 20 | onCloseRight?: (file: MonacoTab) => void; 21 | onClick?: (file: MonacoTab) => void; 22 | } 23 | 24 | export const MonacoTab = ({ 25 | tab, 26 | tabs, 27 | onClose, 28 | onCloseOthers, 29 | onCloseLeft, 30 | onCloseRight, 31 | onClick, 32 | }: TabProps) => { 33 | const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ 34 | id: tab.name, 35 | transition: { 36 | duration: 150, 37 | easing: "cubic-bezier(0.25, 1, 0.5, 1)", 38 | }, 39 | }); 40 | 41 | const style = { 42 | transform: CSS.Translate.toString(transform), 43 | transition, 44 | opacity: isDragging ? 0.5 : undefined, 45 | zIndex: isDragging ? 1 : undefined, 46 | position: isDragging ? ("relative" as const) : undefined, 47 | }; 48 | 49 | const handleClick = () => { 50 | onClick?.(tab); 51 | }; 52 | 53 | const tabContent = ( 54 |
55 | {tab.name} 56 | 67 |
68 | ); 69 | 70 | return ( 71 | {})} 75 | onCloseLeft={onCloseLeft ?? (() => {})} 76 | onCloseOthers={onCloseOthers ?? (() => {})} 77 | onCloseRight={onCloseRight ?? (() => {})} 78 | > 79 |
94 | {tabContent} 95 |
96 |
97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /src/components/common/MonacoTab/TabContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import { MonacoTab } from "@/components/common/MonacoTab"; 2 | import { 3 | ContextMenu, 4 | ContextMenuContent, 5 | ContextMenuItem, 6 | ContextMenuTrigger, 7 | } from "@/components/ui/context-menu"; 8 | 9 | interface TabContextMenuProps { 10 | tab: MonacoTab; 11 | tabs: MonacoTab[]; 12 | onClose: (tab: MonacoTab) => void; 13 | onCloseOthers: (tab: MonacoTab) => void; 14 | onCloseLeft: (tab: MonacoTab) => void; 15 | onCloseRight: (tab: MonacoTab) => void; 16 | children: React.ReactNode; 17 | } 18 | 19 | export const TabContextMenu = ({ 20 | tab, 21 | tabs, 22 | onClose, 23 | onCloseOthers, 24 | onCloseLeft, 25 | onCloseRight, 26 | children, 27 | }: TabContextMenuProps) => { 28 | const tabIndex = tabs.findIndex((t) => t.id === tab.id); 29 | const hasTabsToTheLeft = tabIndex > 0; 30 | const hasTabsToTheRight = tabIndex < tabs.length - 1; 31 | const hasOtherTabs = tabs.length > 1; 32 | 33 | return ( 34 | 35 | {children} 36 | e.stopPropagation()}> 37 | onClose(tab)}>Close 38 | {hasOtherTabs && onCloseOthers(tab)}>Close Others} 39 | {hasTabsToTheLeft && ( 40 | onCloseLeft(tab)}>Close Tabs to the Left 41 | )} 42 | {hasTabsToTheRight && ( 43 | onCloseRight(tab)}>Close Tabs to the Right 44 | )} 45 | 46 | 47 | ); 48 | }; 49 | 50 | -------------------------------------------------------------------------------- /src/components/common/MonacoTab/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MonacoTab"; 2 | -------------------------------------------------------------------------------- /src/components/layout/Header/Header.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Header } from "./Header"; 4 | 5 | const meta: Meta = { 6 | title: "Layout/Header", 7 | component: Header, 8 | }; 9 | export default meta; 10 | 11 | type Story = StoryObj; 12 | 13 | export const Default: Story = { 14 | args: {}, 15 | }; 16 | 17 | export const CustomNav: Story = { 18 | args: { 19 | customNav: ( 20 | 23 | ), 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/layout/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Link } from "react-router"; 3 | import { cn } from "@/lib/utils"; 4 | import { Logo } from "@/components/common/Logo"; 5 | import { ThemeSwitcher } from "@/components/layout/ThemeSwitcher"; 6 | import { Button } from "@/components/ui/button"; 7 | 8 | export interface HeaderProps { 9 | className?: string; 10 | customNav?: React.ReactNode; 11 | postLogoElement?: React.ReactNode; 12 | } 13 | 14 | export const Header = ({ postLogoElement, customNav, className }: HeaderProps) => { 15 | const [isScrolled, setIsScrolled] = useState(false); 16 | 17 | useEffect(() => { 18 | const handleScroll = () => { 19 | setIsScrolled(window.scrollY > 0); 20 | }; 21 | 22 | window.addEventListener("scroll", handleScroll); 23 | return () => window.removeEventListener("scroll", handleScroll); 24 | }, []); 25 | 26 | return ( 27 |
34 |
35 | {/* logo */} 36 |
37 | 38 | 39 | 40 | {postLogoElement} 41 |
42 | 43 | {/* navigation */} 44 | 69 |
70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/layout/Header/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Header"; 2 | -------------------------------------------------------------------------------- /src/components/layout/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { MoonIcon, SunIcon } from "lucide-react"; 3 | import { useUserStore } from "@/stores/userStore"; 4 | import { Button } from "@/components/ui/button"; 5 | 6 | export function ThemeSwitcher() { 7 | const { theme, setTheme } = useUserStore(); 8 | 9 | useEffect(() => { 10 | const root = window.document.documentElement; 11 | root.classList.remove("light", "dark"); 12 | root.classList.add(theme); 13 | }, [theme]); 14 | 15 | return ( 16 | 21 | ); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/components/playground/ShareDialog/ShareDialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import type { BenchmarkStatus } from "@/stores/benchmarkStore"; 3 | import { ShareDialog } from "./ShareDialog"; 4 | 5 | const mockImplementations = [ 6 | { id: "1", filename: "quicksort.ts", content: "// implementation" }, 7 | { id: "2", filename: "mergesort.ts", content: "// implementation" }, 8 | { id: "3", filename: "bubblesort.ts", content: "// implementation" }, 9 | ]; 10 | 11 | const createBenchmarkRun = (id: string, implId: string, filename: string, ops: number) => ({ 12 | id, 13 | implementationId: implId, 14 | filename, 15 | originalCode: "// implementation", 16 | processedCode: "// processed implementation", 17 | status: "completed" as BenchmarkStatus, 18 | progress: 100, 19 | createdAt: Date.now(), 20 | warmupStartedAt: Date.now(), 21 | warmupEndedAt: Date.now(), 22 | runStartedAt: Date.now(), 23 | runEndedAt: Date.now(), 24 | elapsedTime: 1000, 25 | error: null, 26 | completedIterations: 1000, 27 | totalIterations: 1000, 28 | result: { 29 | name: filename, 30 | stats: { 31 | samples: 100, 32 | batches: 10, 33 | time: { 34 | total: 1000, 35 | min: 900, 36 | max: 1100, 37 | average: 1000, 38 | percentile50: 1000, 39 | percentile90: 1050, 40 | percentile95: 1075, 41 | }, 42 | opsPerSecond: { 43 | average: ops, 44 | max: ops * 1.1, 45 | min: ops * 0.9, 46 | margin: 0.01, 47 | }, 48 | }, 49 | }, 50 | }); 51 | 52 | const mockRuns = { 53 | "1": [createBenchmarkRun("run1", "1", "quicksort.ts", 15_000)], 54 | "2": [createBenchmarkRun("run2", "2", "mergesort.ts", 12_000)], 55 | }; 56 | 57 | const meta = { 58 | title: "Playground/ShareDialog", 59 | component: ShareDialog, 60 | parameters: { 61 | layout: "centered", 62 | }, 63 | args: { 64 | open: true, 65 | onOpenChange: () => {}, 66 | shareUrl: "http://localhost:3000/#code=example", 67 | }, 68 | } satisfies Meta; 69 | 70 | export default meta; 71 | type Story = StoryObj; 72 | 73 | export const Default: Story = { 74 | args: { 75 | implementations: mockImplementations, 76 | runs: mockRuns, 77 | }, 78 | }; 79 | 80 | export const NoRuns: Story = { 81 | args: { 82 | implementations: mockImplementations, 83 | runs: {}, 84 | }, 85 | }; 86 | 87 | export const SingleRun: Story = { 88 | args: { 89 | implementations: mockImplementations, 90 | runs: { 91 | "1": [createBenchmarkRun("run1", "1", "quicksort.ts", 15_000)], 92 | }, 93 | }, 94 | }; 95 | -------------------------------------------------------------------------------- /src/components/playground/ShareDialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ShareDialog"; 2 | -------------------------------------------------------------------------------- /src/components/playground/Sidebar/Sidebar.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Sidebar } from "./Sidebar"; 3 | 4 | const meta: Meta = { 5 | title: "Playground/Sidebar", 6 | component: Sidebar, 7 | render: (args) => ( 8 |
9 | 10 |
11 | ), 12 | }; 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = { 18 | args: {}, 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/playground/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChartPieIcon, 3 | FolderTreeIcon, 4 | SettingsIcon, 5 | // PackageIcon, 6 | } from "lucide-react"; 7 | import { SidebarIcon } from "@/components/playground/SidebarIcon"; 8 | 9 | export type SidebarTab = "code" | "compare" | "environment" | "settings"; 10 | 11 | export interface SidebarProps { 12 | children?: React.ReactNode; 13 | activeTab: SidebarTab; 14 | onTabChange: (tab: SidebarTab) => void; 15 | } 16 | 17 | export const Sidebar = ({ children, activeTab, onTabChange }: SidebarProps) => { 18 | return ( 19 |
20 |
21 | onTabChange("code")} 26 | /> 27 | onTabChange("compare")} 32 | /> 33 | {/* onTabChange("environment")} */} 38 | {/* /> */} 39 | onTabChange("settings")} 44 | /> 45 |
46 | {children} 47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/playground/Sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Sidebar"; 2 | -------------------------------------------------------------------------------- /src/components/playground/SidebarIcon/SidebarIcon.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Search } from "lucide-react"; 3 | import { SidebarIcon } from "./SidebarIcon"; 4 | 5 | const meta: Meta = { 6 | title: "Playground/SidebarIcon", 7 | component: SidebarIcon, 8 | render: (args) => ( 9 |
10 | 11 |
12 | ), 13 | }; 14 | export default meta; 15 | 16 | type Story = StoryObj; 17 | 18 | export const Default: Story = { 19 | args: { 20 | icon: Search, 21 | isActive: false, 22 | tooltip: "Search", 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/playground/SidebarIcon/SidebarIcon.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | export const SidebarIcon = ({ 5 | icon: Icon, 6 | isActive, 7 | tooltip, 8 | count, 9 | onClick, 10 | }: { 11 | icon: React.ElementType; 12 | isActive: boolean; 13 | tooltip: string; 14 | count?: number; 15 | onClick?: () => void; 16 | }) => { 17 | const [isHovered, setIsHovered] = useState(false); 18 | 19 | return ( 20 |
21 |
setIsHovered(true)} 28 | onMouseLeave={() => setIsHovered(false)} 29 | > 30 | {/* icon */} 31 | 32 | 33 | {/* badge */} 34 | {count && ( 35 | 36 | {count} 37 | 38 | )} 39 |
40 | {isHovered && ( 41 |
42 | {tooltip} 43 |
44 | )} 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/components/playground/SidebarIcon/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SidebarIcon"; 2 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/RunPanel.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { RunPanel } from "./RunPanel"; 3 | 4 | const meta: Meta = { 5 | title: "Playground/Code/RunPanel", 6 | component: RunPanel, 7 | }; 8 | export default meta; 9 | 10 | type Story = StoryObj; 11 | 12 | export const Default: Story = { 13 | args: { 14 | implementation: { 15 | id: "main.ts", 16 | filename: "main.ts", 17 | content: "// Write your implementation here\n", 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/RunPanel.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from "react"; 2 | import { 3 | ChevronsDownIcon, 4 | ChevronsLeftIcon, 5 | ChevronsRightIcon, 6 | ChevronsUpIcon, 7 | Columns2Icon, 8 | FlameIcon, 9 | Loader2Icon, 10 | Rows2Icon, 11 | SquareChevronRightIcon, 12 | } from "lucide-react"; 13 | import { useShallow } from "zustand/shallow"; 14 | import { useLatestRunForImplementation } from "@/stores/benchmarkStore"; 15 | import { useBenchmarkStore } from "@/stores/benchmarkStore"; 16 | import { Implementation } from "@/stores/persistentStore"; 17 | import { cn } from "@/lib/utils"; 18 | import { RunTab } from "@/components/playground/code/RunPanel/tabs/RunTab"; 19 | import { Button } from "@/components/ui/button"; 20 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 21 | import { ConsoleTab } from "./tabs/ConsoleTab"; 22 | 23 | type RunPanelTab = "console" | "run"; 24 | 25 | interface RunPanelHeaderProps { 26 | activeTab: RunPanelTab; 27 | children?: ReactNode; 28 | isRunning?: boolean; 29 | layout?: "horizontal" | "vertical"; 30 | collapsed?: boolean; 31 | onTabChange: (tab: string) => void; 32 | onLayoutChange?: () => void; 33 | onToggleCollapse?: () => void; 34 | } 35 | 36 | export const RunPanelTabs = ({ 37 | activeTab, 38 | children, 39 | isRunning, 40 | layout, 41 | collapsed, 42 | onTabChange, 43 | onLayoutChange, 44 | onToggleCollapse, 45 | }: RunPanelHeaderProps) => { 46 | const isVerticalCollapsed = collapsed && layout === "horizontal"; 47 | 48 | return ( 49 | 54 | 60 | {!isVerticalCollapsed && ( 61 | <> 62 | 66 | 67 | Run 68 | 69 | 73 | 74 | Console 75 | 76 | 77 | )} 78 | 79 |
85 | {!isVerticalCollapsed && isRunning && ( 86 | <> 87 | 88 | Running... 89 | 90 | )} 91 | 92 | {onLayoutChange && ( 93 | 103 | )} 104 | 115 |
116 |
117 | {children} 118 |
119 | ); 120 | }; 121 | 122 | interface RunPanelProps { 123 | implementation: Implementation; 124 | onRun?: () => void; 125 | onStop?: () => void; 126 | layout?: "horizontal" | "vertical"; 127 | onLayoutChange?: () => void; 128 | onToggleCollapse?: () => void; 129 | activeTab?: RunPanelTab; 130 | onTabChange?: (tab: RunPanelTab) => void; 131 | } 132 | 133 | export const RunPanel = ({ 134 | implementation, 135 | onRun, 136 | onStop, 137 | layout, 138 | onLayoutChange, 139 | onToggleCollapse, 140 | activeTab: externalActiveTab, 141 | onTabChange: externalOnTabChange, 142 | }: RunPanelProps) => { 143 | const latestRun = useLatestRunForImplementation(implementation.id); 144 | const chartData = useBenchmarkStore( 145 | useShallow((state) => (latestRun ? state.chartData[latestRun.id] || [] : [])), 146 | ); 147 | const { clearChartData } = useBenchmarkStore( 148 | useShallow((state) => ({ 149 | addChartPoint: state.addChartPoint, 150 | clearChartData: state.clearChartData, 151 | })), 152 | ); 153 | const consoleLogs = useBenchmarkStore((state) => (latestRun ? state.consoleLogs[latestRun.id] : null)); 154 | 155 | const [internalActiveTab, setInternalActiveTab] = useState("run"); 156 | const [isCollapsed, setIsCollapsed] = useState(false); 157 | const isRunning = latestRun?.status === "running" || latestRun?.status === "warmup"; 158 | 159 | const activeTab = externalActiveTab ?? internalActiveTab; 160 | const handleSetTab = (tab: string) => { 161 | const newTab = tab as RunPanelTab; 162 | if (externalOnTabChange) { 163 | externalOnTabChange(newTab); 164 | } else { 165 | setInternalActiveTab(newTab); 166 | } 167 | }; 168 | 169 | const handleRun = async () => { 170 | onRun?.(); 171 | }; 172 | 173 | const handleToggleCollapse = () => { 174 | setIsCollapsed(!isCollapsed); 175 | onToggleCollapse?.(); 176 | }; 177 | 178 | return ( 179 | <> 180 | 188 |
189 | 190 | 198 | 199 | 200 | 201 | 202 | 203 |
204 |
205 | 206 | ); 207 | }; 208 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RunPanel"; 2 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/tabs/ConsoleTab/ConsoleTab.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { ConsoleTab } from "./ConsoleTab"; 3 | 4 | const meta = { 5 | title: "Playground/Code/RunPanel/tabs/ConsoleTab", 6 | component: ConsoleTab, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | const sampleLogs = [ 13 | { 14 | timestamp: Date.now(), 15 | level: "info", 16 | message: "Application started", 17 | count: 1, 18 | }, 19 | { 20 | timestamp: Date.now() + 1000, 21 | level: "warn", 22 | message: "Deprecated feature used", 23 | count: 3, 24 | }, 25 | { 26 | timestamp: Date.now() + 2000, 27 | level: "error", 28 | message: "Failed to fetch data", 29 | count: 1, 30 | }, 31 | { 32 | timestamp: Date.now() + 3000, 33 | level: "debug", 34 | message: "Debug information", 35 | count: 1, 36 | }, 37 | ]; 38 | 39 | export const Default: Story = { 40 | args: { 41 | logs: sampleLogs, 42 | }, 43 | }; 44 | 45 | export const Empty: Story = { 46 | args: { 47 | logs: [], 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/tabs/ConsoleTab/ConsoleTab.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | import { formatCount } from "@/lib/formatters"; 3 | import { ScrollArea } from "@/components/ui/scroll-area"; 4 | 5 | export interface ConsoleLog { 6 | timestamp: number; 7 | level: string; 8 | message: string; 9 | count: number; 10 | } 11 | 12 | interface ConsoleTabProps { 13 | logs: ConsoleLog[] | null; 14 | } 15 | 16 | const getLogColor = (type: string) => { 17 | switch (type) { 18 | case "error": { 19 | return "text-red-600"; 20 | } 21 | case "warn": { 22 | return "text-yellow-600"; 23 | } 24 | case "info": { 25 | return "text-blue-600"; 26 | } 27 | case "debug": { 28 | return "text-purple-600"; 29 | } 30 | default: { 31 | return "text-foreground"; 32 | } 33 | } 34 | }; 35 | 36 | export const ConsoleTab = ({ logs }: ConsoleTabProps) => { 37 | const displayLogs = logs ?? []; 38 | 39 | return ( 40 | 41 |
42 | {displayLogs.length === 0 && ( 43 |
No console output yet.
44 | )} 45 | {displayLogs.map((log, index) => ( 46 | // eslint-disable-next-line react/no-array-index-key 47 |
48 | 49 | {format(log.timestamp, "HH:mm:ss.SSS")} 50 | 51 | {log.message} 52 | {log.count > 1 && ( 53 | 54 | x{formatCount(log.count)} 55 | 56 | )} 57 |
58 | ))} 59 |
60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/tabs/ConsoleTab/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ConsoleTab"; 2 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/tabs/RunTab/RunTab.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { RunTab } from "./RunTab"; 3 | 4 | const meta = { 5 | title: "Playground/Code/RunPanel/tabs/RunTab", 6 | component: RunTab, 7 | } satisfies Meta; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | const mockChartData = [ 13 | { time: 0, timePerOp: 12, iterations: 0 }, 14 | { time: 100, timePerOp: 11, iterations: 100 }, 15 | { time: 200, timePerOp: 10.5, iterations: 200 }, 16 | { time: 300, timePerOp: 10.2, iterations: 300 }, 17 | { time: 400, timePerOp: 10.1, iterations: 400 }, 18 | ]; 19 | 20 | const now = Date.now(); 21 | 22 | export const Running: Story = { 23 | args: { 24 | isRunning: true, 25 | latestRun: { 26 | id: "1", 27 | implementationId: "impl-1", 28 | createdAt: now, 29 | warmupStartedAt: now + 100, 30 | warmupEndedAt: now + 200, 31 | status: "running", 32 | progress: 45.5, 33 | completedIterations: 455, 34 | totalIterations: 1000, 35 | elapsedTime: 4550, 36 | error: null, 37 | result: null, 38 | filename: "test.js", 39 | originalCode: "function test() {}", 40 | processedCode: "function test() {}", 41 | }, 42 | chartData: mockChartData, 43 | clearChartData: () => {}, 44 | }, 45 | }; 46 | 47 | export const Completed: Story = { 48 | args: { 49 | isRunning: false, 50 | latestRun: { 51 | id: "1", 52 | implementationId: "impl-1", 53 | createdAt: now, 54 | warmupStartedAt: now + 100, 55 | warmupEndedAt: now + 200, 56 | status: "completed", 57 | progress: 100, 58 | completedIterations: 1000, 59 | totalIterations: 1000, 60 | elapsedTime: 10_000, 61 | error: null, 62 | filename: "test.js", 63 | originalCode: "function test() {}", 64 | processedCode: "function test() {}", 65 | result: { 66 | name: "test", 67 | stats: { 68 | samples: 1000, 69 | batches: 10, 70 | time: { 71 | total: 10_000, 72 | average: 10, 73 | min: 8, 74 | max: 15, 75 | percentile50: 10, 76 | percentile90: 13, 77 | percentile95: 14, 78 | }, 79 | opsPerSecond: { 80 | average: 100_000, 81 | min: 66_666, 82 | max: 125_000, 83 | margin: 0.5, 84 | }, 85 | }, 86 | }, 87 | }, 88 | chartData: mockChartData, 89 | clearChartData: () => {}, 90 | }, 91 | }; 92 | 93 | export const Error: Story = { 94 | args: { 95 | isRunning: false, 96 | latestRun: { 97 | id: "1", 98 | implementationId: "impl-1", 99 | createdAt: now, 100 | warmupStartedAt: now + 100, 101 | warmupEndedAt: now + 200, 102 | status: "failed", 103 | progress: 45.5, 104 | completedIterations: 455, 105 | totalIterations: 1000, 106 | elapsedTime: 4550, 107 | error: "Failed to execute benchmark: Stack overflow", 108 | filename: "test.js", 109 | originalCode: "function test() {}", 110 | processedCode: "function test() {}", 111 | result: null, 112 | }, 113 | chartData: mockChartData, 114 | clearChartData: () => {}, 115 | }, 116 | }; 117 | -------------------------------------------------------------------------------- /src/components/playground/code/RunPanel/tabs/RunTab/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RunTab"; 2 | -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonChart/ComparisonChart.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { BenchmarkStatus } from "@/stores/benchmarkStore"; 3 | import { ComparisonChart } from "./ComparisonChart"; 4 | 5 | const meta = { 6 | title: "Playground/Compare/ComparisonChart", 7 | component: ComparisonChart, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | const mockImplementations = [ 14 | { 15 | id: "1", 16 | filename: "implementation1.ts", 17 | content: 'console.log("test")', 18 | }, 19 | { 20 | id: "2", 21 | filename: "implementation2.ts", 22 | content: 'console.log("test2")', 23 | }, 24 | ]; 25 | 26 | const now = Date.now(); 27 | 28 | const mockRuns = { 29 | "1": [ 30 | { 31 | id: "run1", 32 | implementationId: "1", 33 | status: "completed" as BenchmarkStatus, 34 | filename: "implementation1.ts", 35 | originalCode: 'console.log("test")', 36 | processedCode: 'console.log("test")', 37 | elapsedTime: 1000, 38 | completedIterations: 100_000, 39 | totalIterations: 1000, 40 | progress: 100, 41 | createdAt: now, 42 | warmupStartedAt: now + 100, 43 | warmupEndedAt: now + 200, 44 | error: null, 45 | result: { 46 | name: "implementation1.ts", 47 | stats: { 48 | samples: 100, 49 | batches: 10, 50 | time: { 51 | total: 1000, 52 | min: 900, 53 | max: 1100, 54 | average: 1000, 55 | percentile50: 1000, 56 | percentile90: 1050, 57 | percentile95: 1075, 58 | }, 59 | opsPerSecond: { 60 | average: 200, 61 | max: 1100, 62 | min: 900, 63 | margin: 50, 64 | }, 65 | memory: 1024, 66 | }, 67 | }, 68 | }, 69 | ], 70 | "2": [ 71 | { 72 | id: "run2", 73 | implementationId: "2", 74 | status: "running" as BenchmarkStatus, 75 | filename: "implementation2.ts", 76 | originalCode: 'console.log("test2")', 77 | processedCode: 'console.log("test2")', 78 | elapsedTime: 500, 79 | completedIterations: 500, 80 | totalIterations: 1000, 81 | progress: 50, 82 | createdAt: now, 83 | warmupStartedAt: now + 100, 84 | warmupEndedAt: null, 85 | error: null, 86 | result: { 87 | name: "implementation2.ts", 88 | stats: { 89 | samples: 50, 90 | batches: 5, 91 | time: { 92 | total: 500, 93 | min: 450, 94 | max: 550, 95 | average: 500, 96 | percentile50: 500, 97 | percentile90: 525, 98 | percentile95: 537, 99 | }, 100 | opsPerSecond: { 101 | average: 1500, 102 | max: 550, 103 | min: 450, 104 | margin: 25, 105 | }, 106 | memory: 512, 107 | }, 108 | }, 109 | }, 110 | ], 111 | }; 112 | 113 | export const Default: Story = { 114 | args: { 115 | implementations: mockImplementations, 116 | runs: mockRuns, 117 | }, 118 | }; 119 | -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonChart/ComparisonChart.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { 3 | Bar, 4 | CartesianGrid, 5 | Legend, 6 | BarChart as RechartsBarChart, 7 | ResponsiveContainer, 8 | Tooltip, 9 | XAxis, 10 | YAxis, 11 | } from "recharts"; 12 | import { BenchmarkRun } from "@/stores/benchmarkStore"; 13 | import { Implementation } from "@/stores/persistentStore"; 14 | import { formatCountShort } from "@/lib/formatters"; 15 | 16 | interface ComparisonChartProps { 17 | implementations: Implementation[]; 18 | runs: Record; 19 | } 20 | 21 | export const ComparisonChart = ({ implementations, runs }: ComparisonChartProps) => { 22 | const barData = useMemo(() => { 23 | return implementations.map((item) => { 24 | const run = runs[item.id]?.at(-1); 25 | const isRunning = run?.status === "running" || run?.status === "warmup"; 26 | const opsPerSec = 27 | isRunning && run.completedIterations ? 28 | run.completedIterations / (run.elapsedTime / 1000) 29 | : run?.result?.stats.opsPerSecond.average || 0; 30 | 31 | return { 32 | name: item.filename, 33 | "Operations/sec": opsPerSec, 34 | }; 35 | }); 36 | }, [implementations, runs]); 37 | 38 | return ( 39 |
40 |
41 | 42 | 43 | 44 | 45 | 57 | `${Math.round(value).toLocaleString()} ops/sec`} 65 | /> 66 | 67 | 68 | 69 | 70 |
71 |
72 | ); 73 | }; 74 | 75 | -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonChart/index.ts: -------------------------------------------------------------------------------- 1 | export { ComparisonChart } from './ComparisonChart'; -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonTable/ComparisonTable.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { BenchmarkStatus } from "@/stores/benchmarkStore"; 3 | import { ComparisonTable } from "./ComparisonTable"; 4 | 5 | const meta = { 6 | title: "Playground/Compare/ComparisonTable", 7 | component: ComparisonTable, 8 | } satisfies Meta; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | const mockImplementations = [ 14 | { 15 | id: "1", 16 | filename: "implementation1.ts", 17 | content: 'console.log("test")', 18 | selected: false, 19 | }, 20 | { 21 | id: "2", 22 | filename: "implementation2.ts", 23 | content: 'console.log("test2")', 24 | selected: true, 25 | }, 26 | ]; 27 | 28 | const now = Date.now(); 29 | 30 | const mockRuns = { 31 | "1": [ 32 | { 33 | id: "run1", 34 | implementationId: "1", 35 | status: "completed" as BenchmarkStatus, 36 | filename: "implementation1.ts", 37 | originalCode: 'console.log("test")', 38 | processedCode: 'console.log("test")', 39 | elapsedTime: 1000, 40 | completedIterations: 1000, 41 | totalIterations: 1000, 42 | progress: 100, 43 | createdAt: now, 44 | warmupStartedAt: now + 100, 45 | warmupEndedAt: now + 200, 46 | error: null, 47 | result: { 48 | name: "implementation1.ts", 49 | stats: { 50 | samples: 100, 51 | batches: 10, 52 | time: { 53 | total: 1000, 54 | min: 900, 55 | max: 1100, 56 | average: 1000, 57 | percentile50: 1000, 58 | percentile90: 1050, 59 | percentile95: 1075, 60 | }, 61 | opsPerSecond: { 62 | average: 1000, 63 | max: 1100, 64 | min: 900, 65 | margin: 50, 66 | }, 67 | memory: 1024, 68 | }, 69 | }, 70 | }, 71 | ], 72 | "2": [ 73 | { 74 | id: "run2", 75 | implementationId: "2", 76 | status: "running" as BenchmarkStatus, 77 | filename: "implementation2.ts", 78 | originalCode: 'console.log("test2")', 79 | processedCode: 'console.log("test2")', 80 | elapsedTime: 500, 81 | completedIterations: 500, 82 | totalIterations: 1000, 83 | progress: 50, 84 | createdAt: now, 85 | warmupStartedAt: now + 100, 86 | warmupEndedAt: null, 87 | error: null, 88 | result: { 89 | name: "implementation2.ts", 90 | stats: { 91 | samples: 50, 92 | batches: 5, 93 | time: { 94 | total: 500, 95 | min: 450, 96 | max: 550, 97 | average: 500, 98 | percentile50: 500, 99 | percentile90: 525, 100 | percentile95: 537, 101 | }, 102 | opsPerSecond: { 103 | average: 500, 104 | max: 550, 105 | min: 450, 106 | margin: 25, 107 | }, 108 | memory: 512, 109 | }, 110 | }, 111 | }, 112 | ], 113 | }; 114 | 115 | export const Default: Story = { 116 | args: { 117 | implementations: mockImplementations, 118 | runs: mockRuns, 119 | isRunning: true, 120 | onSelectAll: () => {}, 121 | onToggleSelect: () => {}, 122 | onRunSingle: () => {}, 123 | onStop: () => {}, 124 | }, 125 | }; 126 | 127 | -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonTable/ComparisonTable.tsx: -------------------------------------------------------------------------------- 1 | import { Info, Square } from "lucide-react"; 2 | import { BenchmarkRun } from "@/stores/benchmarkStore"; 3 | import { Implementation } from "@/stores/persistentStore"; 4 | import { formatCount, formatDuration } from "@/lib/formatters"; 5 | import { cn } from "@/lib/utils"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Checkbox } from "@/components/ui/checkbox"; 8 | import { Progress } from "@/components/ui/progress"; 9 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; 10 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; 11 | 12 | interface ComparisonTableProps { 13 | implementations: (Implementation & { selected: boolean })[]; 14 | runs: Record; 15 | isRunning: boolean; 16 | onSelectAll: (checked: boolean) => void; 17 | onToggleSelect: (id: string) => void; 18 | onRunSingle: (impl: Implementation) => void; 19 | onStop: (runId: string) => void; 20 | } 21 | 22 | interface RunMetrics { 23 | opsPerSecond: number; 24 | percentageDiff: number; 25 | isBest: boolean; 26 | } 27 | 28 | interface ImplementationRun { 29 | implementation: Implementation & { selected: boolean }; 30 | run: BenchmarkRun | undefined; 31 | metrics: RunMetrics | undefined; 32 | } 33 | 34 | const calculateOpsPerSecond = (run: BenchmarkRun): number => { 35 | if (run.status === "completed" && run.result?.stats.opsPerSecond.average) { 36 | return run.result.stats.opsPerSecond.average; 37 | } 38 | if (run.status === "running" && run.elapsedTime > 0 && run.completedIterations > 0) { 39 | return (run.completedIterations / run.elapsedTime) * 1000; 40 | } 41 | return 0; 42 | }; 43 | 44 | const calculateRunMetrics = (runs: ImplementationRun[]): Map => { 45 | const metrics = new Map(); 46 | 47 | let best = 0; 48 | for (const item of runs) { 49 | const runOpsPerSecond = item.run ? calculateOpsPerSecond(item.run) : 0; 50 | if (runOpsPerSecond > best) best = runOpsPerSecond; 51 | } 52 | 53 | for (const item of runs) { 54 | if (item.run?.status === "completed" && item.run.result?.stats.opsPerSecond.average) { 55 | const opsPerSecond = item.run.result.stats.opsPerSecond.average; 56 | metrics.set(item.implementation.id, { 57 | opsPerSecond, 58 | percentageDiff: ((best - opsPerSecond) / best) * 100, 59 | isBest: opsPerSecond === best, 60 | }); 61 | } else if (item.run?.status === "running") { 62 | const currentOps = calculateOpsPerSecond(item.run); 63 | if (currentOps > 0) { 64 | metrics.set(item.implementation.id, { 65 | opsPerSecond: currentOps, 66 | percentageDiff: ((best - currentOps) / best) * 100, 67 | isBest: currentOps === best, 68 | }); 69 | } 70 | } 71 | } 72 | return metrics; 73 | }; 74 | 75 | const OpsPerSecondCell = ({ 76 | run, 77 | metrics, 78 | }: { 79 | run: BenchmarkRun | undefined; 80 | metrics: RunMetrics | undefined; 81 | }) => { 82 | if (!run) return -; 83 | 84 | const opsPerSecond = calculateOpsPerSecond(run); 85 | if (opsPerSecond === 0) return -; 86 | 87 | const isRunning = run.status === "running"; 88 | 89 | return ( 90 |
91 | 99 | {formatCount(Math.round(opsPerSecond))} 100 | 101 | {metrics?.isBest && Best} 102 | {metrics && !metrics.isBest && metrics.percentageDiff > 0 && ( 103 | (-{metrics.percentageDiff.toFixed(1)}%) 104 | )} 105 |
106 | ); 107 | }; 108 | 109 | const ActionCell = ({ 110 | run, 111 | implementation, 112 | isRunning, 113 | onRunSingle, 114 | onStop, 115 | }: { 116 | run: BenchmarkRun | undefined; 117 | implementation: Implementation; 118 | isRunning: boolean; 119 | onRunSingle: (impl: Implementation) => void; 120 | onStop: (runId: string) => void; 121 | }) => { 122 | const isActiveRun = run?.status === "running" || run?.status === "warmup"; 123 | 124 | if (isActiveRun && run) { 125 | return ( 126 |
127 | 128 | 131 |
132 | ); 133 | } 134 | 135 | return ( 136 |
137 | 140 |
141 | ); 142 | }; 143 | 144 | const HeaderTooltip = ({ label, tooltip }: { label: string; tooltip: string }) => ( 145 |
146 | {label} 147 | 148 | 149 | 150 | 151 | 152 | 153 |

{tooltip}

154 |
155 |
156 |
157 |
158 | ); 159 | 160 | export const ComparisonTable = ({ 161 | implementations, 162 | runs, 163 | isRunning, 164 | onSelectAll, 165 | onToggleSelect, 166 | onRunSingle, 167 | onStop, 168 | }: ComparisonTableProps) => { 169 | const implementationRuns: ImplementationRun[] = implementations.map((implementation) => ({ 170 | implementation, 171 | run: runs[implementation.id]?.at(-1), 172 | metrics: undefined, 173 | })); 174 | 175 | const runMetrics = calculateRunMetrics(implementationRuns); 176 | for (const item of implementationRuns) { 177 | item.metrics = runMetrics.get(item.implementation.id); 178 | } 179 | 180 | return ( 181 | 182 | 183 | 184 | 185 | 0 && implementations.every((impl) => impl.selected)} 187 | onCheckedChange={onSelectAll} 188 | /> 189 | 190 | Name 191 | Status 192 | 193 | 194 | 195 | 196 | 197 | 198 | Action 199 | 200 | 201 | 202 | {implementationRuns.map(({ implementation, run, metrics }) => ( 203 | 204 | 205 | onToggleSelect(implementation.id)} 208 | /> 209 | 210 | {implementation.filename} 211 | {run?.status ?? "N/A"} 212 | {run ? formatDuration(run.elapsedTime) : "-"} 213 | 214 | 215 | 216 | 217 | 224 | 225 | 226 | ))} 227 | 228 |
229 | ); 230 | }; 231 | -------------------------------------------------------------------------------- /src/components/playground/compare/ComparisonTable/index.ts: -------------------------------------------------------------------------------- 1 | export { ComparisonTable } from './ComparisonTable'; -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const badgeVariants = cva( 6 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 7 | { 8 | variants: { 9 | variant: { 10 | default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 11 | secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 12 | destructive: 13 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 14 | outline: "text-foreground", 15 | success: 16 | "border-green-600 bg-green-100 text-green-700 hover:bg-green-200 dark:border-green-400 dark:bg-green-800 dark:text-green-300 dark:hover:bg-green-700", 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: "default", 21 | }, 22 | }, 23 | ); 24 | 25 | export interface BadgeProps 26 | extends React.HTMLAttributes, 27 | VariantProps {} 28 | 29 | function Badge({ className, variant, ...props }: BadgeProps) { 30 | return
; 31 | } 32 | 33 | export { Badge, badgeVariants }; 34 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | import { cn } from "@/lib/utils"; 5 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", 13 | destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 14 | outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 15 | secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 16 | ghost: "hover:bg-accent hover:text-accent-foreground", 17 | link: "text-primary underline-offset-4 hover:underline", 18 | }, 19 | size: { 20 | default: "h-9 px-4 py-2", 21 | sm: "h-8 rounded-md px-3 text-xs", 22 | lg: "h-10 rounded-md px-8", 23 | icon: "h-9 w-9", 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: "default", 28 | size: "default", 29 | }, 30 | }, 31 | ); 32 | 33 | export interface ButtonProps 34 | extends React.ButtonHTMLAttributes, 35 | VariantProps { 36 | asChild?: boolean; 37 | tooltip?: string; 38 | } 39 | 40 | const Button = React.forwardRef( 41 | ({ className, variant, size, asChild = false, tooltip, ...props }, ref) => { 42 | const Component = asChild ? Slot : "button"; 43 | const button = ( 44 | 45 | ); 46 | 47 | if (tooltip) { 48 | return ( 49 | 50 | 51 | {button} 52 | {tooltip} 53 | 54 | 55 | ); 56 | } 57 | 58 | return button; 59 | }, 60 | ); 61 | Button.displayName = "Button"; 62 | 63 | export { Button, buttonVariants }; 64 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | const Card = React.forwardRef>( 5 | ({ className, ...props }, ref) => ( 6 |
11 | ), 12 | ); 13 | Card.displayName = "Card"; 14 | 15 | const CardHeader = React.forwardRef>( 16 | ({ className, ...props }, ref) => ( 17 |
18 | ), 19 | ); 20 | CardHeader.displayName = "CardHeader"; 21 | 22 | const CardTitle = React.forwardRef>( 23 | ({ className, ...props }, ref) => ( 24 |
25 | ), 26 | ); 27 | CardTitle.displayName = "CardTitle"; 28 | 29 | const CardDescription = React.forwardRef>( 30 | ({ className, ...props }, ref) => ( 31 |
32 | ), 33 | ); 34 | CardDescription.displayName = "CardDescription"; 35 | 36 | const CardContent = React.forwardRef>( 37 | ({ className, ...props }, ref) =>
, 38 | ); 39 | CardContent.displayName = "CardContent"; 40 | 41 | const CardFooter = React.forwardRef>( 42 | ({ className, ...props }, ref) => ( 43 |
44 | ), 45 | ); 46 | CardFooter.displayName = "CardFooter"; 47 | 48 | export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; 49 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 3 | import { Check } from "lucide-react"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Checkbox = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 19 | 20 | 21 | 22 | )); 23 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 24 | 25 | export { Checkbox }; 26 | -------------------------------------------------------------------------------- /src/components/ui/context-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; 3 | import { Check, ChevronRight, Circle } from "lucide-react"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | const ContextMenu = ContextMenuPrimitive.Root; 7 | const ContextMenuTrigger = ContextMenuPrimitive.Trigger; 8 | const ContextMenuGroup = ContextMenuPrimitive.Group; 9 | const ContextMenuPortal = ContextMenuPrimitive.Portal; 10 | const ContextMenuSub = ContextMenuPrimitive.Sub; 11 | const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; 12 | 13 | const ContextMenuSubTrigger = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & { 16 | inset?: boolean; 17 | } 18 | >(({ className, inset, children, ...props }, ref) => ( 19 | 28 | {children} 29 | 30 | 31 | )); 32 | ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; 33 | 34 | const ContextMenuSubContent = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, ...props }, ref) => ( 38 | 46 | )); 47 | ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; 48 | 49 | const ContextMenuContent = React.forwardRef< 50 | React.ElementRef, 51 | React.ComponentPropsWithoutRef 52 | >(({ className, ...props }, ref) => ( 53 | 54 | 62 | 63 | )); 64 | ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; 65 | 66 | const ContextMenuItem = React.forwardRef< 67 | React.ElementRef, 68 | React.ComponentPropsWithoutRef & { 69 | inset?: boolean; 70 | } 71 | >(({ className, inset, ...props }, ref) => ( 72 | 81 | )); 82 | ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; 83 | 84 | const ContextMenuCheckboxItem = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, children, checked, ...props }, ref) => ( 88 | 97 | 98 | 99 | 100 | 101 | 102 | {children} 103 | 104 | )); 105 | ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; 106 | 107 | const ContextMenuRadioItem = React.forwardRef< 108 | React.ElementRef, 109 | React.ComponentPropsWithoutRef 110 | >(({ className, children, ...props }, ref) => ( 111 | 119 | 120 | 121 | 122 | 123 | 124 | {children} 125 | 126 | )); 127 | ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; 128 | 129 | const ContextMenuLabel = React.forwardRef< 130 | React.ElementRef, 131 | React.ComponentPropsWithoutRef & { 132 | inset?: boolean; 133 | } 134 | >(({ className, inset, ...props }, ref) => ( 135 | 140 | )); 141 | ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; 142 | 143 | const ContextMenuSeparator = React.forwardRef< 144 | React.ElementRef, 145 | React.ComponentPropsWithoutRef 146 | >(({ className, ...props }, ref) => ( 147 | 152 | )); 153 | ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; 154 | 155 | const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { 156 | return ( 157 | 158 | ); 159 | }; 160 | ContextMenuShortcut.displayName = "ContextMenuShortcut"; 161 | 162 | export { ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger }; 163 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import { X } from "lucide-react"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Dialog = DialogPrimitive.Root; 7 | const DialogTrigger = DialogPrimitive.Trigger; 8 | const DialogPortal = DialogPrimitive.Portal; 9 | const DialogClose = DialogPrimitive.Close; 10 | 11 | const DialogOverlay = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )); 24 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 25 | 26 | const DialogContent = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, children, ...props }, ref) => ( 30 | 31 | 32 | 40 | {children} 41 | 42 | 43 | Close 44 | 45 | 46 | 47 | )); 48 | DialogContent.displayName = DialogPrimitive.Content.displayName; 49 | 50 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( 51 |
52 | ); 53 | DialogHeader.displayName = "DialogHeader"; 54 | 55 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( 56 |
60 | ); 61 | DialogFooter.displayName = "DialogFooter"; 62 | 63 | const DialogTitle = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef 66 | >(({ className, ...props }, ref) => ( 67 | 72 | )); 73 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 74 | 75 | const DialogDescription = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, ...props }, ref) => ( 79 | 84 | )); 85 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 86 | 87 | export { 88 | Dialog, 89 | DialogClose, 90 | DialogContent, 91 | DialogDescription, 92 | DialogFooter, 93 | DialogHeader, 94 | DialogOverlay, 95 | DialogPortal, 96 | DialogTitle, 97 | DialogTrigger, 98 | }; 99 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 3 | import { Check, ChevronRight, Circle } from "lucide-react"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | const DropdownMenu = DropdownMenuPrimitive.Root; 7 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 8 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 9 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 10 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 11 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 12 | 13 | const DropdownMenuSubTrigger = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & { 16 | inset?: boolean; 17 | } 18 | >(({ className, inset, children, ...props }, ref) => ( 19 | 28 | {children} 29 | 30 | 31 | )); 32 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; 33 | 34 | const DropdownMenuSubContent = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, ...props }, ref) => ( 38 | 46 | )); 47 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; 48 | 49 | const DropdownMenuContent = React.forwardRef< 50 | React.ElementRef, 51 | React.ComponentPropsWithoutRef 52 | >(({ className, sideOffset = 4, ...props }, ref) => ( 53 | 54 | 64 | 65 | )); 66 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 67 | 68 | const DropdownMenuItem = React.forwardRef< 69 | React.ElementRef, 70 | React.ComponentPropsWithoutRef & { 71 | inset?: boolean; 72 | } 73 | >(({ className, inset, ...props }, ref) => ( 74 | svg]:size-4 [&>svg]:shrink-0", 78 | inset && "pl-8", 79 | className, 80 | )} 81 | {...props} 82 | /> 83 | )); 84 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 85 | 86 | const DropdownMenuCheckboxItem = React.forwardRef< 87 | React.ElementRef, 88 | React.ComponentPropsWithoutRef 89 | >(({ className, children, checked, ...props }, ref) => ( 90 | 99 | 100 | 101 | 102 | 103 | 104 | {children} 105 | 106 | )); 107 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; 108 | 109 | const DropdownMenuRadioItem = React.forwardRef< 110 | React.ElementRef, 111 | React.ComponentPropsWithoutRef 112 | >(({ className, children, ...props }, ref) => ( 113 | 121 | 122 | 123 | 124 | 125 | 126 | {children} 127 | 128 | )); 129 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 130 | 131 | const DropdownMenuLabel = React.forwardRef< 132 | React.ElementRef, 133 | React.ComponentPropsWithoutRef & { 134 | inset?: boolean; 135 | } 136 | >(({ className, inset, ...props }, ref) => ( 137 | 142 | )); 143 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 144 | 145 | const DropdownMenuSeparator = React.forwardRef< 146 | React.ElementRef, 147 | React.ComponentPropsWithoutRef 148 | >(({ className, ...props }, ref) => ( 149 | 154 | )); 155 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 156 | 157 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { 158 | return ; 159 | }; 160 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 161 | 162 | export { 163 | DropdownMenu, 164 | DropdownMenuCheckboxItem, 165 | DropdownMenuContent, 166 | DropdownMenuGroup, 167 | DropdownMenuItem, 168 | DropdownMenuLabel, 169 | DropdownMenuPortal, 170 | DropdownMenuRadioGroup, 171 | DropdownMenuRadioItem, 172 | DropdownMenuSeparator, 173 | DropdownMenuShortcut, 174 | DropdownMenuSub, 175 | DropdownMenuSubContent, 176 | DropdownMenuSubTrigger, 177 | DropdownMenuTrigger, 178 | }; 179 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | const Input = React.forwardRef>( 5 | ({ className, type, ...props }, ref) => { 6 | return ( 7 | 16 | ); 17 | }, 18 | ); 19 | Input.displayName = "Input"; 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | const labelVariants = cva( 7 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 8 | ); 9 | 10 | const Label = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef & VariantProps 13 | >(({ className, ...props }, ref) => ( 14 | 15 | )); 16 | Label.displayName = LabelPrimitive.Root.displayName; 17 | 18 | export { Label }; 19 | -------------------------------------------------------------------------------- /src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 15 | 19 | 20 | )); 21 | Progress.displayName = ProgressPrimitive.Root.displayName; 22 | 23 | export { Progress }; 24 | -------------------------------------------------------------------------------- /src/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import { GripVertical } from "lucide-react"; 2 | import * as ResizablePrimitive from "react-resizable-panels"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const ResizablePanelGroup = ({ 6 | className, 7 | ...props 8 | }: React.ComponentProps) => ( 9 | 13 | ); 14 | 15 | const ResizablePanel = ResizablePrimitive.Panel; 16 | 17 | const ResizableHandle = ({ 18 | withHandle, 19 | className, 20 | ...props 21 | }: React.ComponentProps & { 22 | withHandle?: boolean; 23 | }) => ( 24 | div]:rotate-90", 27 | className, 28 | )} 29 | {...props} 30 | > 31 | {withHandle && ( 32 |
33 | 34 |
35 | )} 36 |
37 | ); 38 | 39 | export { ResizableHandle, ResizablePanel, ResizablePanelGroup }; 40 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const ScrollBar = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, orientation = "vertical", ...props }, ref) => ( 9 | 20 | 21 | 22 | )); 23 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 24 | 25 | const ScrollArea = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, children, ...props }, ref) => ( 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | 36 | )); 37 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 38 | 39 | export { ScrollArea, ScrollBar }; 40 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SelectPrimitive from "@radix-ui/react-select"; 3 | import { Check, ChevronDown, ChevronUp } from "lucide-react"; 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Select = SelectPrimitive.Root; 7 | const SelectGroup = SelectPrimitive.Group; 8 | const SelectValue = SelectPrimitive.Value; 9 | 10 | const SelectTrigger = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, children, ...props }, ref) => ( 14 | span]:line-clamp-1", 18 | className, 19 | )} 20 | {...props} 21 | > 22 | {children} 23 | 24 | 25 | 26 | 27 | )); 28 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 29 | 30 | const SelectScrollUpButton = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 39 | 40 | 41 | )); 42 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 43 | 44 | const SelectScrollDownButton = React.forwardRef< 45 | React.ElementRef, 46 | React.ComponentPropsWithoutRef 47 | >(({ className, ...props }, ref) => ( 48 | 53 | 54 | 55 | )); 56 | SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; 57 | 58 | const SelectContent = React.forwardRef< 59 | React.ElementRef, 60 | React.ComponentPropsWithoutRef 61 | >(({ className, children, position = "popper", ...props }, ref) => ( 62 | 63 | 74 | 75 | 82 | {children} 83 | 84 | 85 | 86 | 87 | )); 88 | SelectContent.displayName = SelectPrimitive.Content.displayName; 89 | 90 | const SelectLabel = React.forwardRef< 91 | React.ElementRef, 92 | React.ComponentPropsWithoutRef 93 | >(({ className, ...props }, ref) => ( 94 | 99 | )); 100 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 101 | 102 | const SelectItem = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, children, ...props }, ref) => ( 106 | 114 | 115 | 116 | 117 | 118 | 119 | {children} 120 | 121 | )); 122 | SelectItem.displayName = SelectPrimitive.Item.displayName; 123 | 124 | const SelectSeparator = React.forwardRef< 125 | React.ElementRef, 126 | React.ComponentPropsWithoutRef 127 | >(({ className, ...props }, ref) => ( 128 | 129 | )); 130 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 131 | 132 | export { 133 | Select, 134 | SelectContent, 135 | SelectGroup, 136 | SelectItem, 137 | SelectLabel, 138 | SelectScrollDownButton, 139 | SelectScrollUpButton, 140 | SelectSeparator, 141 | SelectTrigger, 142 | SelectValue, 143 | }; 144 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Switch = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 17 | 22 | 23 | )); 24 | Switch.displayName = SwitchPrimitives.Root.displayName; 25 | 26 | export { Switch }; 27 | -------------------------------------------------------------------------------- /src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | const Table = React.forwardRef>( 5 | ({ className, ...props }, ref) => ( 6 |
7 | 8 | 9 | ), 10 | ); 11 | Table.displayName = "Table"; 12 | 13 | const TableHeader = React.forwardRef>( 14 | ({ className, ...props }, ref) => ( 15 | 16 | ), 17 | ); 18 | TableHeader.displayName = "TableHeader"; 19 | 20 | const TableBody = React.forwardRef>( 21 | ({ className, ...props }, ref) => ( 22 | 23 | ), 24 | ); 25 | TableBody.displayName = "TableBody"; 26 | 27 | const TableFooter = React.forwardRef>( 28 | ({ className, ...props }, ref) => ( 29 | tr]:last:border-b-0", className)} 32 | {...props} 33 | /> 34 | ), 35 | ); 36 | TableFooter.displayName = "TableFooter"; 37 | 38 | const TableRow = React.forwardRef>( 39 | ({ className, ...props }, ref) => ( 40 | 45 | ), 46 | ); 47 | TableRow.displayName = "TableRow"; 48 | 49 | const TableHead = React.forwardRef>( 50 | ({ className, ...props }, ref) => ( 51 |
[role=checkbox]]:translate-y-[2px]", 55 | className, 56 | )} 57 | {...props} 58 | /> 59 | ), 60 | ); 61 | TableHead.displayName = "TableHead"; 62 | 63 | const TableCell = React.forwardRef>( 64 | ({ className, ...props }, ref) => ( 65 | [role=checkbox]]:translate-y-[2px]", 69 | className, 70 | )} 71 | {...props} 72 | /> 73 | ), 74 | ); 75 | TableCell.displayName = "TableCell"; 76 | 77 | const TableCaption = React.forwardRef>( 78 | ({ className, ...props }, ref) => ( 79 |
80 | ), 81 | ); 82 | TableCaption.displayName = "TableCaption"; 83 | 84 | export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow }; 85 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Tabs = TabsPrimitive.Root; 6 | 7 | const TabsList = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | )); 20 | TabsList.displayName = TabsPrimitive.List.displayName; 21 | 22 | const TabsTrigger = React.forwardRef< 23 | React.ElementRef, 24 | React.ComponentPropsWithoutRef 25 | >(({ className, ...props }, ref) => ( 26 | 34 | )); 35 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 36 | 37 | const TabsContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, ...props }, ref) => ( 41 | 49 | )); 50 | TabsContent.displayName = TabsPrimitive.Content.displayName; 51 | 52 | export { Tabs, TabsContent, TabsList, TabsTrigger }; 53 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const TooltipProvider = TooltipPrimitive.Provider; 6 | const Tooltip = TooltipPrimitive.Root; 7 | const TooltipTrigger = TooltipPrimitive.Trigger; 8 | 9 | const TooltipContent = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, sideOffset = 4, ...props }, ref) => ( 13 | 14 | 23 | 24 | )); 25 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 26 | 27 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; 28 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const features = { 2 | memory: { 3 | enabled: false, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const README_CONTENT = `# Quick Start Guide 2 | 3 | ## File Structure 4 | - setup.ts: Setup code to prepare data, create helper functions, etc. 5 | - implementations/*.{ts,js}: Implementation files containing benchmarkFn 6 | 7 | ## Writing Benchmarks 8 | 9 | 1. Setup (setup.ts): 10 | 11 | Everything you export will be available as a global for each implementation. 12 | 13 | \`\`\`typescript 14 | // create and export test data 15 | export const data = Array.from({ length: 1000 }, (_, i) => i); 16 | export const sum = (arr: number[]) => arr.reduce((a, b) => a + b, 0); 17 | \`\`\` 18 | 19 | 2. Implementations (example.ts): 20 | 21 | Implementation-specific code goes here, and you need to export a function named 'run'. 22 | 23 | \`\`\`typescript 24 | export const run = () => { 25 | return data.reduce(sum, 0); 26 | } 27 | \`\`\` 28 | `; 29 | 30 | export const DEFAULT_SETUP_CODE = ` 31 | // This is your setup file, use it to prepare data and create shared helpers. 32 | // Everything you export will be available as a global for each implementation. 33 | 34 | // create and export test data 35 | const generateTestData = (size: number) => { 36 | return Array.from({ length: size }, (_, i) => i); 37 | }; 38 | export const data = generateTestData(1000); 39 | 40 | // export a helper function 41 | export const sum = (a: number, b: number) => a + b; 42 | `.trim(); 43 | 44 | export const DEFAULT_SETUP_DTS = ` 45 | declare global { 46 | declare const data: number[]; 47 | declare const sum: (a: number, b: number) => number; 48 | } 49 | export {}; 50 | `.trim(); 51 | 52 | export const DEFAULT_IMPLEMENTATION = ` 53 | // You have access to everything you exported in setup.ts 54 | // All you need to do is export a function named "run" 55 | 56 | export const run = () => { 57 | return data.reduce(sum, 0); 58 | }; 59 | `.trim(); 60 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | @apply bg-white dark:bg-zinc-950; 8 | 9 | @media (prefers-color-scheme: dark) { 10 | color-scheme: dark; 11 | } 12 | } 13 | 14 | @layer base { 15 | :root { 16 | --background: 0 0% 100%; 17 | --foreground: 240 10% 3.9%; 18 | --card: 0 0% 100%; 19 | --card-foreground: 240 10% 3.9%; 20 | --popover: 0 0% 100%; 21 | --popover-foreground: 240 10% 3.9%; 22 | --primary: 240 5.9% 10%; 23 | --primary-foreground: 0 0% 98%; 24 | --secondary: 240 4.8% 95.9%; 25 | --secondary-foreground: 240 5.9% 10%; 26 | --muted: 240 4.8% 95.9%; 27 | --muted-foreground: 240 3.8% 46.1%; 28 | --accent: 240 4.8% 95.9%; 29 | --accent-foreground: 240 5.9% 10%; 30 | --destructive: 0 84.2% 60.2%; 31 | --destructive-foreground: 0 0% 98%; 32 | --border: 240 5.9% 90%; 33 | --input: 240 5.9% 90%; 34 | --ring: 240 10% 3.9%; 35 | --chart-1: 12 76% 61%; 36 | --chart-2: 173 58% 39%; 37 | --chart-3: 197 37% 24%; 38 | --chart-4: 43 74% 66%; 39 | --chart-5: 27 87% 67%; 40 | --radius: 0.5rem; 41 | } 42 | .dark { 43 | --background: 240 10% 3.9%; 44 | --foreground: 0 0% 98%; 45 | --card: 240 10% 3.9%; 46 | --card-foreground: 0 0% 98%; 47 | --popover: 240 10% 3.9%; 48 | --popover-foreground: 0 0% 98%; 49 | --primary: 0 0% 98%; 50 | --primary-foreground: 240 5.9% 10%; 51 | --secondary: 240 3.7% 15.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | --muted: 240 3.7% 15.9%; 54 | --muted-foreground: 240 5% 64.9%; 55 | --accent: 240 3.7% 15.9%; 56 | --accent-foreground: 0 0% 98%; 57 | --destructive: 0 62.8% 30.6%; 58 | --destructive-foreground: 0 0% 98%; 59 | --border: 240 3.7% 15.9%; 60 | --input: 240 3.7% 15.9%; 61 | --ring: 240 4.9% 83.9%; 62 | --chart-1: 220 70% 50%; 63 | --chart-2: 160 60% 45%; 64 | --chart-3: 30 80% 55%; 65 | --chart-4: 280 65% 60%; 66 | --chart-5: 340 75% 55%; 67 | } 68 | } 69 | 70 | @layer base { 71 | * { 72 | @apply border-border; 73 | } 74 | body { 75 | @apply bg-background text-foreground; 76 | } 77 | } 78 | 79 | .custom-scrollbar { 80 | scrollbar-gutter: stable; 81 | overflow: auto; 82 | } 83 | 84 | .custom-scrollbar::-webkit-scrollbar { 85 | width: 4px; 86 | height: 4px; 87 | } 88 | 89 | .custom-scrollbar::-webkit-scrollbar-track { 90 | background: transparent; 91 | } 92 | 93 | .custom-scrollbar::-webkit-scrollbar-thumb { 94 | background-color: rgb(212 212 212 / 0.4); 95 | border-radius: 8px; 96 | } 97 | 98 | .custom-scrollbar:hover::-webkit-scrollbar-thumb { 99 | background-color: rgb(163 163 163 / 0.8); 100 | } 101 | 102 | .dark .custom-scrollbar::-webkit-scrollbar-thumb { 103 | background-color: rgb(64 64 64 / 0.4); 104 | } 105 | 106 | .dark .custom-scrollbar:hover::-webkit-scrollbar-thumb { 107 | background-color: rgb(82 82 82 / 0.8); 108 | } 109 | 110 | .deep-overflow-visible * { 111 | overflow: visible; 112 | } 113 | -------------------------------------------------------------------------------- /src/hooks/useMonacoTabs.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { Implementation } from "@/stores/persistentStore"; 3 | import { MonacoTab } from "@/components/common/MonacoTab"; 4 | 5 | interface UseMonacoTabsOptions { 6 | initialActiveTabId?: string | null; 7 | onTabChange?: (tabId: string | null) => void; 8 | onTabClose?: (tabId: string) => void; 9 | } 10 | 11 | const defaultTabsIds = new Set(["README.md", "setup.ts"]); 12 | 13 | export const useMonacoTabs = (implementations: Implementation[], options?: UseMonacoTabsOptions) => { 14 | const [tabs, setTabs] = useState(() => { 15 | const initialTabs = [{ id: "README.md", name: "README.md", active: true }]; 16 | if (options?.initialActiveTabId) { 17 | const item = implementations.find((i) => i.id === options.initialActiveTabId); 18 | if (item) { 19 | initialTabs[0].active = false; 20 | initialTabs.push({ id: item.id, name: item.filename, active: true }); 21 | } 22 | } 23 | return initialTabs; 24 | }); 25 | 26 | const changeTab = useCallback( 27 | (tab: MonacoTab | string) => { 28 | const tabId = typeof tab === "string" ? tab : tab.id; 29 | setTabs((prev) => 30 | prev.map((item) => ({ 31 | ...item, 32 | active: item.id === tabId, 33 | })), 34 | ); 35 | options?.onTabChange?.(tabId); 36 | }, 37 | [options], 38 | ); 39 | 40 | const closeTab = useCallback( 41 | (tab: MonacoTab) => { 42 | let nextActiveTabId: string | null = null; 43 | let hasNoOpenTabs = false; 44 | setTabs((prev) => { 45 | const filtered = prev.filter((item) => item.id !== tab.id); 46 | if (filtered.length === 0) { 47 | hasNoOpenTabs = true; 48 | } else if (tab.active) { 49 | filtered[filtered.length - 1].active = true; 50 | nextActiveTabId = filtered[filtered.length - 1].id; 51 | } 52 | return filtered; 53 | }); 54 | 55 | options?.onTabClose?.(tab.id); 56 | 57 | if (hasNoOpenTabs) { 58 | const newTab = { id: "README.md", name: "README.md", active: true }; 59 | setTabs((prev) => [...prev.map((item) => ({ ...item, active: false })), newTab]); 60 | changeTab(newTab); 61 | } else if (nextActiveTabId) { 62 | changeTab(nextActiveTabId); 63 | } 64 | }, 65 | [changeTab, options], 66 | ); 67 | 68 | const closeOtherTabs = useCallback((targetTab: MonacoTab) => { 69 | setTabs((prev) => { 70 | const keptTab = prev.find((tab) => tab.id === targetTab.id); 71 | if (!keptTab) return prev; 72 | return [{ ...keptTab, active: true }]; 73 | }); 74 | }, []); 75 | 76 | const closeTabsToLeft = useCallback((targetTab: MonacoTab) => { 77 | setTabs((prev) => { 78 | const targetIndex = prev.findIndex((tab) => tab.id === targetTab.id); 79 | if (targetIndex <= 0) return prev; 80 | 81 | return prev.slice(targetIndex).map((tab) => ({ 82 | ...tab, 83 | active: tab.id === targetTab.id, 84 | })); 85 | }); 86 | }, []); 87 | 88 | const closeTabsToRight = useCallback((targetTab: MonacoTab) => { 89 | setTabs((prev) => { 90 | const targetIndex = prev.findIndex((tab) => tab.id === targetTab.id); 91 | if (targetIndex === -1) return prev; 92 | 93 | return prev.slice(0, targetIndex + 1).map((tab) => ({ 94 | ...tab, 95 | active: tab.id === targetTab.id, 96 | })); 97 | }); 98 | }, []); 99 | 100 | const openTab = useCallback( 101 | (tab: MonacoTab | string) => { 102 | if (typeof tab === "string") { 103 | setTabs((prev) => prev.map((item) => ({ ...item, active: item.id === tab }))); 104 | return; 105 | } 106 | 107 | const hasTab = tabs.some((item) => item.id === tab.id); 108 | if (hasTab) { 109 | setTabs((prev) => { 110 | return prev.map((item) => ({ 111 | ...item, 112 | active: typeof tab === "string" ? item.id === tab : item.id === tab.id, 113 | })); 114 | }); 115 | } else { 116 | const newTab = { id: tab.id, name: tab.name, active: true }; 117 | setTabs((prev) => [...prev.map((item) => ({ ...item, active: false })), newTab]); 118 | changeTab(newTab); 119 | } 120 | }, 121 | [changeTab, tabs], 122 | ); 123 | 124 | useEffect(() => { 125 | const implementationNameMap = implementations.reduce( 126 | (acc, item) => { 127 | acc[item.id] = item.filename; 128 | return acc; 129 | }, 130 | {} as Record, 131 | ); 132 | 133 | setTabs((prev) => { 134 | // sync names 135 | const newTabs = prev 136 | .filter((tab) => defaultTabsIds.has(tab.id) || implementationNameMap[tab.id] !== undefined) 137 | .map((item) => ({ 138 | ...item, 139 | name: implementationNameMap[item.id] ?? item.name, 140 | })); 141 | 142 | // active tab fallback 143 | if (!newTabs.some((tab) => tab.active)) { 144 | const readmeTabIndex = newTabs.findIndex((tab) => tab.id === "README.md"); 145 | if (readmeTabIndex !== -1) { 146 | newTabs[readmeTabIndex] = { ...newTabs[readmeTabIndex], active: true }; 147 | } else if (newTabs.length > 0) { 148 | newTabs[newTabs.length - 1] = { ...newTabs[newTabs.length - 1], active: true }; 149 | } 150 | } 151 | 152 | return newTabs; 153 | }); 154 | }, [implementations]); 155 | 156 | const activeTabId = tabs.find((item) => item.active)?.id ?? null; 157 | 158 | return { 159 | tabs, 160 | activeTabId, 161 | changeTab, 162 | closeTab, 163 | closeOtherTabs, 164 | closeTabsToLeft, 165 | closeTabsToRight, 166 | openTab, 167 | setTabs, 168 | }; 169 | }; 170 | -------------------------------------------------------------------------------- /src/lib/formatters.ts: -------------------------------------------------------------------------------- 1 | export const formatDuration = (ms: number): string => { 2 | if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`; 3 | if (ms >= 1) return `${ms.toFixed(2)}ms`; 4 | if (ms >= 0.001) return `${(ms * 1000).toFixed(2)}µs`; 5 | return `${(ms * 1_000_000).toFixed(2)}ns`; 6 | }; 7 | 8 | export const formatBytes = (bytes: number) => { 9 | if (bytes < 1024) return `${bytes} B`; 10 | const k = 1024; 11 | const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 12 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 13 | return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; 14 | }; 15 | 16 | export const formatCount = (num: number): string => { 17 | return new Intl.NumberFormat("en-US").format(Math.floor(num)); 18 | }; 19 | 20 | export const formatCountShort = (value: number): string => { 21 | if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; 22 | if (value >= 1000) return `${(value / 1000).toFixed(1)}k`; 23 | return value.toString(); 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export const cn = (...inputs: ClassValue[]) => { 5 | return twMerge(clsx(inputs)); 6 | }; 7 | -------------------------------------------------------------------------------- /src/root.tsx: -------------------------------------------------------------------------------- 1 | import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; 2 | import type { Route } from "../src/+types/root"; 3 | import stylesheet from "./global.css?url"; 4 | 5 | export const links: Route.LinksFunction = () => [ 6 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 7 | { 8 | rel: "preconnect", 9 | href: "https://fonts.gstatic.com", 10 | crossOrigin: "anonymous", 11 | }, 12 | { 13 | rel: "stylesheet", 14 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 15 | }, 16 | { 17 | rel: "stylesheet", 18 | href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap", 19 | }, 20 | { rel: "stylesheet", href: stylesheet }, 21 | ]; 22 | 23 | export function Layout({ children }: { children: React.ReactNode }) { 24 | return ( 25 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |