storage: each runtime database connection keeps a page cache sized for its role, read back at open
This commit is contained in:
@@ -878,6 +878,69 @@ pub fn applyPragmas(self: *Db, p: Pragmas) Error!void {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a runtime connection is for, which decides how much page cache it
|
||||
/// may keep.
|
||||
///
|
||||
/// SQLite's default is 2000 KiB per connection, and every page it caches is a
|
||||
/// heap block the process keeps once the cache has filled. A page that falls
|
||||
/// out of this cache is read again from the kernel's page cache, not from the
|
||||
/// disk, so each role keeps only the pages its own access pattern reuses. The
|
||||
/// sizes below are in 4 KiB pages, SQLite's default page size, against the
|
||||
/// Pi's files: `config.db` 136 KiB, `querylog.db` 107 MiB.
|
||||
pub const Role = enum {
|
||||
/// 64 pages. The Pi's whole `config.db` is 34. Its one growing table, the
|
||||
/// resolved diagnostics events, is capped at `events.Store.max_resolved_rows`
|
||||
/// and is read only when the diagnostics page asks; the configuration and
|
||||
/// the active events every request reads stay within the cache.
|
||||
config,
|
||||
/// 256 pages. One insert walks ten b-trees: the query log, its three
|
||||
/// indexes, the domains table and its unique index, and the four bucket
|
||||
/// projections. On a 107 MiB log their interior pages number about a
|
||||
/// hundred; the rest holds the rightmost leaves the appends land on and
|
||||
/// the leaves of the domains asked for most.
|
||||
querylog_writer,
|
||||
/// 64 pages. A pass runs once a day: one statement deletes the day that
|
||||
/// aged out, the four projections drop their old buckets and recompute
|
||||
/// the one the cutoff straddles, and every seventh pass a `VACUUM` streams
|
||||
/// the whole file through the pager. The pages a pass would reuse, such as
|
||||
/// the domain-index leaves its deleted rows share, come back from the
|
||||
/// kernel's cache as copies, and a transaction with more dirty pages than
|
||||
/// fit spills them to the WAL early. A larger cache would stay allocated
|
||||
/// through the 24 hours between passes to save the pass a few copies.
|
||||
querylog_retention,
|
||||
/// 256 pages. The web API's history page walks the timestamp index and
|
||||
/// its rows; the statistics read the bucket projections, which every
|
||||
/// dashboard refresh asks for again.
|
||||
querylog_reader,
|
||||
|
||||
pub fn cacheKib(role: Role) u32 {
|
||||
return switch (role) {
|
||||
.config => 256,
|
||||
.querylog_writer => 1024,
|
||||
.querylog_retention => 256,
|
||||
.querylog_reader => 1024,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// The step that makes a connection a runtime connection: sets its page cache
|
||||
/// for `role` and reads it back. Called after the schema is in place, by the
|
||||
/// code that opens a connection for the running server; migrations and CLI
|
||||
/// commands keep SQLite's default.
|
||||
pub fn initRuntimeConnection(self: *Db, role: Role) Error!void {
|
||||
const kib = role.cacheKib();
|
||||
var buf: [48]u8 = undefined;
|
||||
// A negative `cache_size` is a size in KiB; a positive one counts pages,
|
||||
// whose size depends on the database file.
|
||||
const sql = std.fmt.bufPrintZ(&buf, "PRAGMA cache_size = -{d};", .{kib}) catch unreachable;
|
||||
try self.exec(sql);
|
||||
const set = try self.queryInt("PRAGMA cache_size");
|
||||
if (set != -@as(i64, kib)) {
|
||||
log.warn("PRAGMA cache_size for a {t} connection reads {d}, expected -{d}", .{ role, set, kib });
|
||||
return error.SqliteError;
|
||||
}
|
||||
}
|
||||
|
||||
/// A write transaction.
|
||||
///
|
||||
/// Usage contract, followed everywhere in this milestone:
|
||||
@@ -1168,6 +1231,22 @@ test "applyPragmas succeeds and foreign_keys reads back as 1" {
|
||||
try testing.expectEqual(@as(i64, 1), try db.queryInt("PRAGMA foreign_keys"));
|
||||
}
|
||||
|
||||
test "a runtime connection reports its role's cache size, a migration connection the default" {
|
||||
var migrating = try openMemory();
|
||||
defer migrating.close();
|
||||
try applyPragmas(&migrating, .{});
|
||||
// SQLite's compiled-in default: 2000 KiB.
|
||||
try testing.expectEqual(@as(i64, -2000), try migrating.queryInt("PRAGMA cache_size"));
|
||||
|
||||
for (std.enums.values(Role)) |role| {
|
||||
var runtime = try openMemory();
|
||||
defer runtime.close();
|
||||
try applyPragmas(&runtime, .{});
|
||||
try initRuntimeConnection(&runtime, role);
|
||||
try testing.expectEqual(-@as(i64, role.cacheKib()), try runtime.queryInt("PRAGMA cache_size"));
|
||||
}
|
||||
}
|
||||
|
||||
test "open on a directory path returns error.CantOpen and leaks no handle" {
|
||||
var i: usize = 0;
|
||||
while (i < 1000) : (i += 1) {
|
||||
|
||||
Reference in New Issue
Block a user