Skip to content

Commit 2b496fc

Browse files
committed
Implement per-actor SQLite memory metering.
1 parent 04e4c46 commit 2b496fc

8 files changed

Lines changed: 439 additions & 0 deletions

File tree

src/workerd/io/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ wd_cc_library(
206206
"//src/workerd/util:autogate",
207207
"//src/workerd/util:duration-exceeded-logger",
208208
"//src/workerd/util:sqlite",
209+
"//src/workerd/util:sqlite-metering",
209210
"@capnp-cpp//src/capnp:capnp-rpc",
210211
"@capnp-cpp//src/kj:kj-async",
211212
],

src/workerd/io/actor-sqlite.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <workerd/io/trace.h>
1010
#include <workerd/util/sqlite-kv.h>
1111
#include <workerd/util/sqlite-metadata.h>
12+
#include <workerd/util/sqlite-metering.h>
1213

1314
namespace workerd {
1415

src/workerd/io/limit-enforcer.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ class LimitEnforcer {
188188

189189
// Only used downstream for internal metrics.
190190
virtual kj::Duration consumeTimeElapsedForPeriodicLogging() = 0;
191+
192+
// Returns the last snapshotted SQLite memory usage for the current actor, in bytes. Returns 0
193+
// for non-actor isolates.
194+
virtual size_t getSqliteMemoryUsage() const {
195+
return 0;
196+
}
191197
};
192198

193199
} // namespace workerd

src/workerd/util/BUILD.bazel

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,17 @@ wd_cc_library(
191191
],
192192
)
193193

194+
wd_cc_library(
195+
name = "sqlite-metering",
196+
srcs = ["sqlite-metering.c++"],
197+
hdrs = ["sqlite-metering.h"],
198+
visibility = ["//visibility:public"],
199+
deps = [
200+
"@capnp-cpp//src/kj",
201+
"@sqlite3",
202+
],
203+
)
204+
194205
wd_cc_library(
195206
name = "sqlite",
196207
srcs = [
@@ -211,6 +222,7 @@ wd_cc_library(
211222
deps = [
212223
":account-limits",
213224
":sentry",
225+
":sqlite-metering",
214226
"@capnp-cpp//src/kj:kj-async",
215227
],
216228
)
@@ -380,6 +392,15 @@ kj_test(
380392
],
381393
)
382394

395+
kj_test(
396+
size = "medium",
397+
src = "sqlite-metering-test.c++",
398+
deps = [
399+
":sqlite-metering",
400+
"@capnp-cpp//src/kj",
401+
],
402+
)
403+
383404
kj_test(
384405
src = "sqlite-metadata-test.c++",
385406
deps = [
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright (c) 2025 Cloudflare, Inc.
2+
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3+
// https://opensource.org/licenses/Apache-2.0
4+
5+
#include "sqlite-metering.h"
6+
7+
#include <kj/test.h>
8+
#include <kj/thread.h>
9+
10+
#include <atomic>
11+
12+
namespace workerd {
13+
namespace {
14+
15+
void test_sqlite3_mem_methods(bool expectEnforcedLimits) {
16+
// sqliteMemMalloc with a negative size returns a nullptr.
17+
KJ_EXPECT(sqliteMemMalloc(-1) == nullptr);
18+
19+
// sqliteMemMalloc with a zero size returns a nullptr.
20+
KJ_EXPECT(sqliteMemMalloc(0) == nullptr);
21+
22+
// sqliteMemMalloc with a positive size returns a non-nullptr.
23+
void* ptr = sqliteMemMalloc(1024);
24+
KJ_EXPECT(ptr != nullptr);
25+
26+
// sqliteMemFree on a non-nullptr does not throw.
27+
sqliteMemFree(ptr);
28+
29+
// sqliteMemMalloc with a larger size returns a non-nullptr. Note that the size allocated here
30+
// needs to be less than 1 MiB below, or the pointer address will be reused when requesting 1 MiB.
31+
ptr = sqliteMemMalloc(1024 * 512);
32+
if (expectEnforcedLimits) {
33+
KJ_EXPECT(ptr == nullptr);
34+
} else {
35+
KJ_EXPECT(ptr != nullptr);
36+
}
37+
sqliteMemFree(ptr);
38+
39+
// sqliteMemFree on a nullptr does not throw.
40+
sqliteMemFree(nullptr);
41+
42+
// sqliteMemRealloc(nullptr, <n>) should behave as sqliteMemMalloc(<n>).
43+
ptr = sqliteMemRealloc(nullptr, -1);
44+
KJ_EXPECT(ptr == nullptr);
45+
ptr = sqliteMemRealloc(nullptr, 0);
46+
KJ_EXPECT(ptr == nullptr);
47+
ptr = sqliteMemRealloc(nullptr, 1024);
48+
KJ_EXPECT(ptr != nullptr);
49+
sqliteMemFree(ptr);
50+
51+
// sqliteMemRealloc should return the same pointer if the same size is already allocated by
52+
// sqliteMemMalloc.
53+
ptr = sqliteMemMalloc(1024);
54+
KJ_EXPECT(ptr != nullptr);
55+
void* new_ptr = sqliteMemRealloc(ptr, 1024);
56+
KJ_EXPECT(ptr == new_ptr);
57+
KJ_EXPECT(ptr != nullptr);
58+
59+
// sqliteMemRealloc should return the same pointer if the same size is already allocated by
60+
// sqliteMemRealloc.
61+
new_ptr = sqliteMemRealloc(ptr, 1024);
62+
KJ_EXPECT(ptr == new_ptr);
63+
KJ_EXPECT(ptr != nullptr);
64+
65+
// sqliteMemRealloc should return a different pointer if a larger size is requested.
66+
new_ptr = sqliteMemRealloc(ptr, 1024 * 1024);
67+
KJ_EXPECT(ptr != new_ptr);
68+
KJ_EXPECT(ptr != nullptr);
69+
if (expectEnforcedLimits) {
70+
KJ_EXPECT(new_ptr == nullptr);
71+
} else {
72+
KJ_EXPECT(new_ptr != nullptr);
73+
}
74+
sqliteMemFree(new_ptr);
75+
76+
// sqliteMemRealloc with a valid pointer and negative size returns a nullptr. Note that ptr is
77+
// implicitly freed by the preceding call to sqliteMemRealloc.
78+
ptr = sqliteMemMalloc(1024);
79+
KJ_EXPECT(ptr != nullptr);
80+
new_ptr = sqliteMemRealloc(ptr, -1);
81+
KJ_EXPECT(new_ptr == nullptr);
82+
// sqliteMemRealloc with a valid pointer and zero size returns a nullptr. Note again that ptr is
83+
// implicitly freed by the preceding call to sqliteMemRealloc.
84+
ptr = sqliteMemMalloc(1024);
85+
KJ_EXPECT(ptr != nullptr);
86+
new_ptr = sqliteMemRealloc(ptr, 0);
87+
KJ_EXPECT(new_ptr == nullptr);
88+
}
89+
90+
KJ_TEST("sqlite3_mem_methods work without SqliteMemoryScope") {
91+
test_sqlite3_mem_methods(/*expectEnforcedLimits*/ false);
92+
}
93+
94+
KJ_TEST("sqlite3_mem_methods work with SqliteMemoryScope and high limits") {
95+
size_t counter = 0;
96+
SqliteMemoryScope scope(counter, 2 * 1024 * 1024);
97+
test_sqlite3_mem_methods(/*expectEnforcedLimits*/ false);
98+
}
99+
100+
KJ_TEST("sqlite3_mem_methods work with SqliteMemoryScope and low limits") {
101+
size_t counter = 0;
102+
SqliteMemoryScope scope(counter, 512 * 1024);
103+
test_sqlite3_mem_methods(/*expectEnforcedLimits*/ true);
104+
}
105+
106+
KJ_TEST("sqlite3_mem_methods correctly modify the SqliteMemoryScope counter") {
107+
auto runThread = [&]() {
108+
size_t counter = 0;
109+
SqliteMemoryScope scope(counter, 1024 * 1024);
110+
111+
// sqliteMemMalloc followed by sqliteMemFree should result in a zero counter.
112+
void* p1 = sqliteMemMalloc(1024);
113+
KJ_EXPECT(p1 != nullptr);
114+
KJ_EXPECT(counter >= 1024);
115+
sqliteMemFree(p1);
116+
KJ_EXPECT(counter == 0);
117+
118+
// Multiple sqliteMemMalloc's should increase counter respectively.
119+
void* p2 = sqliteMemMalloc(4096);
120+
KJ_EXPECT(p2 != nullptr);
121+
KJ_EXPECT(counter >= 4096);
122+
void* p3 = sqliteMemMalloc(4096);
123+
KJ_EXPECT(p3 != nullptr);
124+
KJ_EXPECT(counter >= 8192);
125+
size_t counter_snapshot = counter;
126+
127+
// sqliteMemMalloc leaves counter unchanged when hardLimitBytes is exceeded.
128+
KJ_EXPECT(sqliteMemMalloc(1024 * 1024) == nullptr);
129+
KJ_EXPECT(counter == counter_snapshot);
130+
131+
// sqliteMemRealloc with an increase in size should increase counter respectively, but less than
132+
// a sqliteMemMalloc of equivalent size.
133+
void* p4 = sqliteMemRealloc(p2, 8192);
134+
KJ_EXPECT(p4 != nullptr);
135+
KJ_EXPECT(counter >= 12288);
136+
KJ_EXPECT(counter <= 16384);
137+
138+
// sqliteMemRealloc with an decrease in size should decrease counter respectively.
139+
void* p5 = sqliteMemRealloc(p3, 1024);
140+
KJ_EXPECT(p5 != nullptr);
141+
KJ_EXPECT(counter >= 9216);
142+
KJ_EXPECT(counter <= 12288);
143+
counter_snapshot = counter;
144+
145+
// sqliteMemRealloc leaves counter unchanged when hardLimitBytes is exceeded.
146+
KJ_EXPECT(sqliteMemRealloc(p5, 1024 * 1024) == nullptr);
147+
KJ_EXPECT(counter == counter_snapshot);
148+
149+
// sqliteMemFree on all active pointers should result in a zero counter.
150+
sqliteMemFree(p4);
151+
sqliteMemFree(p5);
152+
KJ_EXPECT(counter == 0);
153+
};
154+
155+
kj::Thread thread1(runThread);
156+
kj::Thread thread2(runThread);
157+
}
158+
159+
} // namespace
160+
} // namespace workerd

0 commit comments

Comments
 (0)