diff --git a/.github/workflows/wasm-benhmarks.yml b/.github/workflows/wasm-benhmarks.yml new file mode 100644 index 000000000000..036064d92cf6 --- /dev/null +++ b/.github/workflows/wasm-benhmarks.yml @@ -0,0 +1,107 @@ +name: Wasm perf +on: + pull_request: + paths-ignore: + - ".github/**" + - "!.github/workflows/wasm-benchmarks.yml" + - ".buildkite/**" + - "*.md" + - "LICENSE" + - "CODEOWNERS" + - "renovate.json" + +jobs: + benchmarks: + runs-on: ubuntu-latest + env: # Set environment variables for the whole job + PROFILE: release + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + + - name: "Setup Node.js" + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node_version }} + + - name: "Setup pnpm" + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: "Login to Docker Hub" + uses: docker/login-action@v3 + continue-on-error: true + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + if: "${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }}" + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - uses: cachix/install-nix-action@v24 + + - name: Setup benchmark + run: make setup-pg-bench + + - name: Run benchmarks + id: bench + run: make run-bench | tee out.txt + + - name: Read benchmark results + id: read_results + run: | + { + echo 'bench_output<> "$GITHUB_OUTPUT" + + - name: Summarize results + id: summarize_results + run: | + input="${{ steps.read_results.outputs.bench_output }}" + regressed=$(echo "$input" | grep "slower than Web Assembly: Latest" | cut -f1 -d'x' | awk '$1 > 1.01' | wc -l ) + if [ "$regressed" -gt 0 ]; then + message="🚨 WASM query-engine: $regressed benchmark(s) have regressed at least 1%" + status=failed + else + message="✅ WASM query-engine: no benchmarks have regressed" + status=passed + fi + echo "summary=$message" >> $GITHUB_OUTPUT + echo "status=$status" >> $GITHUB_OUTPUT + + - name: Find past report comment + uses: peter-evans/find-comment@v2 + id: findReportComment + with: + issue-number: ${{ github.event.pull_request.number }} + body-includes: "" + + - name: Create or update report + uses: peter-evans/create-or-update-comment@v3 + with: + comment-id: ${{ steps.findReportComment.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | + + #### ${{ steps.summarize_results.outputs.summary }} + +
+ Full benchmark report + + ``` + ${{ steps.read_results.outputs.bench_output }} + ``` +
+ + After changes in ${{ github.event.pull_request.head.sha }} + edit-mode: replace + + - name: Fail workflow if regression detected + if: steps.summarize_results.outputs.status == 'failed' + run: | + echo "Workflow failed due to benchmark regression." + exit 1 diff --git a/Makefile b/Makefile index 0a2dcfbf832a..10b67ed8bc57 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,8 @@ LIBRARY_EXT := $(shell \ (*) echo "so" ;; \ esac) +PROFILE ?= dev + default: build ################### @@ -88,14 +90,14 @@ start-sqlite: dev-sqlite: cp $(CONFIG_PATH)/sqlite $(CONFIG_FILE) -dev-libsql-js: build-qe-napi build-connector-kit-js +dev-libsql-js: build-qe-napi build-driver-adapters-kit cp $(CONFIG_PATH)/libsql-js $(CONFIG_FILE) test-libsql-js: dev-libsql-js test-qe-st test-driver-adapter-libsql: test-libsql-js -dev-libsql-wasm: build-qe-wasm build-connector-kit-js +dev-libsql-wasm: build-qe-wasm build-driver-adapters-kit cp $(CONFIG_PATH)/libsql-wasm $(CONFIG_FILE) test-libsql-wasm: dev-libsql-wasm test-qe-st @@ -133,12 +135,12 @@ dev-postgres13: start-postgres13 start-pg-js: start-postgres13 -dev-pg-js: start-pg-js build-qe-napi build-connector-kit-js +dev-pg-js: start-pg-js build-qe-napi build-driver-adapters-kit cp $(CONFIG_PATH)/pg-js $(CONFIG_FILE) test-pg-js: dev-pg-js test-qe-st -dev-pg-wasm: start-pg-js build-qe-wasm build-connector-kit-js +dev-pg-wasm: start-pg-js build-qe-wasm build-driver-adapters-kit cp $(CONFIG_PATH)/pg-wasm $(CONFIG_FILE) test-pg-wasm: dev-pg-wasm test-qe-st @@ -146,15 +148,26 @@ test-pg-wasm: dev-pg-wasm test-qe-st test-driver-adapter-pg: test-pg-js test-driver-adapter-pg-wasm: test-pg-wasm +start-pg-bench: + docker compose -f query-engine/driver-adapters/executor/bench/docker-compose.yml up --wait -d --remove-orphans postgres + +setup-pg-bench: start-pg-bench build-qe-napi build-qe-wasm build-driver-adapters-kit + +run-bench: + DATABASE_URL="postgresql://postgres:postgres@localhost:5432/bench?schema=imdb_bench&sslmode=disable" \ + node --experimental-wasm-modules query-engine/driver-adapters/executor/dist/bench.mjs + +bench-pg-js: setup-pg-bench run-bench + start-neon-js: docker compose -f docker-compose.yml up --wait -d --remove-orphans neon-proxy -dev-neon-js: start-neon-js build-qe-napi build-connector-kit-js +dev-neon-js: start-neon-js build-qe-napi build-driver-adapters-kit cp $(CONFIG_PATH)/neon-js $(CONFIG_FILE) test-neon-js: dev-neon-js test-qe-st -dev-neon-wasm: start-neon-js build-qe-wasm build-connector-kit-js +dev-neon-wasm: start-neon-js build-qe-wasm build-driver-adapters-kit cp $(CONFIG_PATH)/neon-wasm $(CONFIG_FILE) test-neon-wasm: dev-neon-wasm test-qe-st @@ -293,12 +306,12 @@ dev-vitess_8_0: start-vitess_8_0 start-planetscale-js: docker compose -f docker-compose.yml up -d --remove-orphans planetscale-proxy -dev-planetscale-js: start-planetscale-js build-qe-napi build-connector-kit-js +dev-planetscale-js: start-planetscale-js build-qe-napi build-driver-adapters-kit cp $(CONFIG_PATH)/planetscale-js $(CONFIG_FILE) test-planetscale-js: dev-planetscale-js test-qe-st -dev-planetscale-wasm: start-planetscale-js build-qe-wasm build-connector-kit-js +dev-planetscale-wasm: start-planetscale-js build-qe-wasm build-driver-adapters-kit cp $(CONFIG_PATH)/planetscale-wasm $(CONFIG_FILE) test-planetscale-wasm: dev-planetscale-wasm test-qe-st @@ -311,7 +324,7 @@ test-driver-adapter-planetscale-wasm: test-planetscale-wasm ###################### build-qe-napi: - cargo build --package query-engine-node-api + cargo build --package query-engine-node-api --profile $(PROFILE) build-qe-wasm: ifndef $(NIX) @@ -322,7 +335,7 @@ else cd query-engine/query-engine-wasm && ./build.sh endif -build-connector-kit-js: build-driver-adapters +build-driver-adapters-kit: build-driver-adapters cd query-engine/driver-adapters && pnpm i && pnpm build build-driver-adapters: ensure-prisma-present @@ -384,7 +397,7 @@ otel: # Build the debug version of Query Engine Node-API library ready to be consumed by Node.js .PHONY: qe-node-api -qe-node-api: build target/debug/libquery_engine.node +qe-node-api: build target/debug/libquery_engine.node --profile=$(PROFILE) %.node: %.$(LIBRARY_EXT) # Remove the file first to work around a macOS bug: https://openradar.appspot.com/FB8914243 diff --git a/nix/all-engines.nix b/nix/all-engines.nix index b2df9630f922..65f65ab6dd1f 100644 --- a/nix/all-engines.nix +++ b/nix/all-engines.nix @@ -115,6 +115,8 @@ in }) { profile = "release"; }; + + packages.query-engine-wasm = lib.makeOverridable ({ profile }: stdenv.mkDerivation { name = "query-engine-wasm"; diff --git a/query-engine/connector-test-kit-rs/README.md b/query-engine/connector-test-kit-rs/README.md index 993f636e0d28..650f8f2d4dd0 100644 --- a/query-engine/connector-test-kit-rs/README.md +++ b/query-engine/connector-test-kit-rs/README.md @@ -89,7 +89,7 @@ To run tests through a driver adapters, you should also configure the following Example: ```shell -export EXTERNAL_TEST_EXECUTOR="$WORKSPACE_ROOT/query-engine/driver-adapters/connector-test-kit-executor/script/start_node.sh" +export EXTERNAL_TEST_EXECUTOR="$WORKSPACE_ROOT/query-engine/driver-adapters/executor/script/testd.sh" export DRIVER_ADAPTER=neon export ENGINE=wasm export DRIVER_ADAPTER_CONFIG ='{ "proxyUrl": "127.0.0.1:5488/v1" }' diff --git a/query-engine/connector-test-kit-rs/query-tests-setup/src/config.rs b/query-engine/connector-test-kit-rs/query-tests-setup/src/config.rs index 49ca5440a25c..f1248e3c4d94 100644 --- a/query-engine/connector-test-kit-rs/query-tests-setup/src/config.rs +++ b/query-engine/connector-test-kit-rs/query-tests-setup/src/config.rs @@ -183,8 +183,7 @@ impl TestConfig { } pub fn external_test_executor_path(&self) -> Option { - const DEFAULT_TEST_EXECUTOR: &str = - "query-engine/driver-adapters/connector-test-kit-executor/script/start_node.sh"; + const DEFAULT_TEST_EXECUTOR: &str = "query-engine/driver-adapters/executor/script/testd.sh"; self.external_test_executor .as_ref() .and_then(|_| { diff --git a/query-engine/driver-adapters/connector-test-kit-executor/script/start_node.sh b/query-engine/driver-adapters/connector-test-kit-executor/script/start_node.sh deleted file mode 100755 index 000f3bd1d45c..000000000000 --- a/query-engine/driver-adapters/connector-test-kit-executor/script/start_node.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -node "$(dirname "${BASH_SOURCE[0]}")/../dist/index.mjs" \ No newline at end of file diff --git a/query-engine/driver-adapters/connector-test-kit-executor/src/qe.ts b/query-engine/driver-adapters/connector-test-kit-executor/src/qe.ts deleted file mode 100644 index 20e9a4917fb5..000000000000 --- a/query-engine/driver-adapters/connector-test-kit-executor/src/qe.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { ErrorCapturingDriverAdapter } from '@prisma/driver-adapter-utils' -import * as napi from './engines/Library' -import * as os from 'node:os' -import * as path from 'node:path' -import { fileURLToPath } from 'node:url' - -const dirname = path.dirname(fileURLToPath(import.meta.url)) - -export interface QueryEngine { - connect(trace: string): Promise - disconnect(trace: string): Promise; - query(body: string, trace: string, tx_id?: string): Promise; - startTransaction(input: string, trace: string): Promise; - commitTransaction(tx_id: string, trace: string): Promise; - rollbackTransaction(tx_id: string, trace: string): Promise; -} - -export type QueryLogCallback = (log: string) => void - - -export async function initQueryEngine(adapter: ErrorCapturingDriverAdapter, datamodel: string, queryLogCallback: QueryLogCallback, debug: (...args: any[]) => void): QueryEngine { - - const queryEngineOptions = { - datamodel, - configDir: '.', - engineProtocol: 'json' as const, - logLevel: process.env["RUST_LOG"] ?? 'info' as any, - logQueries: true, - env: process.env, - ignoreEnvVarErrors: false, - } - - - const logCallback = (event: any) => { - const parsed = JSON.parse(event) - if (parsed.is_query) { - queryLogCallback(parsed.query) - } - debug(parsed) - } - - const engineFromEnv = process.env.EXTERNAL_TEST_EXECUTOR ?? 'Napi' - if (engineFromEnv === 'Wasm') { - const { WasmQueryEngine } = await import('./wasm') - return new WasmQueryEngine(queryEngineOptions, logCallback, adapter) - } else if (engineFromEnv === 'Napi') { - const { QueryEngine } = loadNapiEngine() - return new QueryEngine(queryEngineOptions, logCallback, adapter) - } else { - throw new TypeError(`Invalid EXTERNAL_TEST_EXECUTOR value: ${engineFromEnv}. Expected Napi or Wasm`) - } - - -} - -function loadNapiEngine(): napi.Library { - // I assume nobody will run this on Windows ¯\_(ツ)_/¯ - const libExt = os.platform() === 'darwin' ? 'dylib' : 'so' - - const libQueryEnginePath = path.join(dirname, `../../../../target/debug/libquery_engine.${libExt}`) - - const libqueryEngine = { exports: {} as unknown as napi.Library } - // @ts-ignore - process.dlopen(libqueryEngine, libQueryEnginePath) - - return libqueryEngine.exports -} \ No newline at end of file diff --git a/query-engine/driver-adapters/connector-test-kit-executor/.gitignore b/query-engine/driver-adapters/executor/.gitignore similarity index 100% rename from query-engine/driver-adapters/connector-test-kit-executor/.gitignore rename to query-engine/driver-adapters/executor/.gitignore diff --git a/query-engine/driver-adapters/executor/bench/Dockerfile b/query-engine/driver-adapters/executor/bench/Dockerfile new file mode 100644 index 000000000000..dba43e750a0e --- /dev/null +++ b/query-engine/driver-adapters/executor/bench/Dockerfile @@ -0,0 +1,4 @@ +FROM postgres:15 +COPY seed.sql.gz . +RUN gunzip seed.sql.gz && \ + mv seed.sql /docker-entrypoint-initdb.d/seed.sql \ No newline at end of file diff --git a/query-engine/driver-adapters/executor/bench/docker-compose.yml b/query-engine/driver-adapters/executor/bench/docker-compose.yml new file mode 100644 index 000000000000..e404877e55ad --- /dev/null +++ b/query-engine/driver-adapters/executor/bench/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3.8' + +name: bench + +services: + postgres: + build: + context: . + dockerfile: Dockerfile + shm_size: 1g + restart: unless-stopped + # Uncomment the following line to enable query logging + # Then restart the container. + # command: ['postgres', '-c', 'log_statement=all'] + environment: + - POSTGRES_DB=bench + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + ports: + - '5432:5432' + healthcheck: + test: ['CMD', 'pg_isready'] + interval: 5s + timeout: 2s + retries: 20 diff --git a/query-engine/driver-adapters/executor/bench/queries.json b/query-engine/driver-adapters/executor/bench/queries.json new file mode 100644 index 000000000000..e143da135acc --- /dev/null +++ b/query-engine/driver-adapters/executor/bench/queries.json @@ -0,0 +1,209 @@ +[ + { + "description": "movies.findMany() (all - 25000)", + "query": { + "action": "findMany", + "modelName": "Movie", + "query": { + "selection": { + "$scalars": true + } + } + } + }, + { + "description": "movies.findMany({ take: 2000 })", + "query": { + "action": "findMany", + "modelName": "Movie", + "query": { + "arguments": { + "take": 2000 + }, + "selection": { + "$scalars": true + } + } + } + }, + { + "description": "movies.findMany({ where: {...}, take: 2000 })", + "query": { + "action": "findMany", + "modelName": "Movie", + "query": { + "arguments": { + "take": 2000, + "where": { + "title": { + "contains": "cyan" + } + } + }, + "selection": { + "$scalars": true + } + } + } + }, + { + "description": "movies.findMany({ include: { cast: true } take: 2000 }) (m2m)", + "query": { + "action": "findMany", + "modelName": "Movie", + "query": { + "arguments": { + "take": 2000 + }, + "include": { + "cast": true + }, + "selection": { + "$scalars": true + } + } + } + }, + { + "description": "movies.findMany({ where: {...}, include: { cast: true } take: 2000 }) (m2m)", + "query": { + "action": "findMany", + "modelName": "Movie", + "query": { + "arguments": { + "take": 2000, + "where": { + "title": { + "contains": "cyan" + } + } + }, + "include": { + "cast": true + }, + "selection": { + "$scalars": true + } + } + } + }, + { + "description": "movies.findMany({ take: 2000, include: { cast: { include: { person: true } } } })", + "query": { + "action": "findMany", + "modelName": "Movie", + "query": { + "arguments": { + "take": 2000 + }, + "include": { + "cast": { + "include": { + "person": true + } + } + }, + "selection": { + "$scalars": true + } + } + } + }, + { + "description": "movie.findMany({ where: { ... }, take: 2000, include: { cast: { include: { person: true } } } })", + "query": { + "action": "findMany", + "modelName": "Movie", + "query": { + "arguments": { + "take": 2000, + "where": { + "title": { + "contains": "cyan" + } + } + }, + "include": { + "cast": { + "include": { + "person": true + } + } + }, + "selection": { + "$scalars": true + } + } + } + }, + { + "description": "movie.findMany({ where: { reviews: { author: { ... } }, take: 100 }) (to-many -> to-one)", + "query": { + "action": "findMany", + "modelName": "Movie", + "query": { + "arguments": { + "take": 100, + "where": { + "reviews": { + "some": { + "author": { + "OR": [ + { + "name": { + "gt": "a" + } + }, + { + "name": { + "lt": "f" + } + } + ] + } + } + } + } + }, + "selection": { + "$scalars": true + } + } + } + }, + { + "description": "movie.findMany({ where: { cast: { person: { ... } }, take: 100 }) (m2m -> to-one)", + "query": { + "action": "findMany", + "modelName": "Movie", + "query": { + "arguments": { + "take": 100, + "where": { + "cast": { + "some": { + "person": { + "OR": [ + { + "last_name": { + "gt": "a" + } + }, + { + "last_name": { + "lt": "f" + } + } + ] + } + } + } + } + }, + "selection": { + "$scalars": true + } + } + } + } +] diff --git a/query-engine/driver-adapters/executor/bench/schema.prisma b/query-engine/driver-adapters/executor/bench/schema.prisma new file mode 100644 index 000000000000..a45c1e62b4cc --- /dev/null +++ b/query-engine/driver-adapters/executor/bench/schema.prisma @@ -0,0 +1,56 @@ +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} + +generator foo { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +model Movie { + id Int @id @default(autoincrement()) + title String + year Int + description String + + cast Actor[] // m2m + reviews Review[] // one2m +} + +model Actor { + id Int @id @default(autoincrement()) + + name String + + movies Movie[] // m2m + + person_id Int + person Person @relation(fields: [person_id], references: [id]) +} + +model Person { + id Int @id @default(autoincrement()) + first_name String + last_name String + + acted_in Actor[] // one2m +} + +model Review { + id Int @id @default(autoincrement()) + body String + rating Int + + author_id Int + author User @relation(fields: [author_id], references: [id]) //one2m + + movie_id Int + movie Movie @relation(fields: [movie_id], references: [id]) // one2m +} + +model User { + id Int @id @default(autoincrement()) + name String + reviews Review[] // one2m +} diff --git a/query-engine/driver-adapters/executor/bench/seed.sql.gz b/query-engine/driver-adapters/executor/bench/seed.sql.gz new file mode 100644 index 000000000000..a72994c2a13c Binary files /dev/null and b/query-engine/driver-adapters/executor/bench/seed.sql.gz differ diff --git a/query-engine/driver-adapters/connector-test-kit-executor/package.json b/query-engine/driver-adapters/executor/package.json similarity index 77% rename from query-engine/driver-adapters/connector-test-kit-executor/package.json rename to query-engine/driver-adapters/executor/package.json index e7ee6b999f47..558658d46ddf 100644 --- a/query-engine/driver-adapters/connector-test-kit-executor/package.json +++ b/query-engine/driver-adapters/executor/package.json @@ -3,14 +3,12 @@ "node": ">=16.13", "pnpm": ">=8.6.6 <9" }, - "name": "connector-test-kit-executor", + "name": "executor", "version": "0.0.1", "description": "", - "main": "dist/index.mjs", - "module": "dist/index.mjs", "private": true, "scripts": { - "build": "tsup ./src/index.ts --format esm --dts" + "build": "tsup ./src/testd.ts ./src/bench.ts --format esm --dts" }, "tsup": { "external": [ @@ -25,12 +23,15 @@ "@libsql/client": "0.3.6", "@neondatabase/serverless": "0.6.0", "@planetscale/database": "1.13.0", + "query-engine-wasm-latest": "npm:@prisma/query-engine-wasm@latest", + "query-engine-wasm-baseline": "npm:@prisma/query-engine-wasm@0.0.19", "@prisma/adapter-libsql": "workspace:*", "@prisma/adapter-neon": "workspace:*", "@prisma/adapter-pg": "workspace:*", "@prisma/adapter-planetscale": "workspace:*", "@prisma/driver-adapter-utils": "workspace:*", "@types/pg": "8.10.9", + "mitata": "^0.1.6", "pg": "8.11.3", "undici": "6.0.1", "ws": "8.14.2" @@ -40,4 +41,4 @@ "tsup": "7.2.0", "typescript": "5.3.3" } -} \ No newline at end of file +} diff --git a/query-engine/driver-adapters/executor/script/testd.sh b/query-engine/driver-adapters/executor/script/testd.sh new file mode 100755 index 000000000000..b61fb5deb981 --- /dev/null +++ b/query-engine/driver-adapters/executor/script/testd.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +node "$(dirname "${BASH_SOURCE[0]}")/../dist/testd.mjs" diff --git a/query-engine/driver-adapters/executor/src/bench.ts b/query-engine/driver-adapters/executor/src/bench.ts new file mode 100644 index 000000000000..fbf519272ee5 --- /dev/null +++ b/query-engine/driver-adapters/executor/src/bench.ts @@ -0,0 +1,216 @@ +/** + * Run with: `node --experimental-wasm-modules ./example.js` + * on Node.js 18+. + */ +import { webcrypto } from "node:crypto"; +import * as fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import * as qe from "./qe"; + +import pgDriver from "pg"; +import * as prismaPg from "@prisma/adapter-pg"; +import { bindAdapter, DriverAdapter } from "@prisma/driver-adapter-utils"; + +import { recording } from "./recording"; +import prismaQueries from "../bench/queries.json"; + +import { run, bench, group, baseline } from "mitata"; + +import { QueryEngine as WasmBaseline } from "query-engine-wasm-baseline"; +import { QueryEngine as WasmLatest } from "query-engine-wasm-latest"; + +(global as any).crypto = webcrypto; + +async function main(): Promise { + // read the prisma schema from stdin + + const dirname = path.dirname(fileURLToPath(import.meta.url)); + var datamodel = ( + await fs.readFile(path.resolve(dirname, "..", "bench", "schema.prisma")) + ).toString(); + + const url = process.env.DATABASE_URL; + if (url == null) { + throw new Error("DATABASE_URL is not defined"); + } + const pg = await pgAdapter(url); + const withErrorCapturing = bindAdapter(pg); + + // We build two decorators for recording and replaying db queries. + const { recorder, replayer } = recording(withErrorCapturing); + + // We exercise the queries recording them + await recordQueries(recorder, datamodel, prismaQueries); + + // Then we benchmark the execution of the queries but instead of hitting the DB + // we fetch results from the recordings, thus isolating the performance + // of the engine + driver adapter code from that of the DB IO. + await benchMarkQueries(replayer, datamodel, prismaQueries); +} + +async function recordQueries( + adapter: DriverAdapter, + datamodel: string, + prismaQueries: any +): Promise { + const qe = await initQeWasmBaseLine(adapter, datamodel); + await qe.connect(""); + + try { + for (const prismaQuery of prismaQueries) { + const { description, query } = prismaQuery; + const res = await qe.query(JSON.stringify(query), "", undefined); + + const errors = JSON.parse(res).errors; + if (errors != null && errors.length > 0) { + throw new Error( + `Query failed for ${description}: ${JSON.stringify(res)}` + ); + } + } + } finally { + await qe.disconnect(""); + } +} + +async function benchMarkQueries( + adapter: DriverAdapter, + datamodel: string, + prismaQueries: any +) { + const napi = await initQeNapiCurrent(adapter, datamodel); + await napi.connect(""); + const wasmCurrent = await initQeWasmCurrent(adapter, datamodel); + await wasmCurrent.connect(""); + const wasmBaseline = await initQeWasmBaseLine(adapter, datamodel); + await wasmBaseline.connect(""); + const wasmLatest = await initQeWasmLatest(adapter, datamodel); + await wasmLatest.connect(""); + + for (const prismaQuery of prismaQueries) { + const { description, query } = prismaQuery; + + const engines = { + Napi: napi, + "WASM Current": wasmCurrent, + "WASM Baseline": wasmBaseline, + "WASM Latest": wasmLatest, + }; + + for (const [engineName, engine] of Object.entries(engines)) { + const res = await engine.query(JSON.stringify(query), "", undefined); + const errors = JSON.parse(res).errors; + if (errors != null && errors.length > 0) { + throw new Error( + `${engineName} - Query failed for ${description}: ${JSON.stringify( + res + )}` + ); + } + } + } + + try { + for (const prismaQuery of prismaQueries) { + const { description, query } = prismaQuery; + const jsonQuery = JSON.stringify(query); + const irrelevantTraceId = ""; + const noTx = undefined; + + group(description, () => { + bench( + "Web Assembly: Baseline", + async () => + await wasmBaseline.query(jsonQuery, irrelevantTraceId, noTx) + ); + bench( + "Web Assembly: Latest", + async () => await wasmLatest.query(jsonQuery, irrelevantTraceId, noTx) + ); + baseline( + "Web Assembly: Current", + async () => + await wasmCurrent.query(jsonQuery, irrelevantTraceId, noTx) + ); + bench( + "Node API: Current", + async () => await napi.query(jsonQuery, irrelevantTraceId, noTx) + ); + }); + } + + await run({ + colors: false, + collect: true, + }); + } finally { + await napi.disconnect(""); + await wasmCurrent.disconnect(""); + await wasmBaseline.disconnect(""); + await wasmLatest.disconnect(""); + } +} + +// conditional debug logging based on LOG_LEVEL env var +const debug = (() => { + if ((process.env.LOG_LEVEL ?? "").toLowerCase() != "debug") { + return (...args: any[]) => {}; + } + + return (...args: any[]) => { + console.error("[nodejs] DEBUG:", ...args); + }; +})(); + +async function pgAdapter(url: string): Promise { + const schemaName = new URL(url).searchParams.get("schema") ?? undefined; + let args: any = { connectionString: url }; + if (schemaName != null) { + args.options = `--search_path="${schemaName}"`; + } + const pool = new pgDriver.Pool(args); + + return new prismaPg.PrismaPg(pool, { + schema: schemaName, + }); +} + +async function initQeNapiCurrent( + adapter: DriverAdapter, + datamodel: string +): Promise { + return await qe.initQueryEngine("Napi", adapter, datamodel, debug, debug); +} + +async function initQeWasmCurrent( + adapter: DriverAdapter, + datamodel: string +): Promise { + return await qe.initQueryEngine( + "Wasm", + adapter, + datamodel, + (...args) => {}, + debug + ); +} + +async function initQeWasmLatest( + adapter: DriverAdapter, + datamodel: string +): Promise { + return new WasmLatest(qe.queryEngineOptions(datamodel), debug, adapter); +} + +function initQeWasmBaseLine( + adapter: DriverAdapter, + datamodel: string +): qe.QueryEngine { + return new WasmBaseline(qe.queryEngineOptions(datamodel), debug, adapter); +} + +const err = (...args: any[]) => console.error("[nodejs] ERROR:", ...args); + +main().catch(err); diff --git a/query-engine/driver-adapters/connector-test-kit-executor/src/engines/JsonProtocol.ts b/query-engine/driver-adapters/executor/src/engines/JsonProtocol.ts similarity index 100% rename from query-engine/driver-adapters/connector-test-kit-executor/src/engines/JsonProtocol.ts rename to query-engine/driver-adapters/executor/src/engines/JsonProtocol.ts diff --git a/query-engine/driver-adapters/connector-test-kit-executor/src/engines/Library.ts b/query-engine/driver-adapters/executor/src/engines/Library.ts similarity index 100% rename from query-engine/driver-adapters/connector-test-kit-executor/src/engines/Library.ts rename to query-engine/driver-adapters/executor/src/engines/Library.ts diff --git a/query-engine/driver-adapters/connector-test-kit-executor/src/engines/QueryEngine.ts b/query-engine/driver-adapters/executor/src/engines/QueryEngine.ts similarity index 100% rename from query-engine/driver-adapters/connector-test-kit-executor/src/engines/QueryEngine.ts rename to query-engine/driver-adapters/executor/src/engines/QueryEngine.ts diff --git a/query-engine/driver-adapters/connector-test-kit-executor/src/engines/Transaction.ts b/query-engine/driver-adapters/executor/src/engines/Transaction.ts similarity index 100% rename from query-engine/driver-adapters/connector-test-kit-executor/src/engines/Transaction.ts rename to query-engine/driver-adapters/executor/src/engines/Transaction.ts diff --git a/query-engine/driver-adapters/connector-test-kit-executor/src/jsonRpc.ts b/query-engine/driver-adapters/executor/src/jsonRpc.ts similarity index 100% rename from query-engine/driver-adapters/connector-test-kit-executor/src/jsonRpc.ts rename to query-engine/driver-adapters/executor/src/jsonRpc.ts diff --git a/query-engine/driver-adapters/executor/src/qe.ts b/query-engine/driver-adapters/executor/src/qe.ts new file mode 100644 index 000000000000..6937c02cd4d5 --- /dev/null +++ b/query-engine/driver-adapters/executor/src/qe.ts @@ -0,0 +1,76 @@ +import type { DriverAdapter } from "@prisma/driver-adapter-utils"; +import * as napi from "./engines/Library"; +import * as os from "node:os"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +export interface QueryEngine { + connect(trace: string): Promise; + disconnect(trace: string): Promise; + query(body: string, trace: string, tx_id?: string): Promise; + startTransaction(input: string, trace: string): Promise; + commitTransaction(tx_id: string, trace: string): Promise; + rollbackTransaction(tx_id: string, trace: string): Promise; +} + +export type QueryLogCallback = (log: string) => void; + +export async function initQueryEngine( + engineType: "Napi" | "Wasm", + adapter: DriverAdapter, + datamodel: string, + queryLogCallback: QueryLogCallback, + debug: (...args: any[]) => void +): Promise { + const logCallback = (event: any) => { + const parsed = JSON.parse(event); + if (parsed.is_query) { + queryLogCallback(parsed.query); + } + debug(parsed); + }; + + const options = queryEngineOptions(datamodel); + + if (engineType === "Wasm") { + const { WasmQueryEngine } = await import("./wasm"); + return new WasmQueryEngine(options, logCallback, adapter); + } else { + const { QueryEngine } = loadNapiEngine(); + return new QueryEngine(options, logCallback, adapter); + } +} + +export function queryEngineOptions(datamodel: string) { + return { + datamodel, + configDir: ".", + engineProtocol: "json" as const, + logLevel: process.env["RUST_LOG"] ?? ("info" as any), + logQueries: true, + env: process.env, + ignoreEnvVarErrors: false, + }; +} + +function loadNapiEngine(): napi.Library { + // I assume nobody will run this on Windows ¯\_(ツ)_/¯ + const libExt = os.platform() === "darwin" ? "dylib" : "so"; + const target = + process.env.TARGET || process.env.PROFILE == "release" + ? "release" + : "debug"; + + const libQueryEnginePath = path.resolve( + dirname, + `../../../../target/${target}/libquery_engine.${libExt}` + ); + + const libqueryEngine = { exports: {} as unknown as napi.Library }; + // @ts-ignore + process.dlopen(libqueryEngine, libQueryEnginePath); + + return libqueryEngine.exports; +} diff --git a/query-engine/driver-adapters/executor/src/recording.ts b/query-engine/driver-adapters/executor/src/recording.ts new file mode 100644 index 000000000000..f72997212ea4 --- /dev/null +++ b/query-engine/driver-adapters/executor/src/recording.ts @@ -0,0 +1,93 @@ +import { + type DriverAdapter, + type Query, + type Result, + type ResultSet, +} from "@prisma/driver-adapter-utils"; +import { RetryHandler } from "undici"; + +export function recording(adapter: DriverAdapter) { + const recordings = createInMemoryRecordings(); + + return { + recorder: recorder(adapter, recordings), + replayer: replayer(adapter, recordings), + }; +} + +function recorder(adapter: DriverAdapter, recordings) { + return { + provider: adapter.provider, + startTransaction: () => { + throw new Error("Not implemented"); + }, + getConnectionInfo: () => { + return adapter.getConnectionInfo!(); + }, + queryRaw: async (params) => { + const result = adapter.queryRaw(params); + recordings.addQueryResults(params, result); + return result; + }, + + executeRaw: async (params) => { + const result = adapter.executeRaw(params); + recordings.addCommandResults(params, result); + return result; + }, + }; +} + +function replayer(adapter: DriverAdapter, recordings) { + return { + provider: adapter.provider, + recordings: recordings, + startTransaction: () => { + throw new Error("Not implemented"); + }, + getConnectionInfo: () => { + return adapter.getConnectionInfo!(); + }, + queryRaw: async (params) => { + return recordings.getQueryResults(params); + }, + executeRaw: async (params) => { + return recordings.getCommandResults(params); + }, + }; +} + +function createInMemoryRecordings() { + const queryResults = {}; + const commandResults = {}; + + const queryToKey = (query) => JSON.stringify(query); + + return { + addQueryResults: (params, result) => { + const key = queryToKey(params); + queryResults[key] = result; + }, + + getQueryResults: (params) => { + const key = queryToKey(params); + if (!(key in queryResults)) { + throw new Error(`Query not recorded: ${key}`); + } + return queryResults[key]; + }, + + addCommandResults: (params, result) => { + const key = queryToKey(params); + commandResults[key] = result; + }, + + getCommandResults: (params) => { + const key = queryToKey(params); + if (!(key in commandResults)) { + throw new Error(`Command not recorded: ${key}`); + } + return commandResults[key]; + }, + }; +} diff --git a/query-engine/driver-adapters/connector-test-kit-executor/src/index.ts b/query-engine/driver-adapters/executor/src/testd.ts similarity index 97% rename from query-engine/driver-adapters/connector-test-kit-executor/src/index.ts rename to query-engine/driver-adapters/executor/src/testd.ts index 82ee8a4689dd..0aa03dcb215b 100644 --- a/query-engine/driver-adapters/connector-test-kit-executor/src/index.ts +++ b/query-engine/driver-adapters/executor/src/testd.ts @@ -218,9 +218,10 @@ function respondOk(requestId: number, payload: unknown) { } async function initQe(url: string, prismaSchema: string, logCallback: qe.QueryLogCallback): Promise<[qe.QueryEngine, ErrorCapturingDriverAdapter]> { + const engineType = process.env.EXTERNAL_TEST_EXECUTOR === "Wasm" ? "Wasm" : "Napi"; const adapter = await adapterFromEnv(url) as DriverAdapter const errorCapturingAdapter = bindAdapter(adapter) - const engineInstance = await qe.initQueryEngine(errorCapturingAdapter, prismaSchema, logCallback, debug) + const engineInstance = await qe.initQueryEngine(engineType, errorCapturingAdapter, prismaSchema, logCallback, debug) return [engineInstance, errorCapturingAdapter]; } diff --git a/query-engine/driver-adapters/connector-test-kit-executor/src/wasm.ts b/query-engine/driver-adapters/executor/src/wasm.ts similarity index 100% rename from query-engine/driver-adapters/connector-test-kit-executor/src/wasm.ts rename to query-engine/driver-adapters/executor/src/wasm.ts diff --git a/query-engine/driver-adapters/connector-test-kit-executor/tsconfig.json b/query-engine/driver-adapters/executor/tsconfig.json similarity index 100% rename from query-engine/driver-adapters/connector-test-kit-executor/tsconfig.json rename to query-engine/driver-adapters/executor/tsconfig.json diff --git a/query-engine/driver-adapters/pnpm-workspace.yaml b/query-engine/driver-adapters/pnpm-workspace.yaml index d37910ea5ae6..a616622479a5 100644 --- a/query-engine/driver-adapters/pnpm-workspace.yaml +++ b/query-engine/driver-adapters/pnpm-workspace.yaml @@ -5,4 +5,4 @@ packages: - '../../../prisma/packages/adapter-planetscale' - '../../../prisma/packages/driver-adapter-utils' - '../../../prisma/packages/debug' - - './connector-test-kit-executor' \ No newline at end of file + - './executor' diff --git a/query-engine/query-engine-wasm/build.sh b/query-engine/query-engine-wasm/build.sh index 00f3e696925a..b6cf5b685733 100755 --- a/query-engine/query-engine-wasm/build.sh +++ b/query-engine/query-engine-wasm/build.sh @@ -25,10 +25,10 @@ echo "Using build profile: \"${WASM_BUILD_PROFILE}\"" if ! command -v wasm-pack &> /dev/null then echo "wasm-pack could not be found, installing now..." - # Install wasm-pack curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh fi +echo "Building query-engine-wasm using $WASM_BUILD_PROFILE profile" wasm-pack build "--$WASM_BUILD_PROFILE" --target $OUT_TARGET --out-name query_engine WASM_OPT_ARGS=(