-
Notifications
You must be signed in to change notification settings - Fork 9
WIP: Optimize SurrealDB adapter #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9951329
a0f4249
d632898
cc54408
978e4da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| const MAX_ITER = 10; | ||
| const MAX_DURATION = 60_000; | ||
|
|
||
| type BenchFn = (cb: () => Promise<void>) => void; | ||
|
|
||
| type TimeitFn = (name: string, cb: () => Promise<void>, opt?: BenchOptions) => void; | ||
|
|
||
| interface BenchOptions { | ||
| maxIter?: number; | ||
| maxDuration?: number; | ||
| } | ||
|
|
||
| export const bench = async ( | ||
| cb: (params: { beforeAll: BenchFn; afterAll: BenchFn, time: TimeitFn }) => Promise<void>, | ||
| opt?: BenchOptions | ||
| ) => { | ||
| const { maxIter = MAX_ITER, maxDuration = MAX_DURATION } = opt ?? {}; | ||
| const beforePromises: (() => Promise<void>)[] = []; | ||
| const afterPromises: (() => Promise<void>)[] = []; | ||
| const beforeAll = (cb: () => Promise<void>) => { | ||
| beforePromises.push(cb); | ||
| }; | ||
| const afterAll = (cb: () => Promise<void>) => { | ||
| afterPromises.push(cb); | ||
| }; | ||
| const variants: { name: string, cb: () => Promise<void>, durations: number[], totalDuration: number, maxIter: number, maxDuration: number }[] = []; | ||
| const time = (name: string, cb: () => Promise<void>, opt?: BenchOptions) => { | ||
| variants.push({ | ||
| name, | ||
| cb, | ||
| durations: [], | ||
| totalDuration: 0, | ||
| maxIter: opt?.maxIter ?? maxIter, | ||
| maxDuration: opt?.maxDuration ?? maxDuration, | ||
| }); | ||
| }; | ||
|
|
||
| await cb({ beforeAll, afterAll, time }); | ||
|
|
||
| await Promise.all(beforePromises.map(async (cb) => cb())); | ||
|
|
||
| for (const variant of variants) { | ||
| console.log(`Running "${variant.name}"...`); | ||
| while (variant.durations.length < variant.maxIter && variant.totalDuration < variant.maxDuration) { | ||
| try { | ||
| const start = performance.now(); | ||
| await variant.cb(); | ||
| const duration = performance.now() - start; | ||
| variant.durations.push(duration); | ||
| variant.totalDuration += duration; | ||
| } catch (error) { | ||
| console.error(`Error running "${variant.name}":`, error); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| await Promise.all(afterPromises.map((cb) => cb())); | ||
|
|
||
| const summary = summarize(variants); | ||
| console.log(format(summary)); | ||
| }; | ||
|
|
||
| interface Summary { | ||
| name: string; | ||
| iter: number; | ||
| first: number; | ||
| min: number; | ||
| max: number; | ||
| mean: number; | ||
| median: number; | ||
| p90: number; | ||
| p95: number; | ||
| } | ||
|
|
||
| const summarize = (variants: { name: string, durations: number[] }[]): Summary[] => { | ||
| return variants.map((variant) => { | ||
| const sorted = [...variant.durations].sort((a, b) => a - b); | ||
| const total = sorted.reduce((a, b) => a + b, 0); | ||
| const count = sorted.length; | ||
|
|
||
| const min = sorted[0] || 0; | ||
| const max = sorted[count - 1] || 0; | ||
| const mean = count > 0 ? total / count : 0; | ||
| const median = | ||
| count === 0 | ||
| ? 0 | ||
| : count % 2 === 0 | ||
| ? (sorted[count / 2 - 1] + sorted[count / 2]) / 2 | ||
| : sorted[Math.floor(count / 2)]; | ||
|
|
||
| const p90 = count === 0 ? 0 : sorted[Math.floor(count * 0.9)]; | ||
| const p95 = count === 0 ? 0 : sorted[Math.floor(count * 0.95)]; | ||
|
|
||
| return { | ||
| name: variant.name, | ||
| iter: variant.durations.length, | ||
| first: variant.durations[0] ?? 0, | ||
| min, | ||
| max, | ||
| mean, | ||
| median, | ||
| p90, | ||
| p95, | ||
| }; | ||
| }); | ||
| }; | ||
|
|
||
| const format = (summary: Summary[]): string => { | ||
| const headers = ["name", "iter", "first", "min", "max", "mean", "median"] as const; | ||
|
|
||
| const rows = summary.map((s) => ({ | ||
| name: s.name.slice(0, 50), | ||
| iter: s.iter.toString(), | ||
| first: s.first.toFixed(4), | ||
| min: s.min.toFixed(4), | ||
| max: s.max.toFixed(4), | ||
| mean: s.mean.toFixed(4), | ||
| median: s.median.toFixed(4), | ||
| })); | ||
|
|
||
| const allRows = [ | ||
| { name: "name", iter: "iter", first: "first", min: "min", max: "max", mean: "mean", median: "median" }, | ||
| ...rows, | ||
| ]; | ||
|
|
||
| const widths = headers.map((h) => | ||
| Math.max(...allRows.map((r) => r[h].length)) | ||
| ); | ||
|
|
||
| return allRows | ||
| .map((row) => | ||
| headers.map((h, i) => row[h].padEnd(widths[i])).join(" | ") | ||
| ) | ||
| .join("\n"); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| #!/usr/bin/env bash | ||
|
|
||
| CONTAINER_NAME=borm_bench_v2 | ||
| USER=borm_bench | ||
| PASSWORD=borm_bench | ||
| NAMESPACE=borm_bench | ||
| DATABASE=borm_bench | ||
| SCHEMA_FILE="./benches/schema.v2.surql" | ||
|
|
||
| # Function to clean up the container | ||
| cleanup() { | ||
| echo "Stopping and removing container..." | ||
| docker stop ${CONTAINER_NAME} >/dev/null 2>&1 | ||
| docker rm ${CONTAINER_NAME} >/dev/null 2>&1 | ||
| exit ${EXIT_CODE:-1} # Default to 1 if EXIT_CODE is unset (e.g. early crash) | ||
| } | ||
|
|
||
| # Set up trap to call cleanup function on script exit | ||
| trap cleanup EXIT INT TERM | ||
|
|
||
| # Function to parse command line arguments | ||
| parse_args() { | ||
| VITEST_ARGS=() | ||
| for arg in "$@" | ||
| do | ||
| case $arg in | ||
| -link=*) | ||
| # We'll ignore this parameter now | ||
| ;; | ||
| *) | ||
| VITEST_ARGS+=("$arg") | ||
| ;; | ||
| esac | ||
| done | ||
| } | ||
|
|
||
| # Parse the command line arguments | ||
| parse_args "$@" | ||
|
|
||
| # Start the container | ||
| if ! docker run \ | ||
| --rm \ | ||
| --detach \ | ||
| --name $CONTAINER_NAME \ | ||
| --user root \ | ||
| -p 8002:8002 \ | ||
| --pull always \ | ||
| surrealdb/surrealdb:v2.3.7 \ | ||
| start \ | ||
| --allow-all \ | ||
| -u $USER \ | ||
| -p $PASSWORD \ | ||
| --bind 0.0.0.0:8002 \ | ||
| rocksdb:///data/blitz.db; then | ||
| echo "Failed to start SurrealDB container" | ||
| exit 1 | ||
| fi | ||
|
|
||
| until [ "`docker inspect -f {{.State.Running}} $CONTAINER_NAME`" == "true" ]; do | ||
| sleep 0.1; | ||
| done; | ||
|
|
||
| # Wait for SurrealDB to be ready | ||
| echo "Waiting for SurrealDB to be ready..." | ||
| until docker exec $CONTAINER_NAME ./surreal is-ready --endpoint http://localhost:8002 2>/dev/null; do | ||
| sleep 0.5; | ||
| done; | ||
| echo "SurrealDB is ready!" | ||
|
|
||
| # Setup surrealdb database: create the namespace, database, and user dynamically | ||
| docker exec -i $CONTAINER_NAME ./surreal sql -u $USER -p $PASSWORD --endpoint http://localhost:8002 <<EOF | ||
| DEFINE NAMESPACE $NAMESPACE; | ||
| USE NS $NAMESPACE; | ||
| DEFINE DATABASE $DATABASE; | ||
| DEFINE USER $USER ON NAMESPACE PASSWORD '$PASSWORD' ROLES OWNER; | ||
| EOF | ||
|
|
||
| # Create the schema | ||
| docker cp $SCHEMA_FILE $CONTAINER_NAME:/tmp/schema.surql | ||
| docker exec -i $CONTAINER_NAME ./surreal import -u $USER -p $PASSWORD --namespace $NAMESPACE --database $DATABASE --endpoint http://localhost:8002 /tmp/schema.surql | ||
|
|
||
| # Always stop container, but exit with 1 when tests are failing | ||
| # if CONTAINER_NAME=${CONTAINER_NAME} npx vitest bench "${VITEST_ARGS[@]}"; then | ||
| if CONTAINER_NAME=${CONTAINER_NAME} tsx benches/v2-2.bench.ts; then | ||
| echo "Bench passed. Container ${CONTAINER_NAME} is still running." | ||
| EXIT_CODE=0 | ||
| else | ||
| echo "Bench failed. Container ${CONTAINER_NAME} is still running." | ||
| EXIT_CODE=1 | ||
| fi |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| export interface Base { | ||
| id: string; | ||
| string_1: string; | ||
| number_1: number; | ||
| boolean_1: boolean; | ||
| datetime_1: Date; | ||
| } | ||
|
|
||
| export interface A extends Base { | ||
| one: B['id']; | ||
| few: B['id'][]; | ||
| many: B['id'][]; | ||
| } | ||
|
|
||
| export type B = Base; | ||
|
|
||
| export const generateData = (params: { | ||
| records: number; | ||
| few: { min: number; max: number }; | ||
| many: { min: number; max: number }; | ||
| }): { a: A[]; b: B[]; } => { | ||
| const a: A[] = []; | ||
| const b: B[] = []; | ||
|
|
||
| const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; | ||
| const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | ||
| const randomString = (min: number, max: number) => { | ||
| const length = randomInt(min, max); | ||
| let result = ''; | ||
| for (let i = 0; i < length; i++) { | ||
| result += chars.charAt(Math.floor(Math.random() * chars.length)); | ||
| } | ||
| return result; | ||
| }; | ||
| const randomBoolean = () => Math.random() < 0.5; | ||
| const randomDate = () => { | ||
| const start = new Date('2020-01-01').getTime(); | ||
| const end = new Date('2026-01-01').getTime(); | ||
| return new Date(start + Math.random() * (end - start)); | ||
| }; | ||
|
|
||
| const generateBase = (): Base => ({ | ||
| id: uid(), | ||
| string_1: randomString(10, 20), | ||
| number_1: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER), | ||
| boolean_1: randomBoolean(), | ||
| datetime_1: randomDate(), | ||
| }); | ||
|
|
||
| for (let i = 0; i < params.records; i++) { | ||
| b.push(generateBase()); | ||
| } | ||
|
|
||
| for (let i = 0; i < params.records; i++) { | ||
| const fewLength = randomInt(params.few.min, params.few.max); | ||
| const manyLength = randomInt(params.many.min, params.many.max); | ||
| const fewSet = new Set<string>(); | ||
| const manySet = new Set<string>(); | ||
|
|
||
| while (fewSet.size < fewLength) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Potential infinite loop: if Prompt for AI agents |
||
| fewSet.add(b[randomInt(0, b.length - 1)].id); | ||
| } | ||
|
|
||
| while (manySet.size < manyLength) { | ||
| manySet.add(b[randomInt(0, b.length - 1)].id); | ||
| } | ||
|
|
||
| a.push({ | ||
| ...generateBase(), | ||
| one: b[i].id, | ||
| few: Array.from(fewSet), | ||
| many: Array.from(manySet), | ||
| }); | ||
| } | ||
|
|
||
| return { a, b }; | ||
| } | ||
|
|
||
| const uid = () => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Rule violated: Ensure all TypeScript code adheres to ECMAScript 2025 standards Use Prompt for AI agents |
||
| const firstChar = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; | ||
| const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | ||
| let result = firstChar.charAt(Math.floor(Math.random() * firstChar.length)); | ||
| for (let i = 0; i < 15; i++) { | ||
| result += chars.charAt(Math.floor(Math.random() * chars.length)); | ||
| } | ||
| return result; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Rule violated: Ensure all TypeScript code adheres to ECMAScript 2025 standards
String concatenation in loops is a less performant older pattern. Consider using
Array.from()with.join()for more idiomatic modern JavaScript.Prompt for AI agents