-
Notifications
You must be signed in to change notification settings - Fork 9
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
Changes from 4 commits
9951329
a0f4249
d632898
cc54408
978e4da
081b5dc
70c984a
736a0a1
6b7932f
8fb422c
ea74b6e
75629e8
fa98afb
22bf80b
47808a6
24a2411
3159190
c028d6f
598afd7
e753bbb
3a7e818
8dabf89
52b3c29
21a7f5f
4ec7dca
89b3b17
cd7361a
d9087b4
c0560cc
5328d6e
b650f95
708f018
5c8a745
280ef10
3b2360c
9e63ea6
e75be05
e4f9e28
dc101ae
387cd3e
c8e7225
a43694c
04fcb07
3c36632
7a3532c
5f6cf3c
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: sorted[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) { | ||
|
||
| 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 = () => { | ||
|
||
| 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
✅ Addressed in
d9087b4