More progress

This commit is contained in:
cte 2025-03-24 01:12:34 -07:00
parent 2012901dae
commit d79c72ec2e
22 changed files with 427 additions and 262 deletions

View file

@ -7,7 +7,7 @@ import { build, filesystem, GluegunPrompt, GluegunToolbox } from "gluegun"
import { runTests } from "@vscode/test-electron"
import { type Language, languages } from "@benchmark/types"
import { type Run, findRun, createRun, getTask, createTask, Task } from "@benchmark/db"
import { type Run, findRun, createRun, finishRun, createTask, Task, getTasks } from "@benchmark/db"
import { __dirname, extensionDevelopmentPath, extensionTestsPath, exercisesPath } from "./paths.js"
import { getExercises } from "./exercises.js"
@ -16,68 +16,56 @@ export const isLanguage = (language: string): language is Language => languages.
const run = async (toolbox: GluegunToolbox) => {
const { config, prompt } = toolbox
const id = config.runId ? Number(config.runId) : undefined
let { language, exercise } = config
if (language === "all") {
await runAll(id)
} else if (exercise === "all") {
await runLanguage({ id, language })
if (![undefined, ...languages, "all"].includes(language)) {
throw new Error(`Language is invalid: ${language}`)
}
if (!["undefined", "string"].includes(typeof exercise)) {
throw new Error(`Exercise is invalid: ${exercise}`)
}
const id = config.runId ? Number(config.runId) : undefined
let run: Run
if (id) {
run = await findRun(id)
} else {
language = language || (await askLanguage(prompt))
exercise = exercise || (await askExercise(prompt, language))
await runLanguageExercise({ id, language, exercise })
run = await createRun({
model: "anthropic/claude-3.7-sonnet",
pid: process.pid,
socketPath: path.resolve(os.tmpdir(), `benchmark-${crypto.randomUUID()}.sock`),
})
if (language === "all") {
for (const language of languages) {
const exercises = getExercises()[language as Language]
await pMap(exercises, async (exercise) => createTask({ runId: run.id, language, exercise }), {
concurrency: 10,
})
}
} else if (exercise === "all") {
const exercises = getExercises()[language as Language]
await pMap(exercises, async (exercise) => createTask({ runId: run.id, language, exercise }), {
concurrency: 10,
})
} else {
language = language || (await askLanguage(prompt))
exercise = exercise || (await askExercise(prompt, language))
await createTask({ runId: run.id, language, exercise })
}
}
}
const runAll = async (id?: number) => {
const run = await findOrCreateRun({ id })
const entries = Object.entries(getExercises()).flatMap(([language, languageExercises]) =>
languageExercises.map((exercise) => ({ language: language as Language, exercise })),
)
const tasks = await pMap(
entries,
async ({ language, exercise }) => findOrCreateTask({ runId: run.id, language, exercise }),
{ concurrency: 10 },
)
const tasks = await getTasks(run.id)
for (const task of tasks) {
await runExercise({ run, task })
}
}
const runLanguage = async ({ id, language }: { id?: number; language: Language }) => {
const run = await findOrCreateRun({ id })
const tasks = await pMap(
getExercises()[language],
async (exercise) => findOrCreateTask({ runId: run.id, language, exercise }),
{ concurrency: 10 },
)
for (const task of tasks) {
await runExercise({ run, task })
}
}
const runLanguageExercise = async ({
id,
language,
exercise,
}: {
id?: number
language: Language
exercise: string
}) => {
if (!getExercises()[language].includes(exercise)) {
throw new Error(`Exercise ${exercise} not found for language ${language}`)
}
const run = await findOrCreateRun({ id })
const task = await findOrCreateTask({ runId: run.id, language, exercise })
return runExercise({ run, task })
const result = await finishRun(run.id)
console.log(result)
}
const runExercise = async ({ run, task }: { run: Run; task: Task }) => {
@ -139,25 +127,6 @@ const askExercise = async (prompt: GluegunPrompt, language: Language) => {
return exercise
}
const findOrCreateRun = async ({ id, model = "anthropic/claude-3.7-sonnet" }: { id?: number; model?: string }) =>
id
? findRun(id)
: createRun({
model,
pid: process.pid,
socketPath: path.resolve(os.tmpdir(), `benchmark-${crypto.randomUUID()}.sock`),
})
const findOrCreateTask = async ({
runId,
language,
exercise,
}: {
runId: number
language: Language
exercise: string
}) => (await getTask({ runId, language, exercise })) || (await createTask({ runId, language, exercise }))
const main = async () => {
const cli = build()
.brand("cli")

View file

@ -26,6 +26,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.0",
"fuzzysort": "^3.1.0",
"lucide-react": "^0.479.0",
"next": "15.2.2",
"next-themes": "^0.4.6",

View file

@ -135,5 +135,6 @@
}
body {
@apply bg-background text-foreground;
scrollbar-color: var(--color-background) transparent; /* Firefox */
}
}

View file

@ -4,7 +4,8 @@ import { LoaderCircle } from "lucide-react"
import * as db from "@benchmark/db"
import { useRunStatus } from "./use-run-status"
import { useRunStatus } from "@/hooks/use-run-status"
import { TaskStatus } from "./task-status"
import { ConnectionStatus } from "./connection-status"

View file

@ -1,33 +0,0 @@
"use server"
import { spawn } from "child_process"
import path from "path"
import os from "os"
import { revalidatePath } from "next/cache"
import * as db from "@benchmark/db"
export async function createRun(data: Omit<db.InsertRun, "socketPath">) {
const socketPath = path.join(os.tmpdir(), `benchmark-${crypto.randomUUID()}.sock`)
const run = await db.createRun({ ...data, socketPath })
revalidatePath("/runs")
try {
const process = spawn(
"pnpm",
["--filter", "@benchmark/cli", "dev", "run", "all", "--runId", run.id.toString()],
{
detached: true,
stdio: "ignore",
},
)
process.unref()
await db.updateRun(run.id, { pid: process.pid })
} catch (error) {
console.error(error)
}
return run
}

View file

@ -1,15 +1,17 @@
"use client"
import { useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { useRouter } from "next/navigation"
import { useForm, FormProvider } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { X, Loader2, Rocket } from "lucide-react"
import { languages } from "@benchmark/types"
import fuzzysort from "fuzzysort"
import { X, Rocket, Check, ChevronsUpDown } from "lucide-react"
import { createRun } from "@/lib/server/runs"
import { createRunSchema as formSchema, type CreateRun as FormValues } from "@/lib/schemas"
import { cn } from "@/lib/utils"
import { useOpenRouterModels } from "@/hooks/use-open-router-models"
import { useExercises } from "@/hooks/use-exercises"
import {
Button,
FormControl,
@ -17,56 +19,70 @@ import {
FormItem,
FormLabel,
FormMessage,
FormDescription,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Textarea,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
MultiSelect,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui"
import { createRun } from "./actions"
import { cn } from "@/lib/utils"
const formSchema = z.object({
model: z.string(),
description: z.string().optional(),
})
type FormValues = z.infer<typeof formSchema>
export function NewRun() {
const router = useRouter()
const { data: models, isLoading, isError } = useOpenRouterModels()
const [isSubmitting, setIsSubmitting] = useState(false)
const [modelSearchValue, setModelSearchValue] = useState("")
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
const modelSearchResultsRef = useRef<Map<string, number>>(new Map())
const modelSearchValueRef = useRef("")
const models = useOpenRouterModels()
const exercises = useExercises()
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
model: "anthropic/claude-3.7-sonnet",
model: "",
description: "",
suite: "full",
exercises: [],
},
})
async function onSubmit(data: FormValues) {
setIsSubmitting(true)
const {
setValue,
watch,
formState: { isSubmitting },
} = form
try {
const run = await createRun(data)
router.push(`/runs/${run.id}`)
} catch (_) {
setIsSubmitting(false)
}
}
const [model, suite] = watch(["model", "suite"])
const [selectedSuite, setSelectedSuite] = useState<"full" | "partial">("full")
const [selectedExercises, setSelectedExercises] = useState<string[]>([])
const selectModel = useCallback(
(model: string) => {
setValue("model", model)
setModelPopoverOpen(false)
},
[setValue],
)
const onSubmit = useCallback(
async (data: FormValues) => {
try {
const { id } = await createRun(data)
router.push(`/runs/${id}`)
} catch (_) {
// Surface error.
}
},
[router],
)
return (
<>
@ -75,63 +91,107 @@ export function NewRun() {
<FormField
control={form.control}
name="model"
render={({ field }) => (
render={() => (
<FormItem>
<FormLabel>OpenRouter Model</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select" />
</SelectTrigger>
</FormControl>
<SelectContent>
{isLoading ? (
<Loader2 className="size-4 m-2 animate-spin" />
) : isError ? (
<div className="m-2 text-center text-destructive">
Failed to load models.
<Popover open={modelPopoverOpen} onOpenChange={setModelPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="input"
role="combobox"
aria-expanded={modelPopoverOpen}
className="flex items-center justify-between">
<div>
{models.data?.find(({ id }) => id === model)?.name || model || "Select"}
</div>
) : (
models?.map((model) => (
<SelectItem key={model.id} value={model.id}>
{model.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]">
<Command
filter={(value, search) => {
if (modelSearchValueRef.current !== search) {
modelSearchValueRef.current = search
modelSearchResultsRef.current.clear()
for (const {
obj: { id },
score,
} of fuzzysort.go(search, models.data || [], {
key: "name",
})) {
modelSearchResultsRef.current.set(id, score)
}
}
return modelSearchResultsRef.current.get(value) ?? 0
}}>
<CommandInput
placeholder="Search"
value={modelSearchValue}
onValueChange={setModelSearchValue}
className="h-9"
/>
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{models.data?.map(({ id, name }) => (
<CommandItem key={id} value={id} onSelect={selectModel}>
{name}
<Check
className={cn(
"ml-auto text-accent group-data-[selected=true]:text-accent-foreground size-4",
id === model ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormItem>
<FormLabel>Exercise Suite</FormLabel>
<Tabs
defaultValue="full"
onValueChange={(value) => setSelectedSuite(value as "full" | "partial")}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="full">Full</TabsTrigger>
<TabsTrigger value="partial">Partial</TabsTrigger>
</TabsList>
</Tabs>
</FormItem>
<FormField
control={form.control}
name="suite"
render={() => (
<FormItem>
<FormLabel>Exercise Suite</FormLabel>
<Tabs
defaultValue="full"
onValueChange={(value) => setValue("suite", value as "full" | "partial")}>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="full">Full</TabsTrigger>
<TabsTrigger value="partial">Partial</TabsTrigger>
</TabsList>
</Tabs>
</FormItem>
)}
/>
{selectedSuite === "partial" && (
<FormItem>
<FormLabel>Exercises</FormLabel>
<MultiSelect
options={languages.map((language) => ({
value: language,
label: language,
}))}
onValueChange={setSelectedExercises}
defaultValue={selectedExercises}
placeholder="Select"
variant="inverted"
maxCount={10}
/>
</FormItem>
{suite === "partial" && (
<FormField
control={form.control}
name="exercises"
render={() => (
<FormItem>
<FormLabel>Exercises</FormLabel>
<MultiSelect
options={exercises.data?.map((path) => ({ value: path, label: path })) || []}
onValueChange={(value) => setValue("exercises", value)}
placeholder="Select"
variant="inverted"
maxCount={4}
/>
</FormItem>
)}
/>
)}
<FormField

View file

@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:opacity-80 active:scale-95 cursor-pointer",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:opacity-80 active:scale-95 cursor-pointer",
{
variants: {
variant: {
@ -17,6 +17,7 @@ const buttonVariants = cva(
secondary: "bg-secondary text-secondary-foreground shadow-xs",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-accent underline-offset-4 hover:underline px-1.5!",
input: "bg-input text-input-foreground active:scale-100 shadow-xs",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",

View file

@ -1,20 +1,13 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { CheckIcon, X, ChevronDown } from "lucide-react"
import fuzzysort from "fuzzysort"
import { Check, X, ChevronsUpDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { Badge } from "./badge"
import { Popover, PopoverContent, PopoverTrigger } from "./popover"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "./command"
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./command"
/**
* Variants for the multi-select component to handle different styles.
@ -127,11 +120,6 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
onValueChange(newSelectedValues)
}
const handleClear = () => {
setSelectedValues([])
onValueChange([])
}
const handleTogglePopover = () => {
setIsPopoverOpen((prev) => !prev)
}
@ -142,16 +130,50 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
onValueChange(newSelectedValues)
}
const toggleAll = () => {
if (selectedValues.length === options.length) {
handleClear()
} else {
const allValues = options.map((option) => option.value)
setSelectedValues(allValues)
onValueChange(allValues)
const searchResultsRef = React.useRef<Map<string, number>>(new Map())
const searchValueRef = React.useRef("")
const onSelectAll = () => {
const values = Array.from(searchResultsRef.current.keys())
if (
selectedValues.length === values.length &&
selectedValues.sort().join(",") === values.sort().join(",")
) {
setSelectedValues([])
onValueChange([])
return
}
setSelectedValues(values)
onValueChange(values)
}
const onFilter = React.useCallback(
(value: string, search: string) => {
if (searchValueRef.current !== search) {
searchValueRef.current = search
searchResultsRef.current.clear()
for (const {
obj: { value },
score,
} of fuzzysort.go(search, options, {
key: "label",
})) {
searchResultsRef.current.set(value, score)
}
}
if (value === "all") {
return searchResultsRef.current.size > 1 ? 0.01 : 0
}
return searchResultsRef.current.get(value) ?? 0
},
[options],
)
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal={modalPopover}>
<PopoverTrigger asChild>
@ -160,13 +182,13 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
{...props}
onClick={handleTogglePopover}
className={cn(
"flex w-full p-1 rounded-sm border min-h-10 h-auto items-center justify-between [&_svg]:pointer-events-auto pl-2 pr-3",
"flex w-full rounded-sm border min-h-9 h-auto items-center justify-between [&_svg]:pointer-events-auto",
"border border-input bg-input hover:opacity-80 cursor-pointer",
className,
)}>
{selectedValues.length > 0 ? (
<div className="flex justify-between items-center w-full">
<div className="flex flex-wrap items-center gap-1">
<div className="flex flex-wrap items-center gap-1 p-1">
{selectedValues.slice(0, maxCount).map((value) => (
<Badge key={value} className={cn(multiSelectVariants({ variant }))}>
<div className="flex items-center gap-1.5">
@ -202,39 +224,28 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
) : (
<div className="flex items-center justify-between w-full mx-auto">
<span className="text-muted-foreground mx-3">{placeholder}</span>
<ChevronDown className="h-4 cursor-pointer text-muted-foreground mx-2" />
<ChevronsUpDown className="opacity-50 size-4 mx-2" />
</div>
)}
</div>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}>
<Command>
<PopoverContent
className="p-0 w-[var(--radix-popover-trigger-width)]"
align="start"
onEscapeKeyDown={() => setIsPopoverOpen(false)}>
<Command filter={onFilter}>
<CommandInput placeholder="Search" onKeyDown={handleInputKeyDown} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem
key="all"
onSelect={toggleAll}
className="flex items-center justify-between">
<span>Select All</span>
<CheckIcon
className={cn(
"text-accent group-data-[selected=true]:text-accent-foreground size-4",
{
"opacity-0": selectedValues.length !== options.length,
},
)}
/>
</CommandItem>
<CommandSeparator />
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => toggleOption(option.value)}
className="flex items-center justify-between">
<span>{option.label}</span>
<CheckIcon
<Check
className={cn(
"text-accent group-data-[selected=true]:text-accent-foreground size-4",
{ "opacity-0": !selectedValues.includes(option.value) },
@ -242,16 +253,13 @@ export const MultiSelect = React.forwardRef<HTMLDivElement, MultiSelectProps>(
/>
</CommandItem>
))}
{selectedValues.length > 0 && (
<>
<CommandSeparator />
<CommandItem
className="flex items-center justify-between"
onSelect={handleClear}>
Select None
</CommandItem>
</>
)}
<CommandItem
key="all"
value="all"
onSelect={onSelectAll}
className="flex items-center justify-between">
<span>Select All</span>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>

View file

@ -2,7 +2,7 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
@ -38,7 +38,7 @@ function SelectTrigger({
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
<ChevronDown className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
@ -99,7 +99,7 @@ function SelectItem({ className, children, ...props }: React.ComponentProps<type
{...props}>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="text-accent group-focus:text-accent-foreground size-4" />
<Check className="text-accent group-focus:text-accent-foreground size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
@ -123,7 +123,7 @@ function SelectScrollUpButton({ className, ...props }: React.ComponentProps<type
data-slot="select-scroll-up-button"
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronUpIcon className="size-4" />
<ChevronUp className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
@ -137,7 +137,7 @@ function SelectScrollDownButton({
data-slot="select-scroll-down-button"
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronDownIcon className="size-4" />
<ChevronDown className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}

View file

@ -0,0 +1,5 @@
import { useQuery } from "@tanstack/react-query"
import { getExercises } from "@/lib/server/exercises"
export const useExercises = () => useQuery({ queryKey: ["exercises"], queryFn: getExercises })

View file

@ -1,12 +1,11 @@
import { useState, useCallback } from "react"
import { useQuery, keepPreviousData } from "@tanstack/react-query"
import { useEventSource } from "@/hooks/use-event-source"
import { Run } from "@benchmark/db"
import { getTasks } from "./actions"
import { ipcServerMessageSchema } from "./schemas"
import { getTasks } from "@/lib/server/tasks"
import { ipcServerMessageSchema } from "@/lib/schemas"
import { useEventSource } from "@/hooks/use-event-source"
export const useRunStatus = (run: Run) => {
const [clientId, setClientId] = useState<string>()

View file

@ -1,2 +1,2 @@
export { formatCurrency } from "./formatCurrency"
export { formatDuration } from "./formatDuration"
export { formatCurrency } from "./format-currency"
export { formatDuration } from "./format-duration"

View file

@ -1,5 +1,26 @@
import { z } from "zod"
/**
* CreateRun
*/
export const createRunSchema = z
.object({
model: z.string().min(1),
description: z.string().optional(),
suite: z.enum(["full", "partial"]),
exercises: z.array(z.string()).optional(),
})
.refine((data) => data.suite === "full" || (data.exercises || []).length > 0, {
message: "Exercises are required for partial suite.",
})
export type CreateRun = z.infer<typeof createRunSchema>
/**
* IpcServerMessage
*/
export const ipcServerMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("Ack"),
@ -18,3 +39,5 @@ export const ipcServerMessageSchema = z.discriminatedUnion("type", [
}),
}),
])
export type IpcServerMessage = z.infer<typeof ipcServerMessageSchema>

View file

@ -0,0 +1,35 @@
"use server"
import * as fs from "fs/promises"
import * as path from "path"
import { fileURLToPath } from "url"
import { languages } from "@benchmark/types"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const listDirectories = async (relativePath: string) => {
try {
const targetPath = path.resolve(__dirname, relativePath)
const entries = await fs.readdir(targetPath, { withFileTypes: true })
return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name)
} catch (error) {
console.error(`Error listing directories at ${relativePath}:`, error)
return []
}
}
// __dirname = <repo>/benchmark/apps/web/src/lib/server
const EXERCISES_BASE_PATH = path.resolve(__dirname, "../../../../../../../exercises")
export const getExercises = async () => {
const result = await Promise.all(
languages.map(async (language) => {
const languagePath = path.join(EXERCISES_BASE_PATH, language)
const exercises = await listDirectories(languagePath)
return exercises.map((exercise) => `${language}/${exercise}`)
}),
)
return result.flat()
}

View file

@ -0,0 +1,51 @@
"use server"
import { spawn } from "child_process"
import path from "path"
import os from "os"
import { revalidatePath } from "next/cache"
import { Language } from "@benchmark/types"
import * as db from "@benchmark/db"
import { CreateRun } from "@/lib/schemas"
export async function createRun({ suite, exercises = [], ...values }: CreateRun) {
const run = await db.createRun({
...values,
socketPath: path.join(os.tmpdir(), `benchmark-${crypto.randomUUID()}.sock`),
})
if (suite === "partial") {
for (const path of exercises) {
const [language, exercise] = path.split("/")
if (!language || !exercise) {
throw new Error("Invalid exercise path: " + path)
}
await db.createTask({ ...values, runId: run.id, language: language as Language, exercise })
}
}
revalidatePath("/runs")
try {
const process = spawn(
"pnpm",
["--filter", "@benchmark/cli", "dev", "run", "all", "--runId", run.id.toString()],
{
detached: true,
stdio: "ignore",
},
)
process.unref()
await db.updateRun(run.id, { pid: process.pid })
} catch (error) {
console.error(error)
}
return run
}

View file

@ -7,8 +7,8 @@
"check-types": "turbo check-types --log-order grouped --output-logs new-only",
"format": "turbo format --log-order grouped --output-logs new-only",
"build": "turbo build --log-order grouped --output-logs new-only",
"web": "turbo dev --filter @benchmark/web --log-order grouped --output-logs new-only",
"cli": "turbo dev --filter @benchmark/cli --ui tui -- run",
"web": "turbo dev --filter @benchmark/web --output-logs new-only --ui tui",
"cli": "turbo dev --filter @benchmark/cli --output-logs new-only --ui tui -- run",
"drizzle:studio": "pnpm --filter @benchmark/db db:studio",
"docker:build": "docker build -f Dockerfile -t roo-code-benchmark ..",
"docker:run": "touch /tmp/benchmarks.db && docker run -d -it -p 3000:3000 -v /tmp/benchmarks.db:/tmp/benchmarks.db roo-code-benchmark",

View file

@ -1,9 +1,10 @@
import { desc, eq } from "drizzle-orm"
import { desc, eq, sum } from "drizzle-orm"
import { RecordNotFoundError, RecordNotCreatedError } from "./errors.js"
import type { InsertRun, UpdateRun } from "../schema.js"
import { insertRunSchema, schema } from "../schema.js"
import { db } from "../db.js"
import { createTaskMetrics } from "./taskMetrics.js"
const table = schema.runs
@ -47,3 +48,35 @@ export const updateRun = async (id: number, values: UpdateRun) => {
}
export const getRuns = async () => db.query.runs.findMany({ orderBy: desc(table.id), with: { taskMetrics: true } })
export const finishRun = async (runId: number) => {
const [values] = await db
.select({
tokensIn: sum(schema.taskMetrics.tokensIn).mapWith(Number),
tokensOut: sum(schema.taskMetrics.tokensOut).mapWith(Number),
tokensContext: sum(schema.taskMetrics.tokensContext).mapWith(Number),
cacheWrites: sum(schema.taskMetrics.cacheWrites).mapWith(Number),
cacheReads: sum(schema.taskMetrics.cacheReads).mapWith(Number),
cost: sum(schema.taskMetrics.cost).mapWith(Number),
duration: sum(schema.taskMetrics.duration).mapWith(Number),
})
.from(schema.taskMetrics)
.innerJoin(schema.tasks, eq(schema.taskMetrics.id, schema.tasks.taskMetricsId))
.innerJoin(schema.runs, eq(schema.tasks.runId, schema.runs.id))
.where(eq(schema.runs.id, runId))
if (!values) {
throw new RecordNotFoundError()
}
const taskMetrics = await createTaskMetrics(values)
await updateRun(runId, { taskMetricsId: taskMetrics.id })
const run = await db.query.runs.findFirst({ where: eq(table.id, runId), with: { taskMetrics: true } })
if (!run) {
throw new RecordNotFoundError()
}
return run
}

View file

@ -15,14 +15,16 @@ export async function run() {
* Validate environment variables.
*/
const tid = process.env.TASK_ID ? parseInt(process.env.TASK_ID) : undefined
const taskId = process.env.TASK_ID ? parseInt(process.env.TASK_ID) : undefined
const promptPath = process.env.PROMPT_PATH
const workspacePath = process.env.WORKSPACE_PATH
const openRouterApiKey = process.env.OPENROUTER_API_KEY
const openRouterModelId = process.env.OPENROUTER_MODEL_ID
if (!tid || !promptPath || !workspacePath || !openRouterApiKey || !openRouterModelId) {
throw new Error("ENV not configured.")
if (!taskId || !promptPath || !workspacePath || !openRouterApiKey || !openRouterModelId) {
throw new Error(
`ENV not configured. ${JSON.stringify({ taskId, promptPath, workspacePath, openRouterApiKey, openRouterModelId })}`,
)
}
const prompt = await fs.readFile(promptPath, "utf-8")
@ -31,7 +33,7 @@ export async function run() {
* Fetch and update the task.
*/
let task = await findTask(tid)
let task = await findTask(taskId)
task = await updateTask(task.id, { startedAt: new Date() })
const run = await findRun(task.runId)

View file

@ -114,6 +114,9 @@ importers:
cmdk:
specifier: 1.0.0
version: 1.0.0(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
fuzzysort:
specifier: ^3.1.0
version: 3.1.0
lucide-react:
specifier: ^0.479.0
version: 0.479.0(react@19.0.0)
@ -2689,6 +2692,9 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
fuzzysort@3.1.0:
resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==}
gel@2.0.1:
resolution: {integrity: sha512-gfem3IGvqKqXwEq7XseBogyaRwGsQGuE7Cw/yQsjLGdgiyqX92G1xENPCE0ltunPGcsJIa6XBOTx/PK169mOqw==}
engines: {node: '>= 18.0.0'}
@ -3061,6 +3067,7 @@ packages:
libsql@0.4.7:
resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==}
cpu: [x64, arm64, wasm32]
os: [darwin, linux, win32]
lie@3.3.0:
@ -6483,6 +6490,8 @@ snapshots:
functions-have-names@1.2.3: {}
fuzzysort@3.1.0: {}
gel@2.0.1:
dependencies:
'@petamoriken/float16': 3.9.2