From 94efe9dd4631d9b68ca5932e24d8d5484733ea39 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 1 Aug 2025 16:34:53 +0300 Subject: [PATCH] bindings/javascript: Reduce VM/native crossing overhead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: ``` penberg@vonneumann perf % node perf-turso.js cpu: Apple M1 runtime: node v22.16.0 (arm64-darwin) benchmark time (avg) (min … max) p75 p99 p999 ----------------------------------------------------------------------- ----------------------------- • Statement ----------------------------------------------------------------------- ----------------------------- Statement.get() bind parameters 1'525 ns/iter (1'482 ns … 1'720 ns) 1'534 ns 1'662 ns 1'720 ns summary for Statement Statement.get() bind parameters penberg@vonneumann perf % bun perf-turso.js cpu: Apple M1 runtime: bun 1.2.15 (arm64-darwin) benchmark time (avg) (min … max) p75 p99 p999 ----------------------------------------------------------------------- ----------------------------- • Statement ----------------------------------------------------------------------- ----------------------------- Statement.get() bind parameters 1'198 ns/iter (1'157 ns … 1'495 ns) 1'189 ns 1'456 ns 1'495 ns summary for Statement Statement.get() bind parameters ``` After: ``` benchmark time (avg) (min … max) p75 p99 p999 ----------------------------------------------------------------------- ----------------------------- • Statement ----------------------------------------------------------------------- ----------------------------- Statement.get() bind parameters 1'206 ns/iter (1'180 ns … 1'402 ns) 1'208 ns 1'365 ns 1'402 ns summary for Statement Statement.get() bind parameters penberg@vonneumann perf % bun perf-turso.js cpu: Apple M1 runtime: bun 1.2.15 (arm64-darwin) benchmark time (avg) (min … max) p75 p99 p999 ----------------------------------------------------------------------- ----------------------------- • Statement ----------------------------------------------------------------------- ----------------------------- Statement.get() bind parameters 1'019 ns/iter (980 ns … 1'360 ns) 1'005 ns 1'270 ns 1'360 ns summary for Statement Statement.get() bind parameters ``` --- bindings/javascript/index.d.ts | 11 ++- bindings/javascript/promise.js | 35 ++++++--- bindings/javascript/src/lib.rs | 135 +++++++++++++++++---------------- bindings/javascript/sync.js | 35 ++++++--- 4 files changed, 127 insertions(+), 89 deletions(-) diff --git a/bindings/javascript/index.d.ts b/bindings/javascript/index.d.ts index f38dfcf6d..f9447e696 100644 --- a/bindings/javascript/index.d.ts +++ b/bindings/javascript/index.d.ts @@ -94,8 +94,17 @@ export declare class Statement { * * `value` - The value to bind. */ bindAt(index: number, value: unknown): void - step(): unknown + /** + * Step the statement and return result code: + * 1 = Row available, 2 = Done, 3 = I/O needed + */ + step(): number + /** Get the current row data according to the presentation mode */ + row(): unknown + /** Sets the presentation mode to raw. */ raw(raw?: boolean | undefined | null): void + /** Sets the presentation mode to pluck. */ pluck(pluck?: boolean | undefined | null): void + /** Finalizes the statement. */ finalize(): void } diff --git a/bindings/javascript/promise.js b/bindings/javascript/promise.js index 6e9347a45..7a54871c3 100644 --- a/bindings/javascript/promise.js +++ b/bindings/javascript/promise.js @@ -5,6 +5,11 @@ const { bindParams } = require("./bind.js"); const SqliteError = require("./sqlite-error.js"); +// Step result constants +const STEP_ROW = 1; +const STEP_DONE = 2; +const STEP_IO = 3; + const convertibleErrorTypes = { TypeError }; const CONVERTIBLE_ERROR_PREFIX = "[TURSO_CONVERT_TYPE]"; @@ -258,14 +263,18 @@ class Statement { bindParams(this.stmt, bindParameters); while (true) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { await this.db.db.ioLoopAsync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { break; } + if (stepResult === STEP_ROW) { + // For run(), we don't need the row data, just continue + continue; + } } const lastInsertRowid = this.db.db.lastInsertRowid(); @@ -284,15 +293,17 @@ class Statement { bindParams(this.stmt, bindParameters); while (true) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { await this.db.db.ioLoopAsync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { return undefined; } - return result.value; + if (stepResult === STEP_ROW) { + return this.stmt.row(); + } } } @@ -316,15 +327,17 @@ class Statement { const rows = []; while (true) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { await this.db.db.ioLoopAsync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { break; } - rows.push(result.value); + if (stepResult === STEP_ROW) { + rows.push(this.stmt.row()); + } } return rows; } diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 611494b4a..b75afd85d 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -15,6 +15,11 @@ use napi::{Env, Task}; use napi_derive::napi; use std::{cell::RefCell, num::NonZeroUsize, sync::Arc}; +/// Step result constants +const STEP_ROW: u32 = 1; +const STEP_DONE: u32 = 2; +const STEP_IO: u32 = 3; + /// The presentation mode for rows. #[derive(Debug, Clone)] enum PresentationMode { @@ -289,92 +294,90 @@ impl Statement { Ok(()) } + /// Step the statement and return result code: + /// 1 = Row available, 2 = Done, 3 = I/O needed #[napi] - pub fn step<'env>(&self, env: &'env Env) -> Result> { + pub fn step(&self) -> Result { let mut stmt_ref = self.stmt.borrow_mut(); let stmt = stmt_ref .as_mut() .ok_or_else(|| Error::new(Status::GenericFailure, "Statement has been finalized"))?; - let mut result = Object::new(env)?; - match stmt.step() { - Ok(turso_core::StepResult::Row) => { - result.set_named_property("done", false)?; - - let row_data = stmt - .row() - .ok_or_else(|| Error::new(Status::GenericFailure, "No row data available"))?; - - let mode = self.mode.borrow(); - let row_value = - match *mode { - PresentationMode::Raw => { - let mut raw_array = env.create_array(row_data.len() as u32)?; - for (idx, value) in row_data.get_values().enumerate() { - let js_value = to_js_value(env, value)?; - raw_array.set(idx as u32, js_value)?; - } - raw_array.coerce_to_object()?.to_unknown() - } - PresentationMode::Pluck => { - let (_, value) = row_data.get_values().enumerate().next().ok_or( - napi::Error::new( - napi::Status::GenericFailure, - "Pluck mode requires at least one column in the result", - ), - )?; - to_js_value(env, value)? - } - PresentationMode::Expanded => { - let row = Object::new(env)?; - let raw_row = row.raw(); - let raw_env = env.raw(); - for idx in 0..row_data.len() { - let value = row_data.get_value(idx); - let column_name = &self.column_names[idx]; - let js_value = to_js_value(env, value)?; - unsafe { - napi::sys::napi_set_named_property( - raw_env, - raw_row, - column_name.as_ptr(), - js_value.raw(), - ); - } - } - row.to_unknown() - } - }; - - result.set_named_property("value", row_value)?; - } - Ok(turso_core::StepResult::Done) => { - result.set_named_property("done", true)?; - result.set_named_property("value", Null)?; - } - Ok(turso_core::StepResult::IO) => { - result.set_named_property("io", true)?; - result.set_named_property("value", Null)?; - } + Ok(turso_core::StepResult::Row) => Ok(STEP_ROW), + Ok(turso_core::StepResult::Done) => Ok(STEP_DONE), + Ok(turso_core::StepResult::IO) => Ok(STEP_IO), Ok(turso_core::StepResult::Interrupt) => { - return Err(Error::new( + Err(Error::new( Status::GenericFailure, "Statement was interrupted", - )); + )) } Ok(turso_core::StepResult::Busy) => { - return Err(Error::new(Status::GenericFailure, "Database is busy")); + Err(Error::new(Status::GenericFailure, "Database is busy")) } Err(e) => { - return Err(Error::new( + Err(Error::new( Status::GenericFailure, format!("Step failed: {e}"), )) } } + } - Ok(result.to_unknown()) + /// Get the current row data according to the presentation mode + #[napi] + pub fn row<'env>(&self, env: &'env Env) -> Result> { + let stmt_ref = self.stmt.borrow(); + let stmt = stmt_ref + .as_ref() + .ok_or_else(|| Error::new(Status::GenericFailure, "Statement has been finalized"))?; + + let row_data = stmt + .row() + .ok_or_else(|| Error::new(Status::GenericFailure, "No row data available"))?; + + let mode = self.mode.borrow(); + let row_value = match *mode { + PresentationMode::Raw => { + let mut raw_array = env.create_array(row_data.len() as u32)?; + for (idx, value) in row_data.get_values().enumerate() { + let js_value = to_js_value(env, value)?; + raw_array.set(idx as u32, js_value)?; + } + raw_array.coerce_to_object()?.to_unknown() + } + PresentationMode::Pluck => { + let (_, value) = row_data.get_values().enumerate().next().ok_or( + napi::Error::new( + napi::Status::GenericFailure, + "Pluck mode requires at least one column in the result", + ), + )?; + to_js_value(env, value)? + } + PresentationMode::Expanded => { + let row = Object::new(env)?; + let raw_row = row.raw(); + let raw_env = env.raw(); + for idx in 0..row_data.len() { + let value = row_data.get_value(idx); + let column_name = &self.column_names[idx]; + let js_value = to_js_value(env, value)?; + unsafe { + napi::sys::napi_set_named_property( + raw_env, + raw_row, + column_name.as_ptr(), + js_value.raw(), + ); + } + } + row.to_unknown() + } + }; + + Ok(row_value) } /// Sets the presentation mode to raw. diff --git a/bindings/javascript/sync.js b/bindings/javascript/sync.js index 1cf5954ac..bca456232 100644 --- a/bindings/javascript/sync.js +++ b/bindings/javascript/sync.js @@ -5,6 +5,11 @@ const { bindParams } = require("./bind.js"); const SqliteError = require("./sqlite-error.js"); +// Step result constants +const STEP_ROW = 1; +const STEP_DONE = 2; +const STEP_IO = 3; + const convertibleErrorTypes = { TypeError }; const CONVERTIBLE_ERROR_PREFIX = "[TURSO_CONVERT_TYPE]"; @@ -257,14 +262,18 @@ class Statement { this.stmt.reset(); bindParams(this.stmt, bindParameters); for (;;) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { this.db.db.ioLoopSync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { break; } + if (stepResult === STEP_ROW) { + // For run(), we don't need the row data, just continue + continue; + } } const lastInsertRowid = this.db.db.lastInsertRowid(); @@ -282,15 +291,17 @@ class Statement { this.stmt.reset(); bindParams(this.stmt, bindParameters); for (;;) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { this.db.db.ioLoopSync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { return undefined; } - return result.value; + if (stepResult === STEP_ROW) { + return this.stmt.row(); + } } } @@ -313,15 +324,17 @@ class Statement { bindParams(this.stmt, bindParameters); const rows = []; for (;;) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { this.db.db.ioLoopSync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { break; } - rows.push(result.value); + if (stepResult === STEP_ROW) { + rows.push(this.stmt.row()); + } } return rows; }