Commit 72c37b1

Browse files
geeksilva97aduh95
authored andcommitted
sqlite: expose prepared statement statistics
Signed-off-by: geeksilva97 <edigleyssonsilva@gmail.com> PR-URL: #64541 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent e3fda69 commit 72c37b1

4 files changed

Lines changed: 353 additions & 6 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,18 @@ returns an empty iterator. The prepared statement [parameters are bound][] using
11881188
the values in `namedParameters` and `anonymousParameters`. See
11891189
[Binding parameters][].
11901190

1191+
### `statement.resetStats()`
1192+
1193+
<!-- YAML
1194+
added: REPLACEME
1195+
-->
1196+
1197+
Resets every counter reported by [`statement.stat()`][] back to zero, except
1198+
`memused`, which reports current memory usage and cannot be reset. This
1199+
method is a wrapper around [`sqlite3_stmt_status()`][] and is useful for
1200+
measuring a specific workload without the counts accumulated by earlier
1201+
executions of the same prepared statement.
1202+
11911203
### `statement.run([namedParameters][, ...anonymousParameters])`
11921204

11931205
<!-- YAML
@@ -1314,6 +1326,43 @@ added: REPLACEME
13141326
Finalizes the prepared statement. If the prepared statement is already
13151327
finalized, then this is a no-op.
13161328

1329+
### `statement.stat(counter)`
1330+
1331+
<!-- YAML
1332+
added: REPLACEME
1333+
-->
1334+
1335+
*`counter` {string} The name of the counter to read. One of:
1336+
1337+
*`'fullscanStep'` The number of times SQLite has stepped forward in a table
1338+
as part of a full table scan.
1339+
*`'sort'` The number of sort operations that have occurred.
1340+
*`'autoindex'` The number of rows inserted into transient indices that were
1341+
created automatically to help joins run faster.
1342+
*`'vmStep'` The number of virtual machine operations executed by the
1343+
prepared statement.
1344+
*`'reprepare'` The number of times the statement has been automatically
1345+
reprepared due to schema changes or changes to bound parameters.
1346+
*`'run'` The number of execution cycles started by the prepared statement.
1347+
*`'filterMiss'` The number of times the Bloom filter returned a result that
1348+
required the join step to be processed as normal.
1349+
*`'filterHit'` The number of times a join step was bypassed because a Bloom
1350+
filter returned not-found.
1351+
*`'memused'` The approximate number of bytes of heap memory used to store
1352+
the prepared statement.
1353+
1354+
* Returns: {number} The current value of the requested counter.
1355+
1356+
Returns one of the runtime counters that SQLite tracks for this prepared
1357+
statement. This method is a wrapper around [`sqlite3_stmt_status()`][] and does
1358+
not reset the counter. Asserting that a statement does not perform a full table
1359+
scan (`statement.stat('fullscanStep') === 0`) is a useful check to guard
1360+
against degenerate performance.
1361+
1362+
The `'filterMiss'` and `'filterHit'` counters require SQLite 3.38.0 or later.
1363+
Builds linked against an older SQLite with `--shared-sqlite` do not expose them,
1364+
and passing either name throws `ERR_INVALID_ARG_VALUE`.
1365+
13171366
## Class: `SQLTagStore`
13181367

13191368
<!-- YAML
@@ -1832,6 +1881,7 @@ callback function to indicate what type of operation is being authorized.
18321881
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
18331882
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
18341883
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
1884+
[`sqlite3_stmt_status()`]: https://www.sqlite.org/c3ref/stmt_status.html
18351885
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
18361886
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
18371887
[`sqlite3session_changeset()`]: https://www.sqlite.org/session/sqlite3session_changeset.html
@@ -1840,6 +1890,7 @@ callback function to indicate what type of operation is being authorized.
18401890
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
18411891
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
18421892
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
1893+
[`statement.stat()`]: #statementstatcounter
18431894
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
18441895
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
18451896
[data types]: https://www.sqlite.org/datatype3.html

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ Local<DictionaryTemplate> getLazyIterTemplate(Environment* env) {
177177
}
178178
} // namespace
179179

180-
// Helper function to find limit info from JS property name
181-
staticconstexprconst LimitInfo* GetLimitInfoFromName(std::string_view name) {
182-
for (constauto& info : kLimitMapping) {
180+
// Helper function to look up a mapping entry by its JS-facing name
181+
template <typename T, size_t N>
182+
staticconstexprconst T* FindByJsName(const std::array<T, N>& mapping,
183+
std::string_view name) {
184+
for (constauto& info : mapping) {
183185
if (name == info.js_name) {
184186
return &info;
185187
}
@@ -793,7 +795,8 @@ Intercepted DatabaseSyncLimits::LimitsGetter(
793795
Isolate* isolate = env->isolate();
794796

795797
Utf8Value prop_name(isolate, property);
796-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
798+
const LimitInfo* limit_info =
799+
FindByJsName(kLimitMapping, prop_name.ToStringView());
797800

798801
if (limit_info == nullptr) {
799802
return Intercepted::kNo; // Unknown property, let default handling occur
@@ -825,7 +828,8 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
825828
Isolate* isolate = env->isolate();
826829

827830
Utf8Value prop_name(isolate, property);
828-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
831+
const LimitInfo* limit_info =
832+
FindByJsName(kLimitMapping, prop_name.ToStringView());
829833

830834
if (limit_info == nullptr) {
831835
return Intercepted::kNo;
@@ -873,7 +877,8 @@ Intercepted DatabaseSyncLimits::LimitsQuery(
873877

874878
Isolate* isolate = info.GetIsolate();
875879
Utf8Value prop_name(isolate, property);
876-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
880+
const LimitInfo* limit_info =
881+
FindByJsName(kLimitMapping, prop_name.ToStringView());
877882

878883
if (!limit_info) {
879884
return Intercepted::kNo;
@@ -2694,6 +2699,7 @@ void StatementSync::Finalize() {
26942699

26952700
voidStatementSync::InvalidateColumnNameCache() {
26962701
cached_column_names_.clear();
2702+
cached_column_names_reprepare_count_ = -1;
26972703
}
26982704

26992705
inlineboolStatementSync::IsFinalized() {
@@ -3350,6 +3356,60 @@ void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo<Value>& args) {
33503356
args.GetReturnValue().Set(result);
33513357
}
33523358

3359+
voidStatementSync::Stat(const FunctionCallbackInfo<Value>& args) {
3360+
StatementSync* stmt;
3361+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3362+
Environment* env = Environment::GetCurrent(args);
3363+
THROW_AND_RETURN_ON_BAD_STATE(
3364+
env, stmt->IsFinalized(), "statement has been finalized");
3365+
Isolate* isolate = env->isolate();
3366+
3367+
if (!args[0]->IsString()) {
3368+
THROW_ERR_INVALID_ARG_TYPE(isolate,
3369+
"The \"counter\" argument must be a string.");
3370+
return;
3371+
}
3372+
3373+
Utf8Value counter(isolate, args[0].As<String>());
3374+
const StatusInfo* status_info =
3375+
FindByJsName(kStatusMapping, counter.ToStringView());
3376+
if (status_info == nullptr) {
3377+
THROW_ERR_INVALID_ARG_VALUE(
3378+
isolate, "The \"counter\" argument is not a valid statistic name.");
3379+
return;
3380+
}
3381+
3382+
// The reset flag is always false; the counter is read without being cleared.
3383+
int value = sqlite3_stmt_status(
3384+
stmt->statement_.get(), status_info->sqlite_status_id, false);
3385+
args.GetReturnValue().Set(Integer::New(isolate, value));
3386+
}
3387+
3388+
voidStatementSync::ResetStats(const FunctionCallbackInfo<Value>& args) {
3389+
StatementSync* stmt;
3390+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3391+
Environment* env = Environment::GetCurrent(args);
3392+
THROW_AND_RETURN_ON_BAD_STATE(
3393+
env, stmt->IsFinalized(), "statement has been finalized");
3394+
3395+
// sqlite3_stmt_status() resets a single counter per call, so every exposed
3396+
// counter is visited. The returned value is the pre-reset one and is unused.
3397+
// SQLITE_STMTSTATUS_MEMUSED is skipped: it reports current memory usage
3398+
// rather than an accumulated counter, and SQLite ignores the reset flag for
3399+
// it.
3400+
for (constauto& info : kStatusMapping) {
3401+
if (info.sqlite_status_id == SQLITE_STMTSTATUS_MEMUSED) {
3402+
continue;
3403+
}
3404+
sqlite3_stmt_status(stmt->statement_.get(), info.sqlite_status_id, true);
3405+
}
3406+
3407+
// The column name cache is keyed on SQLITE_STMTSTATUS_REPREPARE, which was
3408+
// just zeroed. Without invalidating, a later re-prepare can make the counter
3409+
// match the cached generation again and the stale names would be reused.
3410+
stmt->InvalidateColumnNameCache();
3411+
}
3412+
33533413
voidStatementSync::SetAllowBareNamedParameters(
33543414
const FunctionCallbackInfo<Value>& args) {
33553415
StatementSync* stmt;
@@ -3775,6 +3835,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
37753835
tmpl,
37763836
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
37773837
StatementSync::ExpandedSQLGetter);
3838+
SetProtoMethodNoSideEffect(isolate, tmpl, "stat", StatementSync::Stat);
3839+
SetProtoMethod(isolate, tmpl, "resetStats", StatementSync::ResetStats);
37783840
SetProtoMethod(isolate,
37793841
tmpl,
37803842
"setAllowBareNamedParameters",

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ static_assert(
5252
CheckLimitIndices(),
5353
"Each kLimitMapping entry's sqlite_limit_id must match its index");
5454

55+
// Mapping from JavaScript counter names to SQLite statement status constants
56+
structStatusInfo {
57+
std::string_view js_name;
58+
int sqlite_status_id;
59+
};
60+
61+
// SQLITE_STMTSTATUS_FILTER_HIT and SQLITE_STMTSTATUS_FILTER_MISS require
62+
// SQLite >= 3.38.0. Older shared-library builds omit the two counters.
63+
#if SQLITE_VERSION_NUMBER >= 3038000
64+
#defineNODE_SQLITE_HAS_FILTER_STATUS1
65+
#endif
66+
67+
inlineconstexprautokStatusMapping = std::to_array<StatusInfo>({
68+
{"fullscanStep", SQLITE_STMTSTATUS_FULLSCAN_STEP},
69+
{"sort", SQLITE_STMTSTATUS_SORT},
70+
{"autoindex", SQLITE_STMTSTATUS_AUTOINDEX},
71+
{"vmStep", SQLITE_STMTSTATUS_VM_STEP},
72+
{"reprepare", SQLITE_STMTSTATUS_REPREPARE},
73+
{"run", SQLITE_STMTSTATUS_RUN},
74+
#ifdef NODE_SQLITE_HAS_FILTER_STATUS
75+
{"filterMiss", SQLITE_STMTSTATUS_FILTER_MISS},
76+
{"filterHit", SQLITE_STMTSTATUS_FILTER_HIT},
77+
#endif
78+
{"memused", SQLITE_STMTSTATUS_MEMUSED},
79+
});
80+
5581
classDatabaseOpenConfiguration {
5682
public:
5783
explicitDatabaseOpenConfiguration(std::string&& location)
@@ -284,6 +310,8 @@ class StatementSync : public BaseObject {
284310
staticvoidSourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
285311
staticvoidExpandedSQLGetter(
286312
const v8::FunctionCallbackInfo<v8::Value>& args);
313+
staticvoidStat(const v8::FunctionCallbackInfo<v8::Value>& args);
314+
staticvoidResetStats(const v8::FunctionCallbackInfo<v8::Value>& args);
287315
staticvoidSetAllowBareNamedParameters(
288316
const v8::FunctionCallbackInfo<v8::Value>& args);
289317
staticvoidSetAllowUnknownNamedParameters(

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit 72c37b1

Browse files
geeksilva97aduh95
authored andcommitted
sqlite: expose prepared statement statistics
Signed-off-by: geeksilva97 <edigleyssonsilva@gmail.com> PR-URL: #64541 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent e3fda69 commit 72c37b1

4 files changed

Lines changed: 353 additions & 6 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,18 @@ returns an empty iterator. The prepared statement [parameters are bound][] using
11881188
the values in `namedParameters` and `anonymousParameters`. See
11891189
[Binding parameters][].
11901190

1191+
### `statement.resetStats()`
1192+
1193+
<!-- YAML
1194+
added: REPLACEME
1195+
-->
1196+
1197+
Resets every counter reported by [`statement.stat()`][] back to zero, except
1198+
`memused`, which reports current memory usage and cannot be reset. This
1199+
method is a wrapper around [`sqlite3_stmt_status()`][] and is useful for
1200+
measuring a specific workload without the counts accumulated by earlier
1201+
executions of the same prepared statement.
1202+
11911203
### `statement.run([namedParameters][, ...anonymousParameters])`
11921204

11931205
<!-- YAML
@@ -1314,6 +1326,43 @@ added: REPLACEME
13141326
Finalizes the prepared statement. If the prepared statement is already
13151327
finalized, then this is a no-op.
13161328

1329+
### `statement.stat(counter)`
1330+
1331+
<!-- YAML
1332+
added: REPLACEME
1333+
-->
1334+
1335+
*`counter` {string} The name of the counter to read. One of:
1336+
1337+
*`'fullscanStep'` The number of times SQLite has stepped forward in a table
1338+
as part of a full table scan.
1339+
*`'sort'` The number of sort operations that have occurred.
1340+
*`'autoindex'` The number of rows inserted into transient indices that were
1341+
created automatically to help joins run faster.
1342+
*`'vmStep'` The number of virtual machine operations executed by the
1343+
prepared statement.
1344+
*`'reprepare'` The number of times the statement has been automatically
1345+
reprepared due to schema changes or changes to bound parameters.
1346+
*`'run'` The number of execution cycles started by the prepared statement.
1347+
*`'filterMiss'` The number of times the Bloom filter returned a result that
1348+
required the join step to be processed as normal.
1349+
*`'filterHit'` The number of times a join step was bypassed because a Bloom
1350+
filter returned not-found.
1351+
*`'memused'` The approximate number of bytes of heap memory used to store
1352+
the prepared statement.
1353+
1354+
* Returns: {number} The current value of the requested counter.
1355+
1356+
Returns one of the runtime counters that SQLite tracks for this prepared
1357+
statement. This method is a wrapper around [`sqlite3_stmt_status()`][] and does
1358+
not reset the counter. Asserting that a statement does not perform a full table
1359+
scan (`statement.stat('fullscanStep') === 0`) is a useful check to guard
1360+
against degenerate performance.
1361+
1362+
The `'filterMiss'` and `'filterHit'` counters require SQLite 3.38.0 or later.
1363+
Builds linked against an older SQLite with `--shared-sqlite` do not expose them,
1364+
and passing either name throws `ERR_INVALID_ARG_VALUE`.
1365+
13171366
## Class: `SQLTagStore`
13181367

13191368
<!-- YAML
@@ -1832,6 +1881,7 @@ callback function to indicate what type of operation is being authorized.
18321881
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
18331882
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
18341883
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
1884+
[`sqlite3_stmt_status()`]: https://www.sqlite.org/c3ref/stmt_status.html
18351885
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
18361886
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
18371887
[`sqlite3session_changeset()`]: https://www.sqlite.org/session/sqlite3session_changeset.html
@@ -1840,6 +1890,7 @@ callback function to indicate what type of operation is being authorized.
18401890
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
18411891
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
18421892
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
1893+
[`statement.stat()`]: #statementstatcounter
18431894
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
18441895
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
18451896
[data types]: https://www.sqlite.org/datatype3.html

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ Local<DictionaryTemplate> getLazyIterTemplate(Environment* env) {
177177
}
178178
} // namespace
179179

180-
// Helper function to find limit info from JS property name
181-
staticconstexprconst LimitInfo* GetLimitInfoFromName(std::string_view name) {
182-
for (constauto& info : kLimitMapping) {
180+
// Helper function to look up a mapping entry by its JS-facing name
181+
template <typename T, size_t N>
182+
staticconstexprconst T* FindByJsName(const std::array<T, N>& mapping,
183+
std::string_view name) {
184+
for (constauto& info : mapping) {
183185
if (name == info.js_name) {
184186
return &info;
185187
}
@@ -793,7 +795,8 @@ Intercepted DatabaseSyncLimits::LimitsGetter(
793795
Isolate* isolate = env->isolate();
794796

795797
Utf8Value prop_name(isolate, property);
796-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
798+
const LimitInfo* limit_info =
799+
FindByJsName(kLimitMapping, prop_name.ToStringView());
797800

798801
if (limit_info == nullptr) {
799802
return Intercepted::kNo; // Unknown property, let default handling occur
@@ -825,7 +828,8 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
825828
Isolate* isolate = env->isolate();
826829

827830
Utf8Value prop_name(isolate, property);
828-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
831+
const LimitInfo* limit_info =
832+
FindByJsName(kLimitMapping, prop_name.ToStringView());
829833

830834
if (limit_info == nullptr) {
831835
return Intercepted::kNo;
@@ -873,7 +877,8 @@ Intercepted DatabaseSyncLimits::LimitsQuery(
873877

874878
Isolate* isolate = info.GetIsolate();
875879
Utf8Value prop_name(isolate, property);
876-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
880+
const LimitInfo* limit_info =
881+
FindByJsName(kLimitMapping, prop_name.ToStringView());
877882

878883
if (!limit_info) {
879884
return Intercepted::kNo;
@@ -2694,6 +2699,7 @@ void StatementSync::Finalize() {
26942699

26952700
voidStatementSync::InvalidateColumnNameCache() {
26962701
cached_column_names_.clear();
2702+
cached_column_names_reprepare_count_ = -1;
26972703
}
26982704

26992705
inlineboolStatementSync::IsFinalized() {
@@ -3350,6 +3356,60 @@ void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo<Value>& args) {
33503356
args.GetReturnValue().Set(result);
33513357
}
33523358

3359+
voidStatementSync::Stat(const FunctionCallbackInfo<Value>& args) {
3360+
StatementSync* stmt;
3361+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3362+
Environment* env = Environment::GetCurrent(args);
3363+
THROW_AND_RETURN_ON_BAD_STATE(
3364+
env, stmt->IsFinalized(), "statement has been finalized");
3365+
Isolate* isolate = env->isolate();
3366+
3367+
if (!args[0]->IsString()) {
3368+
THROW_ERR_INVALID_ARG_TYPE(isolate,
3369+
"The \"counter\" argument must be a string.");
3370+
return;
3371+
}
3372+
3373+
Utf8Value counter(isolate, args[0].As<String>());
3374+
const StatusInfo* status_info =
3375+
FindByJsName(kStatusMapping, counter.ToStringView());
3376+
if (status_info == nullptr) {
3377+
THROW_ERR_INVALID_ARG_VALUE(
3378+
isolate, "The \"counter\" argument is not a valid statistic name.");
3379+
return;
3380+
}
3381+
3382+
// The reset flag is always false; the counter is read without being cleared.
3383+
int value = sqlite3_stmt_status(
3384+
stmt->statement_.get(), status_info->sqlite_status_id, false);
3385+
args.GetReturnValue().Set(Integer::New(isolate, value));
3386+
}
3387+
3388+
voidStatementSync::ResetStats(const FunctionCallbackInfo<Value>& args) {
3389+
StatementSync* stmt;
3390+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3391+
Environment* env = Environment::GetCurrent(args);
3392+
THROW_AND_RETURN_ON_BAD_STATE(
3393+
env, stmt->IsFinalized(), "statement has been finalized");
3394+
3395+
// sqlite3_stmt_status() resets a single counter per call, so every exposed
3396+
// counter is visited. The returned value is the pre-reset one and is unused.
3397+
// SQLITE_STMTSTATUS_MEMUSED is skipped: it reports current memory usage
3398+
// rather than an accumulated counter, and SQLite ignores the reset flag for
3399+
// it.
3400+
for (constauto& info : kStatusMapping) {
3401+
if (info.sqlite_status_id == SQLITE_STMTSTATUS_MEMUSED) {
3402+
continue;
3403+
}
3404+
sqlite3_stmt_status(stmt->statement_.get(), info.sqlite_status_id, true);
3405+
}
3406+
3407+
// The column name cache is keyed on SQLITE_STMTSTATUS_REPREPARE, which was
3408+
// just zeroed. Without invalidating, a later re-prepare can make the counter
3409+
// match the cached generation again and the stale names would be reused.
3410+
stmt->InvalidateColumnNameCache();
3411+
}
3412+
33533413
voidStatementSync::SetAllowBareNamedParameters(
33543414
const FunctionCallbackInfo<Value>& args) {
33553415
StatementSync* stmt;
@@ -3775,6 +3835,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
37753835
tmpl,
37763836
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
37773837
StatementSync::ExpandedSQLGetter);
3838+
SetProtoMethodNoSideEffect(isolate, tmpl, "stat", StatementSync::Stat);
3839+
SetProtoMethod(isolate, tmpl, "resetStats", StatementSync::ResetStats);
37783840
SetProtoMethod(isolate,
37793841
tmpl,
37803842
"setAllowBareNamedParameters",

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ static_assert(
5252
CheckLimitIndices(),
5353
"Each kLimitMapping entry's sqlite_limit_id must match its index");
5454

55+
// Mapping from JavaScript counter names to SQLite statement status constants
56+
structStatusInfo {
57+
std::string_view js_name;
58+
int sqlite_status_id;
59+
};
60+
61+
// SQLITE_STMTSTATUS_FILTER_HIT and SQLITE_STMTSTATUS_FILTER_MISS require
62+
// SQLite >= 3.38.0. Older shared-library builds omit the two counters.
63+
#if SQLITE_VERSION_NUMBER >= 3038000
64+
#defineNODE_SQLITE_HAS_FILTER_STATUS1
65+
#endif
66+
67+
inlineconstexprautokStatusMapping = std::to_array<StatusInfo>({
68+
{"fullscanStep", SQLITE_STMTSTATUS_FULLSCAN_STEP},
69+
{"sort", SQLITE_STMTSTATUS_SORT},
70+
{"autoindex", SQLITE_STMTSTATUS_AUTOINDEX},
71+
{"vmStep", SQLITE_STMTSTATUS_VM_STEP},
72+
{"reprepare", SQLITE_STMTSTATUS_REPREPARE},
73+
{"run", SQLITE_STMTSTATUS_RUN},
74+
#ifdef NODE_SQLITE_HAS_FILTER_STATUS
75+
{"filterMiss", SQLITE_STMTSTATUS_FILTER_MISS},
76+
{"filterHit", SQLITE_STMTSTATUS_FILTER_HIT},
77+
#endif
78+
{"memused", SQLITE_STMTSTATUS_MEMUSED},
79+
});
80+
5581
classDatabaseOpenConfiguration {
5682
public:
5783
explicitDatabaseOpenConfiguration(std::string&& location)
@@ -284,6 +310,8 @@ class StatementSync : public BaseObject {
284310
staticvoidSourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
285311
staticvoidExpandedSQLGetter(
286312
const v8::FunctionCallbackInfo<v8::Value>& args);
313+
staticvoidStat(const v8::FunctionCallbackInfo<v8::Value>& args);
314+
staticvoidResetStats(const v8::FunctionCallbackInfo<v8::Value>& args);
287315
staticvoidSetAllowBareNamedParameters(
288316
const v8::FunctionCallbackInfo<v8::Value>& args);
289317
staticvoidSetAllowUnknownNamedParameters(

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 72c37b1

Browse files
geeksilva97aduh95
authored andcommitted
sqlite: expose prepared statement statistics
Signed-off-by: geeksilva97 <edigleyssonsilva@gmail.com> PR-URL: #64541 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent e3fda69 commit 72c37b1

4 files changed

Lines changed: 353 additions & 6 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,18 @@ returns an empty iterator. The prepared statement [parameters are bound][] using
11881188
the values in `namedParameters` and `anonymousParameters`. See
11891189
[Binding parameters][].
11901190

1191+
### `statement.resetStats()`
1192+
1193+
<!-- YAML
1194+
added: REPLACEME
1195+
-->
1196+
1197+
Resets every counter reported by [`statement.stat()`][] back to zero, except
1198+
`memused`, which reports current memory usage and cannot be reset. This
1199+
method is a wrapper around [`sqlite3_stmt_status()`][] and is useful for
1200+
measuring a specific workload without the counts accumulated by earlier
1201+
executions of the same prepared statement.
1202+
11911203
### `statement.run([namedParameters][, ...anonymousParameters])`
11921204

11931205
<!-- YAML
@@ -1314,6 +1326,43 @@ added: REPLACEME
13141326
Finalizes the prepared statement. If the prepared statement is already
13151327
finalized, then this is a no-op.
13161328

1329+
### `statement.stat(counter)`
1330+
1331+
<!-- YAML
1332+
added: REPLACEME
1333+
-->
1334+
1335+
*`counter` {string} The name of the counter to read. One of:
1336+
1337+
*`'fullscanStep'` The number of times SQLite has stepped forward in a table
1338+
as part of a full table scan.
1339+
*`'sort'` The number of sort operations that have occurred.
1340+
*`'autoindex'` The number of rows inserted into transient indices that were
1341+
created automatically to help joins run faster.
1342+
*`'vmStep'` The number of virtual machine operations executed by the
1343+
prepared statement.
1344+
*`'reprepare'` The number of times the statement has been automatically
1345+
reprepared due to schema changes or changes to bound parameters.
1346+
*`'run'` The number of execution cycles started by the prepared statement.
1347+
*`'filterMiss'` The number of times the Bloom filter returned a result that
1348+
required the join step to be processed as normal.
1349+
*`'filterHit'` The number of times a join step was bypassed because a Bloom
1350+
filter returned not-found.
1351+
*`'memused'` The approximate number of bytes of heap memory used to store
1352+
the prepared statement.
1353+
1354+
* Returns: {number} The current value of the requested counter.
1355+
1356+
Returns one of the runtime counters that SQLite tracks for this prepared
1357+
statement. This method is a wrapper around [`sqlite3_stmt_status()`][] and does
1358+
not reset the counter. Asserting that a statement does not perform a full table
1359+
scan (`statement.stat('fullscanStep') === 0`) is a useful check to guard
1360+
against degenerate performance.
1361+
1362+
The `'filterMiss'` and `'filterHit'` counters require SQLite 3.38.0 or later.
1363+
Builds linked against an older SQLite with `--shared-sqlite` do not expose them,
1364+
and passing either name throws `ERR_INVALID_ARG_VALUE`.
1365+
13171366
## Class: `SQLTagStore`
13181367

13191368
<!-- YAML
@@ -1832,6 +1881,7 @@ callback function to indicate what type of operation is being authorized.
18321881
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
18331882
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
18341883
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
1884+
[`sqlite3_stmt_status()`]: https://www.sqlite.org/c3ref/stmt_status.html
18351885
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
18361886
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
18371887
[`sqlite3session_changeset()`]: https://www.sqlite.org/session/sqlite3session_changeset.html
@@ -1840,6 +1890,7 @@ callback function to indicate what type of operation is being authorized.
18401890
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
18411891
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
18421892
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
1893+
[`statement.stat()`]: #statementstatcounter
18431894
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
18441895
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
18451896
[data types]: https://www.sqlite.org/datatype3.html

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ Local<DictionaryTemplate> getLazyIterTemplate(Environment* env) {
177177
}
178178
} // namespace
179179

180-
// Helper function to find limit info from JS property name
181-
staticconstexprconst LimitInfo* GetLimitInfoFromName(std::string_view name) {
182-
for (constauto& info : kLimitMapping) {
180+
// Helper function to look up a mapping entry by its JS-facing name
181+
template <typename T, size_t N>
182+
staticconstexprconst T* FindByJsName(const std::array<T, N>& mapping,
183+
std::string_view name) {
184+
for (constauto& info : mapping) {
183185
if (name == info.js_name) {
184186
return &info;
185187
}
@@ -793,7 +795,8 @@ Intercepted DatabaseSyncLimits::LimitsGetter(
793795
Isolate* isolate = env->isolate();
794796

795797
Utf8Value prop_name(isolate, property);
796-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
798+
const LimitInfo* limit_info =
799+
FindByJsName(kLimitMapping, prop_name.ToStringView());
797800

798801
if (limit_info == nullptr) {
799802
return Intercepted::kNo; // Unknown property, let default handling occur
@@ -825,7 +828,8 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
825828
Isolate* isolate = env->isolate();
826829

827830
Utf8Value prop_name(isolate, property);
828-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
831+
const LimitInfo* limit_info =
832+
FindByJsName(kLimitMapping, prop_name.ToStringView());
829833

830834
if (limit_info == nullptr) {
831835
return Intercepted::kNo;
@@ -873,7 +877,8 @@ Intercepted DatabaseSyncLimits::LimitsQuery(
873877

874878
Isolate* isolate = info.GetIsolate();
875879
Utf8Value prop_name(isolate, property);
876-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
880+
const LimitInfo* limit_info =
881+
FindByJsName(kLimitMapping, prop_name.ToStringView());
877882

878883
if (!limit_info) {
879884
return Intercepted::kNo;
@@ -2694,6 +2699,7 @@ void StatementSync::Finalize() {
26942699

26952700
voidStatementSync::InvalidateColumnNameCache() {
26962701
cached_column_names_.clear();
2702+
cached_column_names_reprepare_count_ = -1;
26972703
}
26982704

26992705
inlineboolStatementSync::IsFinalized() {
@@ -3350,6 +3356,60 @@ void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo<Value>& args) {
33503356
args.GetReturnValue().Set(result);
33513357
}
33523358

3359+
voidStatementSync::Stat(const FunctionCallbackInfo<Value>& args) {
3360+
StatementSync* stmt;
3361+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3362+
Environment* env = Environment::GetCurrent(args);
3363+
THROW_AND_RETURN_ON_BAD_STATE(
3364+
env, stmt->IsFinalized(), "statement has been finalized");
3365+
Isolate* isolate = env->isolate();
3366+
3367+
if (!args[0]->IsString()) {
3368+
THROW_ERR_INVALID_ARG_TYPE(isolate,
3369+
"The \"counter\" argument must be a string.");
3370+
return;
3371+
}
3372+
3373+
Utf8Value counter(isolate, args[0].As<String>());
3374+
const StatusInfo* status_info =
3375+
FindByJsName(kStatusMapping, counter.ToStringView());
3376+
if (status_info == nullptr) {
3377+
THROW_ERR_INVALID_ARG_VALUE(
3378+
isolate, "The \"counter\" argument is not a valid statistic name.");
3379+
return;
3380+
}
3381+
3382+
// The reset flag is always false; the counter is read without being cleared.
3383+
int value = sqlite3_stmt_status(
3384+
stmt->statement_.get(), status_info->sqlite_status_id, false);
3385+
args.GetReturnValue().Set(Integer::New(isolate, value));
3386+
}
3387+
3388+
voidStatementSync::ResetStats(const FunctionCallbackInfo<Value>& args) {
3389+
StatementSync* stmt;
3390+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3391+
Environment* env = Environment::GetCurrent(args);
3392+
THROW_AND_RETURN_ON_BAD_STATE(
3393+
env, stmt->IsFinalized(), "statement has been finalized");
3394+
3395+
// sqlite3_stmt_status() resets a single counter per call, so every exposed
3396+
// counter is visited. The returned value is the pre-reset one and is unused.
3397+
// SQLITE_STMTSTATUS_MEMUSED is skipped: it reports current memory usage
3398+
// rather than an accumulated counter, and SQLite ignores the reset flag for
3399+
// it.
3400+
for (constauto& info : kStatusMapping) {
3401+
if (info.sqlite_status_id == SQLITE_STMTSTATUS_MEMUSED) {
3402+
continue;
3403+
}
3404+
sqlite3_stmt_status(stmt->statement_.get(), info.sqlite_status_id, true);
3405+
}
3406+
3407+
// The column name cache is keyed on SQLITE_STMTSTATUS_REPREPARE, which was
3408+
// just zeroed. Without invalidating, a later re-prepare can make the counter
3409+
// match the cached generation again and the stale names would be reused.
3410+
stmt->InvalidateColumnNameCache();
3411+
}
3412+
33533413
voidStatementSync::SetAllowBareNamedParameters(
33543414
const FunctionCallbackInfo<Value>& args) {
33553415
StatementSync* stmt;
@@ -3775,6 +3835,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
37753835
tmpl,
37763836
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
37773837
StatementSync::ExpandedSQLGetter);
3838+
SetProtoMethodNoSideEffect(isolate, tmpl, "stat", StatementSync::Stat);
3839+
SetProtoMethod(isolate, tmpl, "resetStats", StatementSync::ResetStats);
37783840
SetProtoMethod(isolate,
37793841
tmpl,
37803842
"setAllowBareNamedParameters",

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ static_assert(
5252
CheckLimitIndices(),
5353
"Each kLimitMapping entry's sqlite_limit_id must match its index");
5454

55+
// Mapping from JavaScript counter names to SQLite statement status constants
56+
structStatusInfo {
57+
std::string_view js_name;
58+
int sqlite_status_id;
59+
};
60+
61+
// SQLITE_STMTSTATUS_FILTER_HIT and SQLITE_STMTSTATUS_FILTER_MISS require
62+
// SQLite >= 3.38.0. Older shared-library builds omit the two counters.
63+
#if SQLITE_VERSION_NUMBER >= 3038000
64+
#defineNODE_SQLITE_HAS_FILTER_STATUS1
65+
#endif
66+
67+
inlineconstexprautokStatusMapping = std::to_array<StatusInfo>({
68+
{"fullscanStep", SQLITE_STMTSTATUS_FULLSCAN_STEP},
69+
{"sort", SQLITE_STMTSTATUS_SORT},
70+
{"autoindex", SQLITE_STMTSTATUS_AUTOINDEX},
71+
{"vmStep", SQLITE_STMTSTATUS_VM_STEP},
72+
{"reprepare", SQLITE_STMTSTATUS_REPREPARE},
73+
{"run", SQLITE_STMTSTATUS_RUN},
74+
#ifdef NODE_SQLITE_HAS_FILTER_STATUS
75+
{"filterMiss", SQLITE_STMTSTATUS_FILTER_MISS},
76+
{"filterHit", SQLITE_STMTSTATUS_FILTER_HIT},
77+
#endif
78+
{"memused", SQLITE_STMTSTATUS_MEMUSED},
79+
});
80+
5581
classDatabaseOpenConfiguration {
5682
public:
5783
explicitDatabaseOpenConfiguration(std::string&& location)
@@ -284,6 +310,8 @@ class StatementSync : public BaseObject {
284310
staticvoidSourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
285311
staticvoidExpandedSQLGetter(
286312
const v8::FunctionCallbackInfo<v8::Value>& args);
313+
staticvoidStat(const v8::FunctionCallbackInfo<v8::Value>& args);
314+
staticvoidResetStats(const v8::FunctionCallbackInfo<v8::Value>& args);
287315
staticvoidSetAllowBareNamedParameters(
288316
const v8::FunctionCallbackInfo<v8::Value>& args);
289317
staticvoidSetAllowUnknownNamedParameters(

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 72c37b1

Browse files
geeksilva97aduh95
authored andcommitted
sqlite: expose prepared statement statistics
Signed-off-by: geeksilva97 <edigleyssonsilva@gmail.com> PR-URL: #64541 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent e3fda69 commit 72c37b1

4 files changed

Lines changed: 353 additions & 6 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,18 @@ returns an empty iterator. The prepared statement [parameters are bound][] using
11881188
the values in `namedParameters` and `anonymousParameters`. See
11891189
[Binding parameters][].
11901190

1191+
### `statement.resetStats()`
1192+
1193+
<!-- YAML
1194+
added: REPLACEME
1195+
-->
1196+
1197+
Resets every counter reported by [`statement.stat()`][] back to zero, except
1198+
`memused`, which reports current memory usage and cannot be reset. This
1199+
method is a wrapper around [`sqlite3_stmt_status()`][] and is useful for
1200+
measuring a specific workload without the counts accumulated by earlier
1201+
executions of the same prepared statement.
1202+
11911203
### `statement.run([namedParameters][, ...anonymousParameters])`
11921204

11931205
<!-- YAML
@@ -1314,6 +1326,43 @@ added: REPLACEME
13141326
Finalizes the prepared statement. If the prepared statement is already
13151327
finalized, then this is a no-op.
13161328

1329+
### `statement.stat(counter)`
1330+
1331+
<!-- YAML
1332+
added: REPLACEME
1333+
-->
1334+
1335+
*`counter` {string} The name of the counter to read. One of:
1336+
1337+
*`'fullscanStep'` The number of times SQLite has stepped forward in a table
1338+
as part of a full table scan.
1339+
*`'sort'` The number of sort operations that have occurred.
1340+
*`'autoindex'` The number of rows inserted into transient indices that were
1341+
created automatically to help joins run faster.
1342+
*`'vmStep'` The number of virtual machine operations executed by the
1343+
prepared statement.
1344+
*`'reprepare'` The number of times the statement has been automatically
1345+
reprepared due to schema changes or changes to bound parameters.
1346+
*`'run'` The number of execution cycles started by the prepared statement.
1347+
*`'filterMiss'` The number of times the Bloom filter returned a result that
1348+
required the join step to be processed as normal.
1349+
*`'filterHit'` The number of times a join step was bypassed because a Bloom
1350+
filter returned not-found.
1351+
*`'memused'` The approximate number of bytes of heap memory used to store
1352+
the prepared statement.
1353+
1354+
* Returns: {number} The current value of the requested counter.
1355+
1356+
Returns one of the runtime counters that SQLite tracks for this prepared
1357+
statement. This method is a wrapper around [`sqlite3_stmt_status()`][] and does
1358+
not reset the counter. Asserting that a statement does not perform a full table
1359+
scan (`statement.stat('fullscanStep') === 0`) is a useful check to guard
1360+
against degenerate performance.
1361+
1362+
The `'filterMiss'` and `'filterHit'` counters require SQLite 3.38.0 or later.
1363+
Builds linked against an older SQLite with `--shared-sqlite` do not expose them,
1364+
and passing either name throws `ERR_INVALID_ARG_VALUE`.
1365+
13171366
## Class: `SQLTagStore`
13181367

13191368
<!-- YAML
@@ -1832,6 +1881,7 @@ callback function to indicate what type of operation is being authorized.
18321881
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
18331882
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
18341883
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
1884+
[`sqlite3_stmt_status()`]: https://www.sqlite.org/c3ref/stmt_status.html
18351885
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
18361886
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
18371887
[`sqlite3session_changeset()`]: https://www.sqlite.org/session/sqlite3session_changeset.html
@@ -1840,6 +1890,7 @@ callback function to indicate what type of operation is being authorized.
18401890
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
18411891
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
18421892
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
1893+
[`statement.stat()`]: #statementstatcounter
18431894
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
18441895
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
18451896
[data types]: https://www.sqlite.org/datatype3.html

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ Local<DictionaryTemplate> getLazyIterTemplate(Environment* env) {
177177
}
178178
} // namespace
179179

180-
// Helper function to find limit info from JS property name
181-
staticconstexprconst LimitInfo* GetLimitInfoFromName(std::string_view name) {
182-
for (constauto& info : kLimitMapping) {
180+
// Helper function to look up a mapping entry by its JS-facing name
181+
template <typename T, size_t N>
182+
staticconstexprconst T* FindByJsName(const std::array<T, N>& mapping,
183+
std::string_view name) {
184+
for (constauto& info : mapping) {
183185
if (name == info.js_name) {
184186
return &info;
185187
}
@@ -793,7 +795,8 @@ Intercepted DatabaseSyncLimits::LimitsGetter(
793795
Isolate* isolate = env->isolate();
794796

795797
Utf8Value prop_name(isolate, property);
796-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
798+
const LimitInfo* limit_info =
799+
FindByJsName(kLimitMapping, prop_name.ToStringView());
797800

798801
if (limit_info == nullptr) {
799802
return Intercepted::kNo; // Unknown property, let default handling occur
@@ -825,7 +828,8 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
825828
Isolate* isolate = env->isolate();
826829

827830
Utf8Value prop_name(isolate, property);
828-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
831+
const LimitInfo* limit_info =
832+
FindByJsName(kLimitMapping, prop_name.ToStringView());
829833

830834
if (limit_info == nullptr) {
831835
return Intercepted::kNo;
@@ -873,7 +877,8 @@ Intercepted DatabaseSyncLimits::LimitsQuery(
873877

874878
Isolate* isolate = info.GetIsolate();
875879
Utf8Value prop_name(isolate, property);
876-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
880+
const LimitInfo* limit_info =
881+
FindByJsName(kLimitMapping, prop_name.ToStringView());
877882

878883
if (!limit_info) {
879884
return Intercepted::kNo;
@@ -2694,6 +2699,7 @@ void StatementSync::Finalize() {
26942699

26952700
voidStatementSync::InvalidateColumnNameCache() {
26962701
cached_column_names_.clear();
2702+
cached_column_names_reprepare_count_ = -1;
26972703
}
26982704

26992705
inlineboolStatementSync::IsFinalized() {
@@ -3350,6 +3356,60 @@ void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo<Value>& args) {
33503356
args.GetReturnValue().Set(result);
33513357
}
33523358

3359+
voidStatementSync::Stat(const FunctionCallbackInfo<Value>& args) {
3360+
StatementSync* stmt;
3361+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3362+
Environment* env = Environment::GetCurrent(args);
3363+
THROW_AND_RETURN_ON_BAD_STATE(
3364+
env, stmt->IsFinalized(), "statement has been finalized");
3365+
Isolate* isolate = env->isolate();
3366+
3367+
if (!args[0]->IsString()) {
3368+
THROW_ERR_INVALID_ARG_TYPE(isolate,
3369+
"The \"counter\" argument must be a string.");
3370+
return;
3371+
}
3372+
3373+
Utf8Value counter(isolate, args[0].As<String>());
3374+
const StatusInfo* status_info =
3375+
FindByJsName(kStatusMapping, counter.ToStringView());
3376+
if (status_info == nullptr) {
3377+
THROW_ERR_INVALID_ARG_VALUE(
3378+
isolate, "The \"counter\" argument is not a valid statistic name.");
3379+
return;
3380+
}
3381+
3382+
// The reset flag is always false; the counter is read without being cleared.
3383+
int value = sqlite3_stmt_status(
3384+
stmt->statement_.get(), status_info->sqlite_status_id, false);
3385+
args.GetReturnValue().Set(Integer::New(isolate, value));
3386+
}
3387+
3388+
voidStatementSync::ResetStats(const FunctionCallbackInfo<Value>& args) {
3389+
StatementSync* stmt;
3390+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3391+
Environment* env = Environment::GetCurrent(args);
3392+
THROW_AND_RETURN_ON_BAD_STATE(
3393+
env, stmt->IsFinalized(), "statement has been finalized");
3394+
3395+
// sqlite3_stmt_status() resets a single counter per call, so every exposed
3396+
// counter is visited. The returned value is the pre-reset one and is unused.
3397+
// SQLITE_STMTSTATUS_MEMUSED is skipped: it reports current memory usage
3398+
// rather than an accumulated counter, and SQLite ignores the reset flag for
3399+
// it.
3400+
for (constauto& info : kStatusMapping) {
3401+
if (info.sqlite_status_id == SQLITE_STMTSTATUS_MEMUSED) {
3402+
continue;
3403+
}
3404+
sqlite3_stmt_status(stmt->statement_.get(), info.sqlite_status_id, true);
3405+
}
3406+
3407+
// The column name cache is keyed on SQLITE_STMTSTATUS_REPREPARE, which was
3408+
// just zeroed. Without invalidating, a later re-prepare can make the counter
3409+
// match the cached generation again and the stale names would be reused.
3410+
stmt->InvalidateColumnNameCache();
3411+
}
3412+
33533413
voidStatementSync::SetAllowBareNamedParameters(
33543414
const FunctionCallbackInfo<Value>& args) {
33553415
StatementSync* stmt;
@@ -3775,6 +3835,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
37753835
tmpl,
37763836
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
37773837
StatementSync::ExpandedSQLGetter);
3838+
SetProtoMethodNoSideEffect(isolate, tmpl, "stat", StatementSync::Stat);
3839+
SetProtoMethod(isolate, tmpl, "resetStats", StatementSync::ResetStats);
37783840
SetProtoMethod(isolate,
37793841
tmpl,
37803842
"setAllowBareNamedParameters",

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ static_assert(
5252
CheckLimitIndices(),
5353
"Each kLimitMapping entry's sqlite_limit_id must match its index");
5454

55+
// Mapping from JavaScript counter names to SQLite statement status constants
56+
structStatusInfo {
57+
std::string_view js_name;
58+
int sqlite_status_id;
59+
};
60+
61+
// SQLITE_STMTSTATUS_FILTER_HIT and SQLITE_STMTSTATUS_FILTER_MISS require
62+
// SQLite >= 3.38.0. Older shared-library builds omit the two counters.
63+
#if SQLITE_VERSION_NUMBER >= 3038000
64+
#defineNODE_SQLITE_HAS_FILTER_STATUS1
65+
#endif
66+
67+
inlineconstexprautokStatusMapping = std::to_array<StatusInfo>({
68+
{"fullscanStep", SQLITE_STMTSTATUS_FULLSCAN_STEP},
69+
{"sort", SQLITE_STMTSTATUS_SORT},
70+
{"autoindex", SQLITE_STMTSTATUS_AUTOINDEX},
71+
{"vmStep", SQLITE_STMTSTATUS_VM_STEP},
72+
{"reprepare", SQLITE_STMTSTATUS_REPREPARE},
73+
{"run", SQLITE_STMTSTATUS_RUN},
74+
#ifdef NODE_SQLITE_HAS_FILTER_STATUS
75+
{"filterMiss", SQLITE_STMTSTATUS_FILTER_MISS},
76+
{"filterHit", SQLITE_STMTSTATUS_FILTER_HIT},
77+
#endif
78+
{"memused", SQLITE_STMTSTATUS_MEMUSED},
79+
});
80+
5581
classDatabaseOpenConfiguration {
5682
public:
5783
explicitDatabaseOpenConfiguration(std::string&& location)
@@ -284,6 +310,8 @@ class StatementSync : public BaseObject {
284310
staticvoidSourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
285311
staticvoidExpandedSQLGetter(
286312
const v8::FunctionCallbackInfo<v8::Value>& args);
313+
staticvoidStat(const v8::FunctionCallbackInfo<v8::Value>& args);
314+
staticvoidResetStats(const v8::FunctionCallbackInfo<v8::Value>& args);
287315
staticvoidSetAllowBareNamedParameters(
288316
const v8::FunctionCallbackInfo<v8::Value>& args);
289317
staticvoidSetAllowUnknownNamedParameters(

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit 72c37b1

Browse files
geeksilva97aduh95
authored andcommitted
sqlite: expose prepared statement statistics
Signed-off-by: geeksilva97 <edigleyssonsilva@gmail.com> PR-URL: #64541 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent e3fda69 commit 72c37b1

4 files changed

Lines changed: 353 additions & 6 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,18 @@ returns an empty iterator. The prepared statement [parameters are bound][] using
11881188
the values in `namedParameters` and `anonymousParameters`. See
11891189
[Binding parameters][].
11901190

1191+
### `statement.resetStats()`
1192+
1193+
<!-- YAML
1194+
added: REPLACEME
1195+
-->
1196+
1197+
Resets every counter reported by [`statement.stat()`][] back to zero, except
1198+
`memused`, which reports current memory usage and cannot be reset. This
1199+
method is a wrapper around [`sqlite3_stmt_status()`][] and is useful for
1200+
measuring a specific workload without the counts accumulated by earlier
1201+
executions of the same prepared statement.
1202+
11911203
### `statement.run([namedParameters][, ...anonymousParameters])`
11921204

11931205
<!-- YAML
@@ -1314,6 +1326,43 @@ added: REPLACEME
13141326
Finalizes the prepared statement. If the prepared statement is already
13151327
finalized, then this is a no-op.
13161328

1329+
### `statement.stat(counter)`
1330+
1331+
<!-- YAML
1332+
added: REPLACEME
1333+
-->
1334+
1335+
*`counter` {string} The name of the counter to read. One of:
1336+
1337+
*`'fullscanStep'` The number of times SQLite has stepped forward in a table
1338+
as part of a full table scan.
1339+
*`'sort'` The number of sort operations that have occurred.
1340+
*`'autoindex'` The number of rows inserted into transient indices that were
1341+
created automatically to help joins run faster.
1342+
*`'vmStep'` The number of virtual machine operations executed by the
1343+
prepared statement.
1344+
*`'reprepare'` The number of times the statement has been automatically
1345+
reprepared due to schema changes or changes to bound parameters.
1346+
*`'run'` The number of execution cycles started by the prepared statement.
1347+
*`'filterMiss'` The number of times the Bloom filter returned a result that
1348+
required the join step to be processed as normal.
1349+
*`'filterHit'` The number of times a join step was bypassed because a Bloom
1350+
filter returned not-found.
1351+
*`'memused'` The approximate number of bytes of heap memory used to store
1352+
the prepared statement.
1353+
1354+
* Returns: {number} The current value of the requested counter.
1355+
1356+
Returns one of the runtime counters that SQLite tracks for this prepared
1357+
statement. This method is a wrapper around [`sqlite3_stmt_status()`][] and does
1358+
not reset the counter. Asserting that a statement does not perform a full table
1359+
scan (`statement.stat('fullscanStep') === 0`) is a useful check to guard
1360+
against degenerate performance.
1361+
1362+
The `'filterMiss'` and `'filterHit'` counters require SQLite 3.38.0 or later.
1363+
Builds linked against an older SQLite with `--shared-sqlite` do not expose them,
1364+
and passing either name throws `ERR_INVALID_ARG_VALUE`.
1365+
13171366
## Class: `SQLTagStore`
13181367

13191368
<!-- YAML
@@ -1832,6 +1881,7 @@ callback function to indicate what type of operation is being authorized.
18321881
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
18331882
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
18341883
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
1884+
[`sqlite3_stmt_status()`]: https://www.sqlite.org/c3ref/stmt_status.html
18351885
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
18361886
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
18371887
[`sqlite3session_changeset()`]: https://www.sqlite.org/session/sqlite3session_changeset.html
@@ -1840,6 +1890,7 @@ callback function to indicate what type of operation is being authorized.
18401890
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
18411891
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
18421892
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
1893+
[`statement.stat()`]: #statementstatcounter
18431894
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
18441895
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
18451896
[data types]: https://www.sqlite.org/datatype3.html

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ Local<DictionaryTemplate> getLazyIterTemplate(Environment* env) {
177177
}
178178
} // namespace
179179

180-
// Helper function to find limit info from JS property name
181-
staticconstexprconst LimitInfo* GetLimitInfoFromName(std::string_view name) {
182-
for (constauto& info : kLimitMapping) {
180+
// Helper function to look up a mapping entry by its JS-facing name
181+
template <typename T, size_t N>
182+
staticconstexprconst T* FindByJsName(const std::array<T, N>& mapping,
183+
std::string_view name) {
184+
for (constauto& info : mapping) {
183185
if (name == info.js_name) {
184186
return &info;
185187
}
@@ -793,7 +795,8 @@ Intercepted DatabaseSyncLimits::LimitsGetter(
793795
Isolate* isolate = env->isolate();
794796

795797
Utf8Value prop_name(isolate, property);
796-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
798+
const LimitInfo* limit_info =
799+
FindByJsName(kLimitMapping, prop_name.ToStringView());
797800

798801
if (limit_info == nullptr) {
799802
return Intercepted::kNo; // Unknown property, let default handling occur
@@ -825,7 +828,8 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
825828
Isolate* isolate = env->isolate();
826829

827830
Utf8Value prop_name(isolate, property);
828-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
831+
const LimitInfo* limit_info =
832+
FindByJsName(kLimitMapping, prop_name.ToStringView());
829833

830834
if (limit_info == nullptr) {
831835
return Intercepted::kNo;
@@ -873,7 +877,8 @@ Intercepted DatabaseSyncLimits::LimitsQuery(
873877

874878
Isolate* isolate = info.GetIsolate();
875879
Utf8Value prop_name(isolate, property);
876-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
880+
const LimitInfo* limit_info =
881+
FindByJsName(kLimitMapping, prop_name.ToStringView());
877882

878883
if (!limit_info) {
879884
return Intercepted::kNo;
@@ -2694,6 +2699,7 @@ void StatementSync::Finalize() {
26942699

26952700
voidStatementSync::InvalidateColumnNameCache() {
26962701
cached_column_names_.clear();
2702+
cached_column_names_reprepare_count_ = -1;
26972703
}
26982704

26992705
inlineboolStatementSync::IsFinalized() {
@@ -3350,6 +3356,60 @@ void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo<Value>& args) {
33503356
args.GetReturnValue().Set(result);
33513357
}
33523358

3359+
voidStatementSync::Stat(const FunctionCallbackInfo<Value>& args) {
3360+
StatementSync* stmt;
3361+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3362+
Environment* env = Environment::GetCurrent(args);
3363+
THROW_AND_RETURN_ON_BAD_STATE(
3364+
env, stmt->IsFinalized(), "statement has been finalized");
3365+
Isolate* isolate = env->isolate();
3366+
3367+
if (!args[0]->IsString()) {
3368+
THROW_ERR_INVALID_ARG_TYPE(isolate,
3369+
"The \"counter\" argument must be a string.");
3370+
return;
3371+
}
3372+
3373+
Utf8Value counter(isolate, args[0].As<String>());
3374+
const StatusInfo* status_info =
3375+
FindByJsName(kStatusMapping, counter.ToStringView());
3376+
if (status_info == nullptr) {
3377+
THROW_ERR_INVALID_ARG_VALUE(
3378+
isolate, "The \"counter\" argument is not a valid statistic name.");
3379+
return;
3380+
}
3381+
3382+
// The reset flag is always false; the counter is read without being cleared.
3383+
int value = sqlite3_stmt_status(
3384+
stmt->statement_.get(), status_info->sqlite_status_id, false);
3385+
args.GetReturnValue().Set(Integer::New(isolate, value));
3386+
}
3387+
3388+
voidStatementSync::ResetStats(const FunctionCallbackInfo<Value>& args) {
3389+
StatementSync* stmt;
3390+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3391+
Environment* env = Environment::GetCurrent(args);
3392+
THROW_AND_RETURN_ON_BAD_STATE(
3393+
env, stmt->IsFinalized(), "statement has been finalized");
3394+
3395+
// sqlite3_stmt_status() resets a single counter per call, so every exposed
3396+
// counter is visited. The returned value is the pre-reset one and is unused.
3397+
// SQLITE_STMTSTATUS_MEMUSED is skipped: it reports current memory usage
3398+
// rather than an accumulated counter, and SQLite ignores the reset flag for
3399+
// it.
3400+
for (constauto& info : kStatusMapping) {
3401+
if (info.sqlite_status_id == SQLITE_STMTSTATUS_MEMUSED) {
3402+
continue;
3403+
}
3404+
sqlite3_stmt_status(stmt->statement_.get(), info.sqlite_status_id, true);
3405+
}
3406+
3407+
// The column name cache is keyed on SQLITE_STMTSTATUS_REPREPARE, which was
3408+
// just zeroed. Without invalidating, a later re-prepare can make the counter
3409+
// match the cached generation again and the stale names would be reused.
3410+
stmt->InvalidateColumnNameCache();
3411+
}
3412+
33533413
voidStatementSync::SetAllowBareNamedParameters(
33543414
const FunctionCallbackInfo<Value>& args) {
33553415
StatementSync* stmt;
@@ -3775,6 +3835,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
37753835
tmpl,
37763836
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
37773837
StatementSync::ExpandedSQLGetter);
3838+
SetProtoMethodNoSideEffect(isolate, tmpl, "stat", StatementSync::Stat);
3839+
SetProtoMethod(isolate, tmpl, "resetStats", StatementSync::ResetStats);
37783840
SetProtoMethod(isolate,
37793841
tmpl,
37803842
"setAllowBareNamedParameters",

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ static_assert(
5252
CheckLimitIndices(),
5353
"Each kLimitMapping entry's sqlite_limit_id must match its index");
5454

55+
// Mapping from JavaScript counter names to SQLite statement status constants
56+
structStatusInfo {
57+
std::string_view js_name;
58+
int sqlite_status_id;
59+
};
60+
61+
// SQLITE_STMTSTATUS_FILTER_HIT and SQLITE_STMTSTATUS_FILTER_MISS require
62+
// SQLite >= 3.38.0. Older shared-library builds omit the two counters.
63+
#if SQLITE_VERSION_NUMBER >= 3038000
64+
#defineNODE_SQLITE_HAS_FILTER_STATUS1
65+
#endif
66+
67+
inlineconstexprautokStatusMapping = std::to_array<StatusInfo>({
68+
{"fullscanStep", SQLITE_STMTSTATUS_FULLSCAN_STEP},
69+
{"sort", SQLITE_STMTSTATUS_SORT},
70+
{"autoindex", SQLITE_STMTSTATUS_AUTOINDEX},
71+
{"vmStep", SQLITE_STMTSTATUS_VM_STEP},
72+
{"reprepare", SQLITE_STMTSTATUS_REPREPARE},
73+
{"run", SQLITE_STMTSTATUS_RUN},
74+
#ifdef NODE_SQLITE_HAS_FILTER_STATUS
75+
{"filterMiss", SQLITE_STMTSTATUS_FILTER_MISS},
76+
{"filterHit", SQLITE_STMTSTATUS_FILTER_HIT},
77+
#endif
78+
{"memused", SQLITE_STMTSTATUS_MEMUSED},
79+
});
80+
5581
classDatabaseOpenConfiguration {
5682
public:
5783
explicitDatabaseOpenConfiguration(std::string&& location)
@@ -284,6 +310,8 @@ class StatementSync : public BaseObject {
284310
staticvoidSourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
285311
staticvoidExpandedSQLGetter(
286312
const v8::FunctionCallbackInfo<v8::Value>& args);
313+
staticvoidStat(const v8::FunctionCallbackInfo<v8::Value>& args);
314+
staticvoidResetStats(const v8::FunctionCallbackInfo<v8::Value>& args);
287315
staticvoidSetAllowBareNamedParameters(
288316
const v8::FunctionCallbackInfo<v8::Value>& args);
289317
staticvoidSetAllowUnknownNamedParameters(

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 72c37b1

Browse files
geeksilva97aduh95
authored andcommitted
sqlite: expose prepared statement statistics
Signed-off-by: geeksilva97 <edigleyssonsilva@gmail.com> PR-URL: #64541 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent e3fda69 commit 72c37b1

4 files changed

Lines changed: 353 additions & 6 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,18 @@ returns an empty iterator. The prepared statement [parameters are bound][] using
11881188
the values in `namedParameters` and `anonymousParameters`. See
11891189
[Binding parameters][].
11901190

1191+
### `statement.resetStats()`
1192+
1193+
<!-- YAML
1194+
added: REPLACEME
1195+
-->
1196+
1197+
Resets every counter reported by [`statement.stat()`][] back to zero, except
1198+
`memused`, which reports current memory usage and cannot be reset. This
1199+
method is a wrapper around [`sqlite3_stmt_status()`][] and is useful for
1200+
measuring a specific workload without the counts accumulated by earlier
1201+
executions of the same prepared statement.
1202+
11911203
### `statement.run([namedParameters][, ...anonymousParameters])`
11921204

11931205
<!-- YAML
@@ -1314,6 +1326,43 @@ added: REPLACEME
13141326
Finalizes the prepared statement. If the prepared statement is already
13151327
finalized, then this is a no-op.
13161328

1329+
### `statement.stat(counter)`
1330+
1331+
<!-- YAML
1332+
added: REPLACEME
1333+
-->
1334+
1335+
*`counter` {string} The name of the counter to read. One of:
1336+
1337+
*`'fullscanStep'` The number of times SQLite has stepped forward in a table
1338+
as part of a full table scan.
1339+
*`'sort'` The number of sort operations that have occurred.
1340+
*`'autoindex'` The number of rows inserted into transient indices that were
1341+
created automatically to help joins run faster.
1342+
*`'vmStep'` The number of virtual machine operations executed by the
1343+
prepared statement.
1344+
*`'reprepare'` The number of times the statement has been automatically
1345+
reprepared due to schema changes or changes to bound parameters.
1346+
*`'run'` The number of execution cycles started by the prepared statement.
1347+
*`'filterMiss'` The number of times the Bloom filter returned a result that
1348+
required the join step to be processed as normal.
1349+
*`'filterHit'` The number of times a join step was bypassed because a Bloom
1350+
filter returned not-found.
1351+
*`'memused'` The approximate number of bytes of heap memory used to store
1352+
the prepared statement.
1353+
1354+
* Returns: {number} The current value of the requested counter.
1355+
1356+
Returns one of the runtime counters that SQLite tracks for this prepared
1357+
statement. This method is a wrapper around [`sqlite3_stmt_status()`][] and does
1358+
not reset the counter. Asserting that a statement does not perform a full table
1359+
scan (`statement.stat('fullscanStep') === 0`) is a useful check to guard
1360+
against degenerate performance.
1361+
1362+
The `'filterMiss'` and `'filterHit'` counters require SQLite 3.38.0 or later.
1363+
Builds linked against an older SQLite with `--shared-sqlite` do not expose them,
1364+
and passing either name throws `ERR_INVALID_ARG_VALUE`.
1365+
13171366
## Class: `SQLTagStore`
13181367

13191368
<!-- YAML
@@ -1832,6 +1881,7 @@ callback function to indicate what type of operation is being authorized.
18321881
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
18331882
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
18341883
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
1884+
[`sqlite3_stmt_status()`]: https://www.sqlite.org/c3ref/stmt_status.html
18351885
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
18361886
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
18371887
[`sqlite3session_changeset()`]: https://www.sqlite.org/session/sqlite3session_changeset.html
@@ -1840,6 +1890,7 @@ callback function to indicate what type of operation is being authorized.
18401890
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
18411891
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
18421892
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
1893+
[`statement.stat()`]: #statementstatcounter
18431894
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
18441895
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
18451896
[data types]: https://www.sqlite.org/datatype3.html

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ Local<DictionaryTemplate> getLazyIterTemplate(Environment* env) {
177177
}
178178
} // namespace
179179

180-
// Helper function to find limit info from JS property name
181-
staticconstexprconst LimitInfo* GetLimitInfoFromName(std::string_view name) {
182-
for (constauto& info : kLimitMapping) {
180+
// Helper function to look up a mapping entry by its JS-facing name
181+
template <typename T, size_t N>
182+
staticconstexprconst T* FindByJsName(const std::array<T, N>& mapping,
183+
std::string_view name) {
184+
for (constauto& info : mapping) {
183185
if (name == info.js_name) {
184186
return &info;
185187
}
@@ -793,7 +795,8 @@ Intercepted DatabaseSyncLimits::LimitsGetter(
793795
Isolate* isolate = env->isolate();
794796

795797
Utf8Value prop_name(isolate, property);
796-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
798+
const LimitInfo* limit_info =
799+
FindByJsName(kLimitMapping, prop_name.ToStringView());
797800

798801
if (limit_info == nullptr) {
799802
return Intercepted::kNo; // Unknown property, let default handling occur
@@ -825,7 +828,8 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
825828
Isolate* isolate = env->isolate();
826829

827830
Utf8Value prop_name(isolate, property);
828-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
831+
const LimitInfo* limit_info =
832+
FindByJsName(kLimitMapping, prop_name.ToStringView());
829833

830834
if (limit_info == nullptr) {
831835
return Intercepted::kNo;
@@ -873,7 +877,8 @@ Intercepted DatabaseSyncLimits::LimitsQuery(
873877

874878
Isolate* isolate = info.GetIsolate();
875879
Utf8Value prop_name(isolate, property);
876-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
880+
const LimitInfo* limit_info =
881+
FindByJsName(kLimitMapping, prop_name.ToStringView());
877882

878883
if (!limit_info) {
879884
return Intercepted::kNo;
@@ -2694,6 +2699,7 @@ void StatementSync::Finalize() {
26942699

26952700
voidStatementSync::InvalidateColumnNameCache() {
26962701
cached_column_names_.clear();
2702+
cached_column_names_reprepare_count_ = -1;
26972703
}
26982704

26992705
inlineboolStatementSync::IsFinalized() {
@@ -3350,6 +3356,60 @@ void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo<Value>& args) {
33503356
args.GetReturnValue().Set(result);
33513357
}
33523358

3359+
voidStatementSync::Stat(const FunctionCallbackInfo<Value>& args) {
3360+
StatementSync* stmt;
3361+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3362+
Environment* env = Environment::GetCurrent(args);
3363+
THROW_AND_RETURN_ON_BAD_STATE(
3364+
env, stmt->IsFinalized(), "statement has been finalized");
3365+
Isolate* isolate = env->isolate();
3366+
3367+
if (!args[0]->IsString()) {
3368+
THROW_ERR_INVALID_ARG_TYPE(isolate,
3369+
"The \"counter\" argument must be a string.");
3370+
return;
3371+
}
3372+
3373+
Utf8Value counter(isolate, args[0].As<String>());
3374+
const StatusInfo* status_info =
3375+
FindByJsName(kStatusMapping, counter.ToStringView());
3376+
if (status_info == nullptr) {
3377+
THROW_ERR_INVALID_ARG_VALUE(
3378+
isolate, "The \"counter\" argument is not a valid statistic name.");
3379+
return;
3380+
}
3381+
3382+
// The reset flag is always false; the counter is read without being cleared.
3383+
int value = sqlite3_stmt_status(
3384+
stmt->statement_.get(), status_info->sqlite_status_id, false);
3385+
args.GetReturnValue().Set(Integer::New(isolate, value));
3386+
}
3387+
3388+
voidStatementSync::ResetStats(const FunctionCallbackInfo<Value>& args) {
3389+
StatementSync* stmt;
3390+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3391+
Environment* env = Environment::GetCurrent(args);
3392+
THROW_AND_RETURN_ON_BAD_STATE(
3393+
env, stmt->IsFinalized(), "statement has been finalized");
3394+
3395+
// sqlite3_stmt_status() resets a single counter per call, so every exposed
3396+
// counter is visited. The returned value is the pre-reset one and is unused.
3397+
// SQLITE_STMTSTATUS_MEMUSED is skipped: it reports current memory usage
3398+
// rather than an accumulated counter, and SQLite ignores the reset flag for
3399+
// it.
3400+
for (constauto& info : kStatusMapping) {
3401+
if (info.sqlite_status_id == SQLITE_STMTSTATUS_MEMUSED) {
3402+
continue;
3403+
}
3404+
sqlite3_stmt_status(stmt->statement_.get(), info.sqlite_status_id, true);
3405+
}
3406+
3407+
// The column name cache is keyed on SQLITE_STMTSTATUS_REPREPARE, which was
3408+
// just zeroed. Without invalidating, a later re-prepare can make the counter
3409+
// match the cached generation again and the stale names would be reused.
3410+
stmt->InvalidateColumnNameCache();
3411+
}
3412+
33533413
voidStatementSync::SetAllowBareNamedParameters(
33543414
const FunctionCallbackInfo<Value>& args) {
33553415
StatementSync* stmt;
@@ -3775,6 +3835,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
37753835
tmpl,
37763836
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
37773837
StatementSync::ExpandedSQLGetter);
3838+
SetProtoMethodNoSideEffect(isolate, tmpl, "stat", StatementSync::Stat);
3839+
SetProtoMethod(isolate, tmpl, "resetStats", StatementSync::ResetStats);
37783840
SetProtoMethod(isolate,
37793841
tmpl,
37803842
"setAllowBareNamedParameters",

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ static_assert(
5252
CheckLimitIndices(),
5353
"Each kLimitMapping entry's sqlite_limit_id must match its index");
5454

55+
// Mapping from JavaScript counter names to SQLite statement status constants
56+
structStatusInfo {
57+
std::string_view js_name;
58+
int sqlite_status_id;
59+
};
60+
61+
// SQLITE_STMTSTATUS_FILTER_HIT and SQLITE_STMTSTATUS_FILTER_MISS require
62+
// SQLite >= 3.38.0. Older shared-library builds omit the two counters.
63+
#if SQLITE_VERSION_NUMBER >= 3038000
64+
#defineNODE_SQLITE_HAS_FILTER_STATUS1
65+
#endif
66+
67+
inlineconstexprautokStatusMapping = std::to_array<StatusInfo>({
68+
{"fullscanStep", SQLITE_STMTSTATUS_FULLSCAN_STEP},
69+
{"sort", SQLITE_STMTSTATUS_SORT},
70+
{"autoindex", SQLITE_STMTSTATUS_AUTOINDEX},
71+
{"vmStep", SQLITE_STMTSTATUS_VM_STEP},
72+
{"reprepare", SQLITE_STMTSTATUS_REPREPARE},
73+
{"run", SQLITE_STMTSTATUS_RUN},
74+
#ifdef NODE_SQLITE_HAS_FILTER_STATUS
75+
{"filterMiss", SQLITE_STMTSTATUS_FILTER_MISS},
76+
{"filterHit", SQLITE_STMTSTATUS_FILTER_HIT},
77+
#endif
78+
{"memused", SQLITE_STMTSTATUS_MEMUSED},
79+
});
80+
5581
classDatabaseOpenConfiguration {
5682
public:
5783
explicitDatabaseOpenConfiguration(std::string&& location)
@@ -284,6 +310,8 @@ class StatementSync : public BaseObject {
284310
staticvoidSourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
285311
staticvoidExpandedSQLGetter(
286312
const v8::FunctionCallbackInfo<v8::Value>& args);
313+
staticvoidStat(const v8::FunctionCallbackInfo<v8::Value>& args);
314+
staticvoidResetStats(const v8::FunctionCallbackInfo<v8::Value>& args);
287315
staticvoidSetAllowBareNamedParameters(
288316
const v8::FunctionCallbackInfo<v8::Value>& args);
289317
staticvoidSetAllowUnknownNamedParameters(

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 72c37b1

Browse files
geeksilva97aduh95
authored andcommitted
sqlite: expose prepared statement statistics
Signed-off-by: geeksilva97 <edigleyssonsilva@gmail.com> PR-URL: #64541 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent e3fda69 commit 72c37b1

4 files changed

Lines changed: 353 additions & 6 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,18 @@ returns an empty iterator. The prepared statement [parameters are bound][] using
11881188
the values in `namedParameters` and `anonymousParameters`. See
11891189
[Binding parameters][].
11901190

1191+
### `statement.resetStats()`
1192+
1193+
<!-- YAML
1194+
added: REPLACEME
1195+
-->
1196+
1197+
Resets every counter reported by [`statement.stat()`][] back to zero, except
1198+
`memused`, which reports current memory usage and cannot be reset. This
1199+
method is a wrapper around [`sqlite3_stmt_status()`][] and is useful for
1200+
measuring a specific workload without the counts accumulated by earlier
1201+
executions of the same prepared statement.
1202+
11911203
### `statement.run([namedParameters][, ...anonymousParameters])`
11921204

11931205
<!-- YAML
@@ -1314,6 +1326,43 @@ added: REPLACEME
13141326
Finalizes the prepared statement. If the prepared statement is already
13151327
finalized, then this is a no-op.
13161328

1329+
### `statement.stat(counter)`
1330+
1331+
<!-- YAML
1332+
added: REPLACEME
1333+
-->
1334+
1335+
*`counter` {string} The name of the counter to read. One of:
1336+
1337+
*`'fullscanStep'` The number of times SQLite has stepped forward in a table
1338+
as part of a full table scan.
1339+
*`'sort'` The number of sort operations that have occurred.
1340+
*`'autoindex'` The number of rows inserted into transient indices that were
1341+
created automatically to help joins run faster.
1342+
*`'vmStep'` The number of virtual machine operations executed by the
1343+
prepared statement.
1344+
*`'reprepare'` The number of times the statement has been automatically
1345+
reprepared due to schema changes or changes to bound parameters.
1346+
*`'run'` The number of execution cycles started by the prepared statement.
1347+
*`'filterMiss'` The number of times the Bloom filter returned a result that
1348+
required the join step to be processed as normal.
1349+
*`'filterHit'` The number of times a join step was bypassed because a Bloom
1350+
filter returned not-found.
1351+
*`'memused'` The approximate number of bytes of heap memory used to store
1352+
the prepared statement.
1353+
1354+
* Returns: {number} The current value of the requested counter.
1355+
1356+
Returns one of the runtime counters that SQLite tracks for this prepared
1357+
statement. This method is a wrapper around [`sqlite3_stmt_status()`][] and does
1358+
not reset the counter. Asserting that a statement does not perform a full table
1359+
scan (`statement.stat('fullscanStep') === 0`) is a useful check to guard
1360+
against degenerate performance.
1361+
1362+
The `'filterMiss'` and `'filterHit'` counters require SQLite 3.38.0 or later.
1363+
Builds linked against an older SQLite with `--shared-sqlite` do not expose them,
1364+
and passing either name throws `ERR_INVALID_ARG_VALUE`.
1365+
13171366
## Class: `SQLTagStore`
13181367

13191368
<!-- YAML
@@ -1832,6 +1881,7 @@ callback function to indicate what type of operation is being authorized.
18321881
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
18331882
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
18341883
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
1884+
[`sqlite3_stmt_status()`]: https://www.sqlite.org/c3ref/stmt_status.html
18351885
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
18361886
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
18371887
[`sqlite3session_changeset()`]: https://www.sqlite.org/session/sqlite3session_changeset.html
@@ -1840,6 +1890,7 @@ callback function to indicate what type of operation is being authorized.
18401890
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
18411891
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
18421892
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
1893+
[`statement.stat()`]: #statementstatcounter
18431894
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
18441895
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
18451896
[data types]: https://www.sqlite.org/datatype3.html

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ Local<DictionaryTemplate> getLazyIterTemplate(Environment* env) {
177177
}
178178
} // namespace
179179

180-
// Helper function to find limit info from JS property name
181-
staticconstexprconst LimitInfo* GetLimitInfoFromName(std::string_view name) {
182-
for (constauto& info : kLimitMapping) {
180+
// Helper function to look up a mapping entry by its JS-facing name
181+
template <typename T, size_t N>
182+
staticconstexprconst T* FindByJsName(const std::array<T, N>& mapping,
183+
std::string_view name) {
184+
for (constauto& info : mapping) {
183185
if (name == info.js_name) {
184186
return &info;
185187
}
@@ -793,7 +795,8 @@ Intercepted DatabaseSyncLimits::LimitsGetter(
793795
Isolate* isolate = env->isolate();
794796

795797
Utf8Value prop_name(isolate, property);
796-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
798+
const LimitInfo* limit_info =
799+
FindByJsName(kLimitMapping, prop_name.ToStringView());
797800

798801
if (limit_info == nullptr) {
799802
return Intercepted::kNo; // Unknown property, let default handling occur
@@ -825,7 +828,8 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
825828
Isolate* isolate = env->isolate();
826829

827830
Utf8Value prop_name(isolate, property);
828-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
831+
const LimitInfo* limit_info =
832+
FindByJsName(kLimitMapping, prop_name.ToStringView());
829833

830834
if (limit_info == nullptr) {
831835
return Intercepted::kNo;
@@ -873,7 +877,8 @@ Intercepted DatabaseSyncLimits::LimitsQuery(
873877

874878
Isolate* isolate = info.GetIsolate();
875879
Utf8Value prop_name(isolate, property);
876-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
880+
const LimitInfo* limit_info =
881+
FindByJsName(kLimitMapping, prop_name.ToStringView());
877882

878883
if (!limit_info) {
879884
return Intercepted::kNo;
@@ -2694,6 +2699,7 @@ void StatementSync::Finalize() {
26942699

26952700
voidStatementSync::InvalidateColumnNameCache() {
26962701
cached_column_names_.clear();
2702+
cached_column_names_reprepare_count_ = -1;
26972703
}
26982704

26992705
inlineboolStatementSync::IsFinalized() {
@@ -3350,6 +3356,60 @@ void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo<Value>& args) {
33503356
args.GetReturnValue().Set(result);
33513357
}
33523358

3359+
voidStatementSync::Stat(const FunctionCallbackInfo<Value>& args) {
3360+
StatementSync* stmt;
3361+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3362+
Environment* env = Environment::GetCurrent(args);
3363+
THROW_AND_RETURN_ON_BAD_STATE(
3364+
env, stmt->IsFinalized(), "statement has been finalized");
3365+
Isolate* isolate = env->isolate();
3366+
3367+
if (!args[0]->IsString()) {
3368+
THROW_ERR_INVALID_ARG_TYPE(isolate,
3369+
"The \"counter\" argument must be a string.");
3370+
return;
3371+
}
3372+
3373+
Utf8Value counter(isolate, args[0].As<String>());
3374+
const StatusInfo* status_info =
3375+
FindByJsName(kStatusMapping, counter.ToStringView());
3376+
if (status_info == nullptr) {
3377+
THROW_ERR_INVALID_ARG_VALUE(
3378+
isolate, "The \"counter\" argument is not a valid statistic name.");
3379+
return;
3380+
}
3381+
3382+
// The reset flag is always false; the counter is read without being cleared.
3383+
int value = sqlite3_stmt_status(
3384+
stmt->statement_.get(), status_info->sqlite_status_id, false);
3385+
args.GetReturnValue().Set(Integer::New(isolate, value));
3386+
}
3387+
3388+
voidStatementSync::ResetStats(const FunctionCallbackInfo<Value>& args) {
3389+
StatementSync* stmt;
3390+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3391+
Environment* env = Environment::GetCurrent(args);
3392+
THROW_AND_RETURN_ON_BAD_STATE(
3393+
env, stmt->IsFinalized(), "statement has been finalized");
3394+
3395+
// sqlite3_stmt_status() resets a single counter per call, so every exposed
3396+
// counter is visited. The returned value is the pre-reset one and is unused.
3397+
// SQLITE_STMTSTATUS_MEMUSED is skipped: it reports current memory usage
3398+
// rather than an accumulated counter, and SQLite ignores the reset flag for
3399+
// it.
3400+
for (constauto& info : kStatusMapping) {
3401+
if (info.sqlite_status_id == SQLITE_STMTSTATUS_MEMUSED) {
3402+
continue;
3403+
}
3404+
sqlite3_stmt_status(stmt->statement_.get(), info.sqlite_status_id, true);
3405+
}
3406+
3407+
// The column name cache is keyed on SQLITE_STMTSTATUS_REPREPARE, which was
3408+
// just zeroed. Without invalidating, a later re-prepare can make the counter
3409+
// match the cached generation again and the stale names would be reused.
3410+
stmt->InvalidateColumnNameCache();
3411+
}
3412+
33533413
voidStatementSync::SetAllowBareNamedParameters(
33543414
const FunctionCallbackInfo<Value>& args) {
33553415
StatementSync* stmt;
@@ -3775,6 +3835,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
37753835
tmpl,
37763836
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
37773837
StatementSync::ExpandedSQLGetter);
3838+
SetProtoMethodNoSideEffect(isolate, tmpl, "stat", StatementSync::Stat);
3839+
SetProtoMethod(isolate, tmpl, "resetStats", StatementSync::ResetStats);
37783840
SetProtoMethod(isolate,
37793841
tmpl,
37803842
"setAllowBareNamedParameters",

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ static_assert(
5252
CheckLimitIndices(),
5353
"Each kLimitMapping entry's sqlite_limit_id must match its index");
5454

55+
// Mapping from JavaScript counter names to SQLite statement status constants
56+
structStatusInfo {
57+
std::string_view js_name;
58+
int sqlite_status_id;
59+
};
60+
61+
// SQLITE_STMTSTATUS_FILTER_HIT and SQLITE_STMTSTATUS_FILTER_MISS require
62+
// SQLite >= 3.38.0. Older shared-library builds omit the two counters.
63+
#if SQLITE_VERSION_NUMBER >= 3038000
64+
#defineNODE_SQLITE_HAS_FILTER_STATUS1
65+
#endif
66+
67+
inlineconstexprautokStatusMapping = std::to_array<StatusInfo>({
68+
{"fullscanStep", SQLITE_STMTSTATUS_FULLSCAN_STEP},
69+
{"sort", SQLITE_STMTSTATUS_SORT},
70+
{"autoindex", SQLITE_STMTSTATUS_AUTOINDEX},
71+
{"vmStep", SQLITE_STMTSTATUS_VM_STEP},
72+
{"reprepare", SQLITE_STMTSTATUS_REPREPARE},
73+
{"run", SQLITE_STMTSTATUS_RUN},
74+
#ifdef NODE_SQLITE_HAS_FILTER_STATUS
75+
{"filterMiss", SQLITE_STMTSTATUS_FILTER_MISS},
76+
{"filterHit", SQLITE_STMTSTATUS_FILTER_HIT},
77+
#endif
78+
{"memused", SQLITE_STMTSTATUS_MEMUSED},
79+
});
80+
5581
classDatabaseOpenConfiguration {
5682
public:
5783
explicitDatabaseOpenConfiguration(std::string&& location)
@@ -284,6 +310,8 @@ class StatementSync : public BaseObject {
284310
staticvoidSourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
285311
staticvoidExpandedSQLGetter(
286312
const v8::FunctionCallbackInfo<v8::Value>& args);
313+
staticvoidStat(const v8::FunctionCallbackInfo<v8::Value>& args);
314+
staticvoidResetStats(const v8::FunctionCallbackInfo<v8::Value>& args);
287315
staticvoidSetAllowBareNamedParameters(
288316
const v8::FunctionCallbackInfo<v8::Value>& args);
289317
staticvoidSetAllowUnknownNamedParameters(

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit 72c37b1

Browse files
geeksilva97aduh95
authored andcommitted
sqlite: expose prepared statement statistics
Signed-off-by: geeksilva97 <edigleyssonsilva@gmail.com> PR-URL: #64541 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent e3fda69 commit 72c37b1

4 files changed

Lines changed: 353 additions & 6 deletions

File tree

β€Ždoc/api/sqlite.mdβ€Ž

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,18 @@ returns an empty iterator. The prepared statement [parameters are bound][] using
11881188
the values in `namedParameters` and `anonymousParameters`. See
11891189
[Binding parameters][].
11901190

1191+
### `statement.resetStats()`
1192+
1193+
<!-- YAML
1194+
added: REPLACEME
1195+
-->
1196+
1197+
Resets every counter reported by [`statement.stat()`][] back to zero, except
1198+
`memused`, which reports current memory usage and cannot be reset. This
1199+
method is a wrapper around [`sqlite3_stmt_status()`][] and is useful for
1200+
measuring a specific workload without the counts accumulated by earlier
1201+
executions of the same prepared statement.
1202+
11911203
### `statement.run([namedParameters][, ...anonymousParameters])`
11921204

11931205
<!-- YAML
@@ -1314,6 +1326,43 @@ added: REPLACEME
13141326
Finalizes the prepared statement. If the prepared statement is already
13151327
finalized, then this is a no-op.
13161328

1329+
### `statement.stat(counter)`
1330+
1331+
<!-- YAML
1332+
added: REPLACEME
1333+
-->
1334+
1335+
*`counter` {string} The name of the counter to read. One of:
1336+
1337+
*`'fullscanStep'` The number of times SQLite has stepped forward in a table
1338+
as part of a full table scan.
1339+
*`'sort'` The number of sort operations that have occurred.
1340+
*`'autoindex'` The number of rows inserted into transient indices that were
1341+
created automatically to help joins run faster.
1342+
*`'vmStep'` The number of virtual machine operations executed by the
1343+
prepared statement.
1344+
*`'reprepare'` The number of times the statement has been automatically
1345+
reprepared due to schema changes or changes to bound parameters.
1346+
*`'run'` The number of execution cycles started by the prepared statement.
1347+
*`'filterMiss'` The number of times the Bloom filter returned a result that
1348+
required the join step to be processed as normal.
1349+
*`'filterHit'` The number of times a join step was bypassed because a Bloom
1350+
filter returned not-found.
1351+
*`'memused'` The approximate number of bytes of heap memory used to store
1352+
the prepared statement.
1353+
1354+
* Returns: {number} The current value of the requested counter.
1355+
1356+
Returns one of the runtime counters that SQLite tracks for this prepared
1357+
statement. This method is a wrapper around [`sqlite3_stmt_status()`][] and does
1358+
not reset the counter. Asserting that a statement does not perform a full table
1359+
scan (`statement.stat('fullscanStep') === 0`) is a useful check to guard
1360+
against degenerate performance.
1361+
1362+
The `'filterMiss'` and `'filterHit'` counters require SQLite 3.38.0 or later.
1363+
Builds linked against an older SQLite with `--shared-sqlite` do not expose them,
1364+
and passing either name throws `ERR_INVALID_ARG_VALUE`.
1365+
13171366
## Class: `SQLTagStore`
13181367

13191368
<!-- YAML
@@ -1832,6 +1881,7 @@ callback function to indicate what type of operation is being authorized.
18321881
[`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html
18331882
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
18341883
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
1884+
[`sqlite3_stmt_status()`]: https://www.sqlite.org/c3ref/stmt_status.html
18351885
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
18361886
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
18371887
[`sqlite3session_changeset()`]: https://www.sqlite.org/session/sqlite3session_changeset.html
@@ -1840,6 +1890,7 @@ callback function to indicate what type of operation is being authorized.
18401890
[`sqlite3session_patchset()`]: https://www.sqlite.org/session/sqlite3session_patchset.html
18411891
[`statement.setAllowBareNamedParameters()`]: #statementsetallowbarenamedparametersenabled
18421892
[`statement.setAllowUnknownNamedParameters()`]: #statementsetallowunknownnamedparametersenabled
1893+
[`statement.stat()`]: #statementstatcounter
18431894
[busy timeout]: https://sqlite.org/c3ref/busy_timeout.html
18441895
[connection]: https://www.sqlite.org/c3ref/sqlite3.html
18451896
[data types]: https://www.sqlite.org/datatype3.html

β€Žsrc/node_sqlite.ccβ€Ž

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ Local<DictionaryTemplate> getLazyIterTemplate(Environment* env) {
177177
}
178178
} // namespace
179179

180-
// Helper function to find limit info from JS property name
181-
staticconstexprconst LimitInfo* GetLimitInfoFromName(std::string_view name) {
182-
for (constauto& info : kLimitMapping) {
180+
// Helper function to look up a mapping entry by its JS-facing name
181+
template <typename T, size_t N>
182+
staticconstexprconst T* FindByJsName(const std::array<T, N>& mapping,
183+
std::string_view name) {
184+
for (constauto& info : mapping) {
183185
if (name == info.js_name) {
184186
return &info;
185187
}
@@ -793,7 +795,8 @@ Intercepted DatabaseSyncLimits::LimitsGetter(
793795
Isolate* isolate = env->isolate();
794796

795797
Utf8Value prop_name(isolate, property);
796-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
798+
const LimitInfo* limit_info =
799+
FindByJsName(kLimitMapping, prop_name.ToStringView());
797800

798801
if (limit_info == nullptr) {
799802
return Intercepted::kNo; // Unknown property, let default handling occur
@@ -825,7 +828,8 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
825828
Isolate* isolate = env->isolate();
826829

827830
Utf8Value prop_name(isolate, property);
828-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
831+
const LimitInfo* limit_info =
832+
FindByJsName(kLimitMapping, prop_name.ToStringView());
829833

830834
if (limit_info == nullptr) {
831835
return Intercepted::kNo;
@@ -873,7 +877,8 @@ Intercepted DatabaseSyncLimits::LimitsQuery(
873877

874878
Isolate* isolate = info.GetIsolate();
875879
Utf8Value prop_name(isolate, property);
876-
const LimitInfo* limit_info = GetLimitInfoFromName(prop_name.ToStringView());
880+
const LimitInfo* limit_info =
881+
FindByJsName(kLimitMapping, prop_name.ToStringView());
877882

878883
if (!limit_info) {
879884
return Intercepted::kNo;
@@ -2694,6 +2699,7 @@ void StatementSync::Finalize() {
26942699

26952700
voidStatementSync::InvalidateColumnNameCache() {
26962701
cached_column_names_.clear();
2702+
cached_column_names_reprepare_count_ = -1;
26972703
}
26982704

26992705
inlineboolStatementSync::IsFinalized() {
@@ -3350,6 +3356,60 @@ void StatementSync::ExpandedSQLGetter(const FunctionCallbackInfo<Value>& args) {
33503356
args.GetReturnValue().Set(result);
33513357
}
33523358

3359+
voidStatementSync::Stat(const FunctionCallbackInfo<Value>& args) {
3360+
StatementSync* stmt;
3361+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3362+
Environment* env = Environment::GetCurrent(args);
3363+
THROW_AND_RETURN_ON_BAD_STATE(
3364+
env, stmt->IsFinalized(), "statement has been finalized");
3365+
Isolate* isolate = env->isolate();
3366+
3367+
if (!args[0]->IsString()) {
3368+
THROW_ERR_INVALID_ARG_TYPE(isolate,
3369+
"The \"counter\" argument must be a string.");
3370+
return;
3371+
}
3372+
3373+
Utf8Value counter(isolate, args[0].As<String>());
3374+
const StatusInfo* status_info =
3375+
FindByJsName(kStatusMapping, counter.ToStringView());
3376+
if (status_info == nullptr) {
3377+
THROW_ERR_INVALID_ARG_VALUE(
3378+
isolate, "The \"counter\" argument is not a valid statistic name.");
3379+
return;
3380+
}
3381+
3382+
// The reset flag is always false; the counter is read without being cleared.
3383+
int value = sqlite3_stmt_status(
3384+
stmt->statement_.get(), status_info->sqlite_status_id, false);
3385+
args.GetReturnValue().Set(Integer::New(isolate, value));
3386+
}
3387+
3388+
voidStatementSync::ResetStats(const FunctionCallbackInfo<Value>& args) {
3389+
StatementSync* stmt;
3390+
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
3391+
Environment* env = Environment::GetCurrent(args);
3392+
THROW_AND_RETURN_ON_BAD_STATE(
3393+
env, stmt->IsFinalized(), "statement has been finalized");
3394+
3395+
// sqlite3_stmt_status() resets a single counter per call, so every exposed
3396+
// counter is visited. The returned value is the pre-reset one and is unused.
3397+
// SQLITE_STMTSTATUS_MEMUSED is skipped: it reports current memory usage
3398+
// rather than an accumulated counter, and SQLite ignores the reset flag for
3399+
// it.
3400+
for (constauto& info : kStatusMapping) {
3401+
if (info.sqlite_status_id == SQLITE_STMTSTATUS_MEMUSED) {
3402+
continue;
3403+
}
3404+
sqlite3_stmt_status(stmt->statement_.get(), info.sqlite_status_id, true);
3405+
}
3406+
3407+
// The column name cache is keyed on SQLITE_STMTSTATUS_REPREPARE, which was
3408+
// just zeroed. Without invalidating, a later re-prepare can make the counter
3409+
// match the cached generation again and the stale names would be reused.
3410+
stmt->InvalidateColumnNameCache();
3411+
}
3412+
33533413
voidStatementSync::SetAllowBareNamedParameters(
33543414
const FunctionCallbackInfo<Value>& args) {
33553415
StatementSync* stmt;
@@ -3775,6 +3835,8 @@ Local<FunctionTemplate> StatementSync::GetConstructorTemplate(
37753835
tmpl,
37763836
FIXED_ONE_BYTE_STRING(isolate, "expandedSQL"),
37773837
StatementSync::ExpandedSQLGetter);
3838+
SetProtoMethodNoSideEffect(isolate, tmpl, "stat", StatementSync::Stat);
3839+
SetProtoMethod(isolate, tmpl, "resetStats", StatementSync::ResetStats);
37783840
SetProtoMethod(isolate,
37793841
tmpl,
37803842
"setAllowBareNamedParameters",

β€Žsrc/node_sqlite.hβ€Ž

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,32 @@ static_assert(
5252
CheckLimitIndices(),
5353
"Each kLimitMapping entry's sqlite_limit_id must match its index");
5454

55+
// Mapping from JavaScript counter names to SQLite statement status constants
56+
structStatusInfo {
57+
std::string_view js_name;
58+
int sqlite_status_id;
59+
};
60+
61+
// SQLITE_STMTSTATUS_FILTER_HIT and SQLITE_STMTSTATUS_FILTER_MISS require
62+
// SQLite >= 3.38.0. Older shared-library builds omit the two counters.
63+
#if SQLITE_VERSION_NUMBER >= 3038000
64+
#defineNODE_SQLITE_HAS_FILTER_STATUS1
65+
#endif
66+
67+
inlineconstexprautokStatusMapping = std::to_array<StatusInfo>({
68+
{"fullscanStep", SQLITE_STMTSTATUS_FULLSCAN_STEP},
69+
{"sort", SQLITE_STMTSTATUS_SORT},
70+
{"autoindex", SQLITE_STMTSTATUS_AUTOINDEX},
71+
{"vmStep", SQLITE_STMTSTATUS_VM_STEP},
72+
{"reprepare", SQLITE_STMTSTATUS_REPREPARE},
73+
{"run", SQLITE_STMTSTATUS_RUN},
74+
#ifdef NODE_SQLITE_HAS_FILTER_STATUS
75+
{"filterMiss", SQLITE_STMTSTATUS_FILTER_MISS},
76+
{"filterHit", SQLITE_STMTSTATUS_FILTER_HIT},
77+
#endif
78+
{"memused", SQLITE_STMTSTATUS_MEMUSED},
79+
});
80+
5581
classDatabaseOpenConfiguration {
5682
public:
5783
explicitDatabaseOpenConfiguration(std::string&& location)
@@ -284,6 +310,8 @@ class StatementSync : public BaseObject {
284310
staticvoidSourceSQLGetter(const v8::FunctionCallbackInfo<v8::Value>& args);
285311
staticvoidExpandedSQLGetter(
286312
const v8::FunctionCallbackInfo<v8::Value>& args);
313+
staticvoidStat(const v8::FunctionCallbackInfo<v8::Value>& args);
314+
staticvoidResetStats(const v8::FunctionCallbackInfo<v8::Value>& args);
287315
staticvoidSetAllowBareNamedParameters(
288316
const v8::FunctionCallbackInfo<v8::Value>& args);
289317
staticvoidSetAllowUnknownNamedParameters(

0 commit comments

Comments
Β (0)