mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-03 08:24:19 +01:00
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:
11
bindings/javascript/index.d.ts
vendored
11
bindings/javascript/index.d.ts
vendored
@@ -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
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user