-
Notifications
You must be signed in to change notification settings - Fork 593
Attribute SQLite memory consumed to isolate memory consumed #6380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,153 @@ | ||||||||||||||||||||
| // Copyright (c) 2025 Cloudflare, Inc. | ||||||||||||||||||||
| // Licensed under the Apache 2.0 license found in the LICENSE file or at: | ||||||||||||||||||||
| // https://opensource.org/licenses/Apache-2.0 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #include "sqlite-metering.h" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #include <sqlite3.h> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #include <kj/debug.h> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #include <cstdlib> | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #if defined(__linux__) || defined(__GLIBC__) | ||||||||||||||||||||
| #include <malloc.h> | ||||||||||||||||||||
| #define SQLITE_METERING_MALLOC_USABLE_SIZE(p) malloc_usable_size(p) | ||||||||||||||||||||
| #elif defined(__APPLE__) | ||||||||||||||||||||
| #include <malloc/malloc.h> | ||||||||||||||||||||
| #define SQLITE_METERING_MALLOC_USABLE_SIZE(p) malloc_size(p) | ||||||||||||||||||||
| #elif defined(_WIN32) | ||||||||||||||||||||
| #include <malloc.h> | ||||||||||||||||||||
| #define SQLITE_METERING_MALLOC_USABLE_SIZE(p) _msize(p) | ||||||||||||||||||||
| #else | ||||||||||||||||||||
| // Fallback: no accounting. This path is unreachable in production (Linux). | ||||||||||||||||||||
| #define SQLITE_METERING_MALLOC_USABLE_SIZE(p) 0 | ||||||||||||||||||||
| #endif | ||||||||||||||||||||
|
|
||||||||||||||||||||
| namespace workerd { | ||||||||||||||||||||
|
|
||||||||||||||||||||
| thread_local SqliteMemoryScope* SqliteMemoryScope::threadLocalScope = nullptr; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||
| // Custom sqlite3_mem_methods implementation | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // All actual allocation is delegated to the system allocator (malloc/free/realloc). | ||||||||||||||||||||
| // We layer per-actor accounting on top by adjusting the ExternalMemoryAdjustment when | ||||||||||||||||||||
| // threadLocalScope is non-null. | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // Thread-safety: since threadLocalScope is thread-local, each thread sees its own actor's | ||||||||||||||||||||
| // scope independently with no locking needed. | ||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||
|
|
||||||||||||||||||||
| namespace { | ||||||||||||||||||||
|
|
||||||||||||||||||||
| inline size_t usableSize(void* ptr) { | ||||||||||||||||||||
| if (ptr == nullptr) return 0; | ||||||||||||||||||||
| #if defined(SQLITE_METERING_MALLOC_USABLE_SIZE) | ||||||||||||||||||||
| auto sz = SQLITE_METERING_MALLOC_USABLE_SIZE(ptr); | ||||||||||||||||||||
| if (sz > 0) return sz; | ||||||||||||||||||||
| #endif | ||||||||||||||||||||
| // Fallback: no accounting. This path is unreachable in production (Linux). | ||||||||||||||||||||
| return 0; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| } // namespace | ||||||||||||||||||||
|
|
||||||||||||||||||||
| void* sqliteMemMalloc(int size) { | ||||||||||||||||||||
| if (size <= 0) return nullptr; | ||||||||||||||||||||
| void* ptr = std::malloc(static_cast<size_t>(size)); | ||||||||||||||||||||
| if (ptr == nullptr) return nullptr; | ||||||||||||||||||||
| SqliteMemoryScope* scope = SqliteMemoryScope::threadLocalScope; | ||||||||||||||||||||
| if (scope != nullptr) { | ||||||||||||||||||||
| scope->adjustment.adjust(static_cast<ssize_t>(usableSize(ptr))); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return ptr; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| void sqliteMemFree(void* ptr) { | ||||||||||||||||||||
| if (ptr == nullptr) return; | ||||||||||||||||||||
| SqliteMemoryScope* scope = SqliteMemoryScope::threadLocalScope; | ||||||||||||||||||||
| if (scope != nullptr) { | ||||||||||||||||||||
| size_t actual = usableSize(ptr); | ||||||||||||||||||||
| if (actual > 0) { | ||||||||||||||||||||
| scope->adjustment.adjust(-static_cast<ssize_t>(actual)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| std::free(ptr); | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assertion risk: If memory was allocated outside a Consider tracking only the delta (allocs minus frees) within the scope's lifetime, or guarding against going negative:
Suggested change
Alternatively, consider using a separate counter that allows negative drift (e.g., a plain |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| void* sqliteMemRealloc(void* ptr, int newSize) { | ||||||||||||||||||||
| if (newSize <= 0) { | ||||||||||||||||||||
| sqliteMemFree(ptr); | ||||||||||||||||||||
| return nullptr; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| SqliteMemoryScope* scope = SqliteMemoryScope::threadLocalScope; | ||||||||||||||||||||
| size_t oldActual = (scope != nullptr && ptr != nullptr) ? usableSize(ptr) : 0; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| void* newPtr = std::realloc(ptr, static_cast<size_t>(newSize)); | ||||||||||||||||||||
| if (newPtr == nullptr) return nullptr; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (scope != nullptr && oldActual > 0) { | ||||||||||||||||||||
| size_t newActual = usableSize(newPtr); | ||||||||||||||||||||
| if (newActual > 0) { | ||||||||||||||||||||
| ssize_t delta = static_cast<ssize_t>(newActual) - static_cast<ssize_t>(oldActual); | ||||||||||||||||||||
| scope->adjustment.adjust(delta); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Under-accounting: When Consider accounting for the new allocation regardless:
Suggested change
This handles all cases: if |
||||||||||||||||||||
| return newPtr; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| namespace { | ||||||||||||||||||||
|
|
||||||||||||||||||||
| int sqliteMemSize(void* ptr) { | ||||||||||||||||||||
| return static_cast<int>(usableSize(ptr)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| int sqliteMemRoundup(int n) { | ||||||||||||||||||||
| // Round up to the next multiple of 8 as a conservative estimate. | ||||||||||||||||||||
| return (n + 7) & ~7; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| int sqliteMemInit(void* /*pAppData*/) { | ||||||||||||||||||||
| return SQLITE_OK; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| void sqliteMemShutdown(void* /*pAppData*/) {} | ||||||||||||||||||||
|
|
||||||||||||||||||||
| static const sqlite3_mem_methods kSqliteMemMethods = {sqliteMemMalloc, sqliteMemFree, | ||||||||||||||||||||
| sqliteMemRealloc, sqliteMemSize, sqliteMemRoundup, sqliteMemInit, sqliteMemShutdown, | ||||||||||||||||||||
| /*pAppData=*/nullptr}; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| } // namespace | ||||||||||||||||||||
|
|
||||||||||||||||||||
| SqliteMemoryScope::SqliteMemoryScope(jsg::ExternalMemoryAdjustment& adjustment) | ||||||||||||||||||||
| : adjustment(adjustment) { | ||||||||||||||||||||
| KJ_ASSERT( | ||||||||||||||||||||
| threadLocalScope == nullptr, "Another SqliteMemoryScope is already active on this thread."); | ||||||||||||||||||||
| threadLocalScope = this; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| SqliteMemoryScope::~SqliteMemoryScope() noexcept(false) { | ||||||||||||||||||||
| KJ_ASSERT(threadLocalScope == this); | ||||||||||||||||||||
| threadLocalScope = nullptr; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| void installSqliteCustomAllocator() { | ||||||||||||||||||||
| // Use a static once-flag. sqlite3_config() must be called before sqlite3_initialize(), which is | ||||||||||||||||||||
| // itself invoked implicitly by the first sqlite3_vfs_register() or sqlite3_open_v2() call. | ||||||||||||||||||||
| // We rely on callers (Vfs constructors, ensureMonkeyPatchedSqlite) to call this before any of | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale reference:
Suggested change
|
||||||||||||||||||||
| // those. | ||||||||||||||||||||
| static bool installed KJ_UNUSED = []() { | ||||||||||||||||||||
| int rc = sqlite3_config(SQLITE_CONFIG_MALLOC, &kSqliteMemMethods); | ||||||||||||||||||||
| KJ_ASSERT(rc == SQLITE_OK, "sqlite3_config(SQLITE_CONFIG_MALLOC) failed", rc); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Disable the global sqlite3_mem_used counter. It carries per-call mutex overhead and is | ||||||||||||||||||||
| // redundant now that per-actor accounting is done via ExternalMemoryAdjustment. | ||||||||||||||||||||
| rc = sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0); | ||||||||||||||||||||
| KJ_ASSERT(rc == SQLITE_OK, "sqlite3_config(SQLITE_CONFIG_MEMSTATUS) failed", rc); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return true; | ||||||||||||||||||||
| }(); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| } // namespace workerd | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,57 @@ | ||||||||||
| // Copyright (c) 2025 Cloudflare, Inc. | ||||||||||
| // Licensed under the Apache 2.0 license found in the LICENSE file or at: | ||||||||||
| // https://opensource.org/licenses/Apache-2.0 | ||||||||||
|
|
||||||||||
| #pragma once | ||||||||||
|
|
||||||||||
| // Per-isolate SQLite memory metering. | ||||||||||
| // | ||||||||||
| // SQLite uses a single process-wide memory allocator, but we want to account allocations against | ||||||||||
| // the specific actor (Durable Object) on whose behalf they are made. We do this by: | ||||||||||
| // | ||||||||||
| // 1. Installing a custom sqlite3_mem_methods that wraps the system allocator and checks a | ||||||||||
| // thread-local pointer on every allocation. | ||||||||||
| // | ||||||||||
| // 2. Installing a SqliteMemoryScope (RAII) on the current thread for the duration of each JS | ||||||||||
| // turn that may invoke SQLite, pointing at the actor's ExternalMemoryAdjustment. | ||||||||||
| // | ||||||||||
| // When the thread-local pointer is null (outside of an actor JS turn, or when the | ||||||||||
| // SQLITE_MEMORY_METERING autogate is disabled), allocations pass through to the underlying | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment references a |
||||||||||
| // allocator with no accounting. | ||||||||||
|
|
||||||||||
| #include <workerd/jsg/jsg.h> | ||||||||||
|
|
||||||||||
| #include <kj/common.h> | ||||||||||
|
|
||||||||||
| namespace workerd { | ||||||||||
|
|
||||||||||
| // RAII scope: installs this instance as the active per-actor SQLite memory context on the current | ||||||||||
| // thread for the duration of one JS turn, then clears it on destruction. | ||||||||||
| // | ||||||||||
| // Only one SqliteMemoryScope may be active per thread at a time. Construction asserts that no | ||||||||||
| // other scope is active, mirroring the CPU limit enforcement in LimitEnforcerImpl. | ||||||||||
| class SqliteMemoryScope { | ||||||||||
| public: | ||||||||||
| explicit SqliteMemoryScope(jsg::ExternalMemoryAdjustment& adjustment); | ||||||||||
| ~SqliteMemoryScope() noexcept(false); | ||||||||||
| KJ_DISALLOW_COPY_AND_MOVE(SqliteMemoryScope); | ||||||||||
|
|
||||||||||
| jsg::ExternalMemoryAdjustment& adjustment; | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This data member is only accessed by the three
Suggested change
(The existing |
||||||||||
|
|
||||||||||
| private: | ||||||||||
| // Thread-local pointer to the active scope. Set to this on construction, cleared on destruction. | ||||||||||
| static thread_local SqliteMemoryScope* threadLocalScope; | ||||||||||
|
|
||||||||||
| friend void* sqliteMemMalloc(int); | ||||||||||
| friend void sqliteMemFree(void*); | ||||||||||
| friend void* sqliteMemRealloc(void*, int); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| // Install the custom sqlite3_mem_methods. | ||||||||||
| // | ||||||||||
| // This must be called before the first sqlite3_initialize(), sqlite3_open_v2(), or | ||||||||||
| // sqlite3_vfs_register() call in the process. The implementation of this function must be | ||||||||||
| // idempotent. | ||||||||||
| void installSqliteCustomAllocator(); | ||||||||||
|
|
||||||||||
| } // namespace workerd | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This include may be unnecessary —
ExternalMemoryTargetandExternalMemoryAdjustmentare fully defined injsg/jsg.h, which is already included transitively viaactor-cache.h→jsg/jsg.h.setup.his a heavy header that pulls in the full isolate/V8 setup machinery.