Merge 'bindings/javascript: Reduce VM/native crossing overhead' from Pekka Enberg

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
```

Closes #2391
This commit is contained in:
Pekka Enberg
2025-08-01 19:18:33 +03:00
committed by GitHub
6 changed files with 163 additions and 110 deletions

View File

@@ -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
}

View File

@@ -4,15 +4,23 @@ import Database from 'better-sqlite3';
const db = new Database(':memory:');
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)");
db.exec("CREATE TABLE users (id INTEGER, name TEXT, email TEXT)");
db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')");
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
const stmtSelect = db.prepare("SELECT * FROM users WHERE id = ?");
const rawStmtSelect = db.prepare("SELECT * FROM users WHERE id = ?").raw();
const stmtInsert = db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)");
group('Statement', () => {
bench('Statement.get() bind parameters', () => {
stmt.get(1);
});
bench('Statement.get() with bind parameters [expanded]', () => {
stmtSelect.get(1);
});
bench('Statement.git() with bind parameters [raw]', () => {
rawStmtSelect.get(1);
});
bench('Statement.run() with bind parameters', () => {
stmtInsert.run([1, 'foobar', 'foobar@example.com']);
});
await run({

View File

@@ -4,15 +4,23 @@ import Database from '@tursodatabase/turso';
const db = new Database(':memory:');
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)");
db.exec("CREATE TABLE users (id INTEGER, name TEXT, email TEXT)");
db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')");
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
const stmtSelect = db.prepare("SELECT * FROM users WHERE id = ?");
const rawStmtSelect = db.prepare("SELECT * FROM users WHERE id = ?").raw();
const stmtInsert = db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)");
group('Statement', () => {
bench('Statement.get() bind parameters', () => {
stmt.get(1);
});
bench('Statement.get() with bind parameters [expanded]', () => {
stmtSelect.get(1);
});
bench('Statement.get() with bind parameters [raw]', () => {
rawStmtSelect.get(1);
});
bench('Statement.run() with bind parameters', () => {
stmtInsert.run([1, 'foobar', 'foobar@example.com']);
});
await run({

View File

@@ -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;
}

View File

@@ -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,89 @@ 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<Unknown<'env>> {
pub fn step(&self) -> Result<u32> {
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::Interrupt) => {
return Err(Error::new(
Status::GenericFailure,
"Statement was interrupted",
));
}
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) => Err(Error::new(
Status::GenericFailure,
"Statement was interrupted",
)),
Ok(turso_core::StepResult::Busy) => {
return Err(Error::new(Status::GenericFailure, "Database is busy"));
}
Err(e) => {
return Err(Error::new(
Status::GenericFailure,
format!("Step failed: {e}"),
))
Err(Error::new(Status::GenericFailure, "Database is busy"))
}
Err(e) => 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<Unknown<'env>> {
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.

View File

@@ -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;
}