Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions benchmark/sqlite/sqlite-diagnostic-channel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';
const common = require('../common.js');
const sqlite = require('node:sqlite');
const dc = require('node:diagnostics_channel');
const assert = require('node:assert');

const bench = common.createBenchmark(main, {
n: [1e5],
mode: ['none', 'subscribed', 'unsubscribed'],
});

function main(conf) {
const { n, mode } = conf;

const db = new sqlite.DatabaseSync(':memory:');
db.exec('CREATE TABLE t (x INTEGER)');
const insert = db.prepare('INSERT INTO t VALUES (?)');

let subscriber;
if (mode === 'subscribed') {
subscriber = () => {};
dc.subscribe('sqlite.db.query', subscriber);
} else if (mode === 'unsubscribed') {
subscriber = () => {};
dc.subscribe('sqlite.db.query', subscriber);
dc.unsubscribe('sqlite.db.query', subscriber);
}
// mode === 'none': no subscription ever made

let result;
bench.start();
for (let i = 0; i < n; i++) {
result = insert.run(i);
}
bench.end(n);

if (mode === 'subscribed') {
dc.unsubscribe('sqlite.db.query', subscriber);
}

assert.ok(result !== undefined);
}
39 changes: 39 additions & 0 deletions doc/api/diagnostics_channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -1924,10 +1924,47 @@ added: v16.18.0

Emitted when a new thread is created.

#### SQLite

<!-- YAML
added: REPLACEME
-->

Comment thread
araujogui marked this conversation as resolved.
> Stability: 1 - Experimental

##### Event: `'sqlite.db.query'`

* `sql` {string} The expanded SQL with bound parameter values substituted.
If expansion fails, the source SQL with unsubstituted placeholders is used
instead.
* `database` {DatabaseSync} The [`DatabaseSync`][] instance that executed the
statement.
* `duration` {number} SQLite's internal estimate of the statement run time in
nanoseconds. This reflects C-layer execution time only and does not include
JavaScript binding overhead such as argument marshaling or result-row
construction.

Emitted after a SQL statement finishes executing against a [`DatabaseSync`][]
instance. This is a **profiling** event: it fires once per statement upon
completion and reports an estimated duration from SQLite's internal profiler.
It is not a distributed-tracing span. There is no corresponding start event,
no async context propagation, and no parent-span linkage. If you need
OpenTelemetry-compatible spans or async context propagation, wrap your SQLite
calls with a [`TracingChannel`][] at the JavaScript layer instead.

Publishing is zero-overhead when there are no subscribers.

No event is emitted for a statement that is abandoned mid-iteration and later
finalized, either explicitly through [`statement.close()`][] or when the
statement is garbage collected. Subscribers must not close the database or the
statement, since both are still in use while the event is being delivered; see
[`database.close()`][] and [`statement.close()`][].

[BoundedChannel Channels]: #boundedchannel-channels
[TracingChannel Channels]: #tracingchannel-channels
[`'uncaughtException'`]: process.md#event-uncaughtexception
[`BoundedChannel`]: #class-boundedchannel
[`DatabaseSync`]: sqlite.md#class-databasesync
[`TracingChannel`]: #class-tracingchannel
[`asyncEnd` event]: #asyncendevent
[`asyncStart` event]: #asyncstartevent
Expand All @@ -1938,6 +1975,7 @@ Emitted when a new thread is created.
[`channel.unsubscribe(onMessage)`]: #channelunsubscribeonmessage
[`channel.withStoreScope(data)`]: #channelwithstorescopedata
[`child_process.spawn()`]: child_process.md#child_processspawncommand-args-options
[`database.close()`]: sqlite.md#databaseclose
[`diagnostics_channel.channel(name)`]: #diagnostics_channelchannelname
[`diagnostics_channel.subscribe(name, onMessage)`]: #diagnostics_channelsubscribename-onmessage
[`diagnostics_channel.tracingChannel()`]: #diagnostics_channeltracingchannelnameorchannels
Expand All @@ -1947,6 +1985,7 @@ Emitted when a new thread is created.
[`net.Server.listen()`]: net.md#serverlisten
[`process.execve()`]: process.md#processexecvefile-args-env
[`start` event]: #startevent
[`statement.close()`]: sqlite.md#statementclose
[`worker_threads.locks`]: worker_threads.md#worker_threadslocks
[context loss]: async_context.md#troubleshooting-context-loss
[thenable object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables
22 changes: 17 additions & 5 deletions doc/api/sqlite.md
Comment thread
araujogui marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import sqlite from 'node:sqlite';
const sqlite = require('node:sqlite');
```

This module is only available under the `node:` scheme.
This module is only available under the `node:` scheme. SQL trace events can
be observed via the [`diagnostics_channel`][] module. See
[`'sqlite.db.query'`][] for details.

The following example shows the basic usage of the `node:sqlite` module to open
an in-memory database, write data to the database, and then read the data back.
Expand Down Expand Up @@ -311,8 +313,8 @@ added: v22.5.0
Closes the database connection. An exception is thrown if the database is not
open. An [`ERR_INVALID_STATE`][] error is thrown if the method is called while
a statement is executing, such as inside a user-defined function, an aggregate
function, or an authorizer callback. This method is a wrapper around
[`sqlite3_close_v2()`][].
function, an authorizer callback, or a [`'sqlite.db.query'`][] subscriber. This
method is a wrapper around [`sqlite3_close_v2()`][].

### `database.loadExtension(path[, entryPoint])`

Expand Down Expand Up @@ -1122,7 +1124,12 @@ added: REPLACEME
-->

Finalizes the prepared statement. An exception is thrown if the statement is
already finalized. This method is a wrapper around [`sqlite3_finalize()`][].
already finalized. An [`ERR_INVALID_STATE`][] error is thrown if this statement
is currently executing, which happens when the method is called from a callback
that the statement itself triggered, such as a user-defined function, an
aggregate function, or a [`'sqlite.db.query'`][] subscriber. Other statements
on the same connection can be finalized from such a callback. This method is a
wrapper around [`sqlite3_finalize()`][].

### `statement.columns()`

Expand Down Expand Up @@ -1369,7 +1376,9 @@ added: REPLACEME
-->

Finalizes the prepared statement. If the prepared statement is already
finalized, then this is a no-op.
finalized, then this is a no-op. An [`ERR_INVALID_STATE`][] error is thrown if
this statement is currently executing, under the same conditions as
[`statement.close()`][].

### `statement.stat(counter)`

Expand Down Expand Up @@ -1890,6 +1899,7 @@ callback function to indicate what type of operation is being authorized.
[Run-Time Limits]: https://www.sqlite.org/c3ref/limit.html
[SQL injection]: https://en.wikipedia.org/wiki/SQL_injection
[Type conversion between JavaScript and SQLite]: #type-conversion-between-javascript-and-sqlite
[`'sqlite.db.query'`]: diagnostics_channel.md#event-sqlitedbquery
[`ATTACH DATABASE`]: https://www.sqlite.org/lang_attach.html
[`ERR_INVALID_STATE`]: errors.md#err_invalid_state
[`PRAGMA foreign_keys`]: https://www.sqlite.org/pragma.html#pragma_foreign_keys
Expand All @@ -1903,6 +1913,7 @@ callback function to indicate what type of operation is being authorized.
[`database.createTagStore()`]: #databasecreatetagstoremaxsize
[`database.serialize()`]: #databaseserializedbname
[`database.setAuthorizer()`]: #databasesetauthorizercallback
[`diagnostics_channel`]: diagnostics_channel.md
[`sqlite3_backup_finish()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish
[`sqlite3_backup_init()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
[`sqlite3_backup_step()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
Expand Down Expand Up @@ -1934,6 +1945,7 @@ callback function to indicate what type of operation is being authorized.
[`sqlite3session_create()`]: https://www.sqlite.org/session/sqlite3session_create.html
[`sqlite3session_delete()`]: https://www.sqlite.org/session/sqlite3session_delete.html
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
[`statement.close()`]: #statementclose
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
[`statement.stat()`]: #statementstatcounter
Expand Down
8 changes: 8 additions & 0 deletions lib/diagnostics_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,19 @@ function markActive(channel) {
ObjectSetPrototypeOf(channel, ActiveChannel.prototype);
channel._subscribers = [];
channel._stores = new SafeMap();

// Notify native modules that this channel just got its first subscriber.
if (channel._index !== undefined)
dc_binding.notifyChannelActive(channel._index);
}

function maybeMarkInactive(channel) {
// When there are no more active subscribers or bound, restore to fast prototype.
if (!channel._subscribers.length && !channel._stores.size) {
// Notify native modules that this channel just lost its last subscriber.
if (channel._index !== undefined)
dc_binding.notifyChannelInactive(channel._index);

// eslint-disable-next-line no-use-before-define
ObjectSetPrototypeOf(channel, Channel.prototype);
channel._subscribers = undefined;
Expand Down
3 changes: 2 additions & 1 deletion src/base_object_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ namespace node {
#define UNSERIALIZABLE_BINDING_TYPES(V) \
V(http2_binding_data, http2::BindingData) \
V(http_parser_binding_data, http_parser::BindingData) \
V(quic_binding_data, quic::BindingData)
V(quic_binding_data, quic::BindingData) \
V(sqlite_binding_data, sqlite::BindingData)

// List of (non-binding) BaseObjects that are serializable in the snapshot.
// The first argument should match what the type passes to
Expand Down
2 changes: 2 additions & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
V(crypto_rsa_pss_string, "rsa-pss") \
V(cwd_string, "cwd") \
V(data_string, "data") \
V(database_string, "database") \
V(default_is_true_string, "defaultIsTrue") \
V(defensive_string, "defensive") \
V(deserialize_info_string, "deserializeInfo") \
Expand Down Expand Up @@ -359,6 +360,7 @@
V(source_map_url_string, "sourceMapURL") \
V(source_url_string, "sourceURL") \
V(specifier_string, "specifier") \
V(sql_string, "sql") \
V(stack_string, "stack") \
V(start_string, "start") \
V(state_string, "state") \
Expand Down
30 changes: 30 additions & 0 deletions src/node_diagnostics_channel.cc
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,38 @@ void BindingData::Deserialize(Local<Context> context,
CHECK_NOT_NULL(binding);
}

void BindingData::SetChannelStatusCallback(uint32_t index,
ChannelStatusCallback cb) {
channel_status_callbacks_[index] = std::move(cb);
}

void BindingData::NotifyChannelActive(const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);
BindingData* binding = realm->GetBindingData<BindingData>();
if (binding == nullptr) return;
CHECK(args[0]->IsUint32());
uint32_t index = args[0].As<v8::Uint32>()->Value();
auto it = binding->channel_status_callbacks_.find(index);
if (it != binding->channel_status_callbacks_.end()) it->second(true);
}
Comment thread
araujogui marked this conversation as resolved.

void BindingData::NotifyChannelInactive(
const FunctionCallbackInfo<Value>& args) {
Realm* realm = Realm::GetCurrent(args);
BindingData* binding = realm->GetBindingData<BindingData>();
if (binding == nullptr) return;
CHECK(args[0]->IsUint32());
uint32_t index = args[0].As<v8::Uint32>()->Value();
auto it = binding->channel_status_callbacks_.find(index);
if (it != binding->channel_status_callbacks_.end()) it->second(false);
}
Comment thread
araujogui marked this conversation as resolved.

void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
SetMethod(isolate, target, "linkNativeChannel", LinkNativeChannel);
SetMethod(isolate, target, "notifyChannelActive", NotifyChannelActive);
SetMethod(isolate, target, "notifyChannelInactive", NotifyChannelInactive);
}

void BindingData::CreatePerContextProperties(Local<Object> target,
Expand All @@ -145,6 +173,8 @@ void BindingData::CreatePerContextProperties(Local<Object> target,
void BindingData::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(LinkNativeChannel);
registry->Register(NotifyChannelActive);
registry->Register(NotifyChannelInactive);
}

Channel::Channel(Environment* env,
Expand Down
10 changes: 10 additions & 0 deletions src/node_diagnostics_channel.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include <cinttypes>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
Expand Down Expand Up @@ -52,6 +53,14 @@ class BindingData : public SnapshotableObject {
static void LinkNativeChannel(
const v8::FunctionCallbackInfo<v8::Value>& args);

using ChannelStatusCallback = std::function<void(bool is_active)>;
void SetChannelStatusCallback(uint32_t index, ChannelStatusCallback cb);

static void NotifyChannelActive(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void NotifyChannelInactive(
const v8::FunctionCallbackInfo<v8::Value>& args);

static void CreatePerIsolateProperties(IsolateData* isolate_data,
v8::Local<v8::ObjectTemplate> target);
static void CreatePerContextProperties(v8::Local<v8::Object> target,
Expand All @@ -62,6 +71,7 @@ class BindingData : public SnapshotableObject {

private:
InternalFieldInfo* internal_field_info_ = nullptr;
std::unordered_map<uint32_t, ChannelStatusCallback> channel_status_callbacks_;
};
Comment thread
araujogui marked this conversation as resolved.

class Channel : public BaseObject {
Expand Down
Loading
Loading