From ddab4faec52fbb63f1403652ebbc528c0fbe4f41 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 25 Aug 2026 16:30:54 -0300 Subject: [PATCH] feat(kv-store): expose read-only lmdb transactions `store.readOnlyTransaction(cb)` opens a real LMDB read transaction and keeps it open for the whole callback, so every read inside sees one snapshot. Readers do not go through the writer queue, so a write can commit while the callback runs without the callback observing it. The transaction is propagated through an AsyncLocalStorage, so container reads (`map.getAsync`, `entriesAsync`, ...) inside the callback hit the snapshot without being handed the transaction explicitly. Nested calls reuse the enclosing transaction, matching how `transactionAsync` handles recursion. Each open snapshot consumes an LMDB reader slot, so it acquires from the same semaphore as cursors; cursors bound to a snapshot skip acquisition since they reuse its slot, which the store now tracks per cursor id to avoid over-releasing on close. `readOnlyTransaction` is added to `AztecAsyncKVStore`; the other backends have no snapshot of their own that outlives an operation, so they delegate to their regular transaction, which gives the callback a consistent view. On the native side this adds START_READ_TX / CLOSE_READ_TX messages and an optional txId on GET and START_CURSOR, so the JS side can hold one LMDB read transaction open across many reads and iterations. LMDBStore gets a get() overload that reads against a caller-supplied read transaction. LMDBStoreWrapper keeps a registry of read transactions mirroring the cursor registry; because a read transaction must never be used by two threads at once, each one carries a mutex that every get and every cursor bound to it locks. Cursors record that mutex so advance_cursor serializes against sibling cursors and gets on the same snapshot. --- .../src/barretenberg/lmdblib/lmdb_store.cpp | 16 +- .../src/barretenberg/lmdblib/lmdb_store.hpp | 9 +- .../barretenberg/lmdblib/lmdb_store.test.cpp | 62 +++++ .../lmdb_store/lmdb_store_message.hpp | 21 +- .../lmdb_store/lmdb_store_wrapper.cpp | 129 +++++++--- .../lmdb_store/lmdb_store_wrapper.hpp | 24 ++ .../src/deprecated/indexeddb/store.ts | 11 + yarn-project/kv-store/src/interfaces/store.ts | 8 + yarn-project/kv-store/src/lmdb-v2/array.ts | 7 +- yarn-project/kv-store/src/lmdb-v2/map.ts | 7 +- yarn-project/kv-store/src/lmdb-v2/message.ts | 24 ++ .../kv-store/src/lmdb-v2/multi_map.ts | 7 +- .../src/lmdb-v2/read_only_transaction.test.ts | 229 ++++++++++++++++++ .../src/lmdb-v2/read_transaction.test.ts | 14 ++ .../kv-store/src/lmdb-v2/read_transaction.ts | 24 +- yarn-project/kv-store/src/lmdb-v2/store.ts | 93 ++++++- .../kv-store/src/lmdb-v2/tx-helpers.ts | 32 ++- yarn-project/kv-store/src/lmdb/store.ts | 11 + .../kv-store/src/sqlite-opfs/store.ts | 7 + .../store_spy.ts | 1 + 20 files changed, 668 insertions(+), 68 deletions(-) create mode 100644 yarn-project/kv-store/src/lmdb-v2/read_only_transaction.test.ts diff --git a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp index 15165a93d910..0e0a9e8de5c0 100644 --- a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp +++ b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.cpp @@ -113,7 +113,15 @@ void LMDBStore::put(std::vector& data) void LMDBStore::get(KeysVector& keys, OptionalValuesVector& values, const std::string& name) { - get(keys, values, get_database(name)); + get(keys, values, get_database(name), create_shared_read_transaction()); +} + +void LMDBStore::get(KeysVector& keys, + OptionalValuesVector& values, + const std::string& name, + ReadTransaction::SharedPtr tx) +{ + get(keys, values, get_database(name), std::move(tx)); } void LMDBStore::put(KeyDupValuesVector& toWrite, @@ -136,10 +144,12 @@ void LMDBStore::put(KeyDupValuesVector& toWrite, } } } -void LMDBStore::get(KeysVector& keys, OptionalValuesVector& values, LMDBDatabase::SharedPtr db) +void LMDBStore::get(KeysVector& keys, + OptionalValuesVector& values, + LMDBDatabase::SharedPtr db, + ReadTransaction::SharedPtr tx) { values.reserve(keys.size()); - ReadTransaction::SharedPtr tx = create_read_transaction(); if (!db->duplicate_keys_permitted()) { const LMDBDatabase& dbRef = *db; for (auto& k : keys) { diff --git a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp index 299dc9f65c12..6058dee0e8b1 100644 --- a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp +++ b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.hpp @@ -48,6 +48,13 @@ class LMDBStore : public LMDBStoreBase { void put(std::vector& data); void get(KeysVector& keys, OptionalValuesVector& values, const std::string& name); + /** + * @brief Reads the given keys against an already open read transaction, so the values observed belong to that + * transaction's snapshot rather than to whatever is committed at call time. + * @note LMDB read transactions are not thread safe. The caller must ensure no other operation runs against `tx` + * concurrently. + */ + void get(KeysVector& keys, OptionalValuesVector& values, const std::string& name, ReadTransaction::SharedPtr tx); void has(const KeyOptionalValuesVector& entries, std::vector& results, const std::string& name); Cursor::Ptr create_cursor(ReadTransaction::SharedPtr tx, const std::string& dbName); @@ -63,7 +70,7 @@ class LMDBStore : public LMDBStoreBase { KeyOptionalValuesVector& toDelete, const LMDBDatabase& db, LMDBWriteTransaction& tx); - void get(KeysVector& keys, OptionalValuesVector& values, LMDBDatabase::SharedPtr db); + void get(KeysVector& keys, OptionalValuesVector& values, LMDBDatabase::SharedPtr db, ReadTransaction::SharedPtr tx); // Returns the database of the given name Database::SharedPtr get_database(const std::string& name); // Returns all databases diff --git a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp index 75c677adc14b..fde09162f6d3 100644 --- a/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp +++ b/barretenberg/cpp/src/barretenberg/lmdblib/lmdb_store.test.cpp @@ -232,6 +232,68 @@ TEST_F(LMDBStoreTest, can_read_from_database) EXPECT_EQ(data[0].value(), ValuesVector{ expected }); } +TEST_F(LMDBStoreTest, reads_against_a_shared_read_transaction_see_a_stable_snapshot) +{ + LMDBStore::Ptr store = create_store(); + const std::string dbName = "Test Database"; + store->open_database(dbName); + + auto key = get_key(0); + auto original = get_value(0, 1); + auto updated = get_value(0, 2); + + KeyOptionalValuesVector toDelete; + KeyDupValuesVector toWrite = { { { key, { original } } } }; + std::vector putDatas = { { toWrite, toDelete, dbName } }; + store->put(putDatas); + + LMDBStore::ReadTransaction::SharedPtr tx = store->create_shared_read_transaction(); + + KeysVector keys = { { key } }; + OptionalValuesVector snapshot; + store->get(keys, snapshot, dbName, tx); + EXPECT_EQ(snapshot[0].value(), ValuesVector{ original }); + + // overwrite the key in a new write transaction that commits while the read transaction is still open + toWrite = { { { key, { updated } } } }; + putDatas = { { toWrite, toDelete, dbName } }; + store->put(putDatas); + + // the held transaction still sees the value as of the moment it was created + OptionalValuesVector afterWrite; + store->get(keys, afterWrite, dbName, tx); + EXPECT_EQ(afterWrite[0].value(), ValuesVector{ original }); + + // whereas a fresh read sees the new value + OptionalValuesVector latest; + store->get(keys, latest, dbName); + EXPECT_EQ(latest[0].value(), ValuesVector{ updated }); +} + +TEST_F(LMDBStoreTest, can_read_duplicates_against_a_shared_read_transaction) +{ + LMDBStore::Ptr store = create_store(); + const std::string dbName = "Test Database"; + store->open_database(dbName, true); + + int64_t numKeys = 5; + int64_t numValues = 3; + write_test_data({ dbName }, numKeys, numValues, *store); + + LMDBStore::ReadTransaction::SharedPtr tx = store->create_shared_read_transaction(); + + KeysVector keys = { { get_key(2) } }; + OptionalValuesVector values; + store->get(keys, values, dbName, tx); + + ValuesVector expected; + for (int64_t i = 0; i < numValues; i++) { + expected.emplace_back(get_value(2, i)); + } + ASSERT_TRUE(values[0].has_value()); + EXPECT_EQ(values[0].value(), expected); +} + TEST_F(LMDBStoreTest, can_not_read_from_non_existent_database) { LMDBStore::Ptr store = create_store(); diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_message.hpp b/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_message.hpp index b63bd1849f7d..2a1bb2e4328f 100644 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_message.hpp +++ b/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_message.hpp @@ -28,6 +28,9 @@ enum LMDBStoreMessageType { CLOSE, COPY_STORE, + + START_READ_TX, + CLOSE_READ_TX, }; struct OpenDatabaseRequest { @@ -39,7 +42,9 @@ struct OpenDatabaseRequest { struct GetRequest { lmdblib::KeysVector keys; std::string db; - SERIALIZATION_FIELDS(keys, db); + // When set, read against the snapshot of the read transaction with this id instead of opening a fresh one + std::optional txId; + SERIALIZATION_FIELDS(keys, db, txId); }; struct GetResponse { @@ -78,7 +83,9 @@ struct StartCursorRequest { std::optional count; std::optional onePage; std::string db; - SERIALIZATION_FIELDS(key, reverse, count, onePage, db); + // When set, iterate against the snapshot of the read transaction with this id instead of opening a fresh one + std::optional txId; + SERIALIZATION_FIELDS(key, reverse, count, onePage, db, txId); }; struct StartCursorResponse { @@ -139,6 +146,16 @@ struct CopyStoreRequest { SERIALIZATION_FIELDS(dstPath, compact); }; +struct StartReadTxResponse { + uint64_t tx; + SERIALIZATION_FIELDS(tx); +}; + +struct CloseReadTxRequest { + uint64_t tx; + SERIALIZATION_FIELDS(tx); +}; + } // namespace bb::nodejs::lmdb_store MSGPACK_ADD_ENUM(bb::nodejs::lmdb_store::LMDBStoreMessageType) diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp b/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp index b891f0e3f115..e339b1b70d51 100644 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp +++ b/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.cpp @@ -6,9 +6,11 @@ #include #include #include +#include #include #include #include +#include #include using namespace bb::nodejs; @@ -74,6 +76,9 @@ LMDBStoreWrapper::LMDBStoreWrapper(const Napi::CallbackInfo& info) _msg_processor.register_handler(LMDBStoreMessageType::OPEN_DATABASE, this, &LMDBStoreWrapper::open_database); + _msg_processor.register_handler(LMDBStoreMessageType::START_READ_TX, this, &LMDBStoreWrapper::start_read_tx); + _msg_processor.register_handler(LMDBStoreMessageType::CLOSE_READ_TX, this, &LMDBStoreWrapper::close_read_tx); + _msg_processor.register_handler(LMDBStoreMessageType::GET, this, &LMDBStoreWrapper::get); _msg_processor.register_handler(LMDBStoreMessageType::HAS, this, &LMDBStoreWrapper::has); @@ -116,6 +121,18 @@ void LMDBStoreWrapper::verify_store() const throw std::runtime_error(format("LMDB store unavailable, was close already called?")); } +// Returned by value: the copied shared_ptrs keep the transaction and its mutex alive even if the entry is erased by +// a concurrent CLOSE_READ_TX. +ReadTxData LMDBStoreWrapper::get_read_tx(uint64_t id) +{ + std::lock_guard lock(_read_tx_mutex); + auto it = _read_txs.find(id); + if (it == _read_txs.end()) { + throw std::runtime_error(format("Read transaction ", id, " not found, was it already closed?")); + } + return it->second; +} + BoolResponse LMDBStoreWrapper::open_database(const OpenDatabaseRequest& req) { verify_store(); @@ -123,12 +140,43 @@ BoolResponse LMDBStoreWrapper::open_database(const OpenDatabaseRequest& req) return { true }; } +StartReadTxResponse LMDBStoreWrapper::start_read_tx() +{ + verify_store(); + // This consumes one of the environment's reader slots until the matching CLOSE_READ_TX arrives, and pins the + // pages the snapshot references. The JS side caps how many of these can be open at once. + auto tx = _store->create_shared_read_transaction(); + uint64_t id = _next_read_tx_id++; + { + std::lock_guard lock(_read_tx_mutex); + _read_txs[id] = { tx, std::make_shared() }; + } + return { id }; +} + +BoolResponse LMDBStoreWrapper::close_read_tx(const CloseReadTxRequest& req) +{ + { + std::lock_guard lock(_read_tx_mutex); + // Cursors opened against this transaction hold their own reference, so the underlying transaction is only + // aborted once the last of them is closed too. + _read_txs.erase(req.tx); + } + return { true }; +} + GetResponse LMDBStoreWrapper::get(const GetRequest& req) { verify_store(); lmdblib::OptionalValuesVector vals; lmdblib::KeysVector keys = req.keys; - _store->get(keys, vals, req.db); + if (req.txId.has_value()) { + ReadTxData data = get_read_tx(req.txId.value()); + std::lock_guard tx_lock(*data.mtx); + _store->get(keys, vals, req.db, data.tx); + } else { + _store->get(keys, vals, req.db); + } return { vals }; } @@ -148,37 +196,56 @@ StartCursorResponse LMDBStoreWrapper::start_cursor(const StartCursorRequest& req bool one_page = req.onePage.value_or(false); lmdblib::Key key = req.key; - auto tx = _store->create_shared_read_transaction(); + lmdblib::LMDBReadTransaction::SharedPtr tx; + std::shared_ptr tx_mtx; + if (req.txId.has_value()) { + // Iterate over the snapshot the client already holds open, rather than over whatever is committed now + ReadTxData data = get_read_tx(req.txId.value()); + tx = data.tx; + tx_mtx = data.mtx; + } else { + tx = _store->create_shared_read_transaction(); + tx_mtx = std::make_shared(); + } + lmdblib::LMDBCursor::SharedPtr cursor = _store->create_cursor(tx, req.db); - bool start_ok = cursor->set_at_key(key); - - if (!start_ok) { - // we couldn't find exactly the requested key. Find the next biggest one. - start_ok = cursor->set_at_key_gte(key); - // if we found a key that's greater _and_ we want to go in reverse order - // then we're actually outside the requested bounds, we need to go back one position - if (start_ok && reverse) { - lmdblib::KeyDupValuesVector entries; - // read_prev returns `true` if there's nothing more to read - // turn this into a "not ok" because there's nothing in the db for this cursor to read - start_ok = !cursor->read_prev(1, entries); - } else if (!start_ok && reverse) { - // we couldn't find a key greater than our starting point _and_ we want to go in reverse.. - // then we start at the end of the database (the client requested to start at a key greater than anything in - // the DB) - start_ok = cursor->set_at_end(); + + bool done = false; + lmdblib::KeyDupValuesVector first_page; + { + // Never hold _cursor_mutex while taking a transaction mutex: advance_cursor takes them in this order too + std::lock_guard tx_lock(*tx_mtx); + bool start_ok = cursor->set_at_key(key); + + if (!start_ok) { + // we couldn't find exactly the requested key. Find the next biggest one. + start_ok = cursor->set_at_key_gte(key); + // if we found a key that's greater _and_ we want to go in reverse order + // then we're actually outside the requested bounds, we need to go back one position + if (start_ok && reverse) { + lmdblib::KeyDupValuesVector entries; + // read_prev returns `true` if there's nothing more to read + // turn this into a "not ok" because there's nothing in the db for this cursor to read + start_ok = !cursor->read_prev(1, entries); + } else if (!start_ok && reverse) { + // we couldn't find a key greater than our starting point _and_ we want to go in reverse.. + // then we start at the end of the database (the client requested to start at a key greater than + // anything in the DB) + start_ok = cursor->set_at_end(); + } + + // in case we're iterating in ascending order and we can't find the exact key or one that's greater than it + // then that means theren's nothing in the DB for the cursor to read } - // in case we're iterating in ascending order and we can't find the exact key or one that's greater than it - // then that means theren's nothing in the DB for the cursor to read - } + // we couldn't find a starting position + if (!start_ok) { + return { std::nullopt, {} }; + } - // we couldn't find a starting position - if (!start_ok) { - return { std::nullopt, {} }; + std::tie(done, first_page) = _advance_cursor(*cursor, reverse, page_size); } - auto [done, first_page] = _advance_cursor(*cursor, reverse, page_size); // cursor finished after reading a single page or client only wanted the first page if (done || one_page) { return { std::nullopt, first_page }; @@ -187,7 +254,7 @@ StartCursorResponse LMDBStoreWrapper::start_cursor(const StartCursorRequest& req auto cursor_id = cursor->id(); { std::lock_guard lock(_cursor_mutex); - _cursors[cursor_id] = { cursor, reverse }; + _cursors[cursor_id] = { cursor, reverse, tx_mtx }; } return { cursor_id, first_page }; @@ -212,6 +279,7 @@ AdvanceCursorResponse LMDBStoreWrapper::advance_cursor(const AdvanceCursorReques } uint32_t page_size = req.count.value_or(DEFAULT_CURSOR_PAGE_SIZE); + std::lock_guard tx_lock(*data.txMtx); auto [done, entries] = _advance_cursor(*data.cursor, data.reverse, page_size); return { entries, done }; } @@ -225,6 +293,7 @@ AdvanceCursorCountResponse LMDBStoreWrapper::advance_cursor_count(const AdvanceC data = _cursors.at(req.cursor); } + std::lock_guard tx_lock(*data.txMtx); auto [done, count] = _advance_cursor_count(*data.cursor, data.reverse, req.endKey); return { count, done }; } @@ -267,6 +336,12 @@ BoolResponse LMDBStoreWrapper::close() _cursors.clear(); } + { + // and all of the read transactions still held open on behalf of the JS side + std::lock_guard read_txs(_read_tx_mutex); + _read_txs.clear(); + } + // and finally close the database handle _store.reset(nullptr); diff --git a/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.hpp b/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.hpp index 4fea656edc5b..b7cc28028ae7 100644 --- a/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.hpp +++ b/barretenberg/cpp/src/barretenberg/nodejs_module/lmdb_store/lmdb_store_wrapper.hpp @@ -7,16 +7,30 @@ #include "barretenberg/messaging/header.hpp" #include "barretenberg/nodejs_module/lmdb_store/lmdb_store_message.hpp" #include "barretenberg/nodejs_module/util/message_processor.hpp" +#include #include #include +#include #include #include namespace bb::nodejs::lmdb_store { +/** + * @brief A read transaction kept open on behalf of the JavaScript side, together with the mutex that serializes + * access to it. LMDB read transactions may move between threads (the env is opened with `MDB_NOTLS`) but must never + * be used by two threads at once, and messages are dispatched onto a pool of libuv workers. + */ +struct ReadTxData { + lmdblib::LMDBReadTransaction::SharedPtr tx; + std::shared_ptr mtx; +}; + struct CursorData { lmdblib::LMDBCursor::SharedPtr cursor; bool reverse; + // Serializes access to the read transaction backing this cursor, which may be shared with other cursors and gets + std::shared_ptr txMtx; }; /** * @brief Manages the interaction between the JavaScript runtime and the LMDB instance. @@ -38,12 +52,22 @@ class LMDBStoreWrapper : public Napi::ObjectWrap { std::mutex _cursor_mutex; std::unordered_map _cursors; + std::mutex _read_tx_mutex; + std::unordered_map _read_txs; + std::atomic _next_read_tx_id{ 1 }; + bb::nodejs::AsyncMessageProcessor _msg_processor; void verify_store() const; + // Returns the registered read transaction, throwing if it is unknown (never opened, or already closed) + ReadTxData get_read_tx(uint64_t id); + BoolResponse open_database(const OpenDatabaseRequest& req); + StartReadTxResponse start_read_tx(); + BoolResponse close_read_tx(const CloseReadTxRequest& req); + GetResponse get(const GetRequest& req); HasResponse has(const HasRequest& req); diff --git a/yarn-project/kv-store/src/deprecated/indexeddb/store.ts b/yarn-project/kv-store/src/deprecated/indexeddb/store.ts index 592adbfdad28..6c287fc149a7 100644 --- a/yarn-project/kv-store/src/deprecated/indexeddb/store.ts +++ b/yarn-project/kv-store/src/deprecated/indexeddb/store.ts @@ -185,6 +185,17 @@ export class AztecIndexedDBStore implements AztecAsyncKVStore { }); } + /** + * Runs a callback against a consistent view of the store. + * @param callback - Function to execute against the snapshot + * @returns A promise that resolves to the return value of the callback + */ + readOnlyTransaction(callback: () => Promise): Promise { + // IndexedDB has no read-only snapshot of its own that outlives a single transaction, so the regular + // transaction is what gives the callback a consistent view. + return this.transactionAsync(callback); + } + /** * Clears all entries in the store & sub DBs. */ diff --git a/yarn-project/kv-store/src/interfaces/store.ts b/yarn-project/kv-store/src/interfaces/store.ts index 4bc41a391c98..9f3318339b0b 100644 --- a/yarn-project/kv-store/src/interfaces/store.ts +++ b/yarn-project/kv-store/src/interfaces/store.ts @@ -125,6 +125,14 @@ export interface AztecAsyncKVStore { */ transactionAsync>>(callback: () => Promise): Promise; + /** + * Runs the callback against a consistent read-only snapshot of the store. All reads performed inside the callback + * see the same committed state; concurrent writers are not blocked. Keep the callback short: an open snapshot pins + * old pages in LMDB-backed stores. Nested calls reuse the enclosing transaction. + * @param callback - The callback to execute against the snapshot + */ + readOnlyTransaction>>(callback: () => Promise): Promise; + /** Clears all entries in the store */ clear(): Promise; diff --git a/yarn-project/kv-store/src/lmdb-v2/array.ts b/yarn-project/kv-store/src/lmdb-v2/array.ts index c3d4264248f3..014cfd2d5042 100644 --- a/yarn-project/kv-store/src/lmdb-v2/array.ts +++ b/yarn-project/kv-store/src/lmdb-v2/array.ts @@ -3,9 +3,8 @@ import { Encoder } from 'msgpackr/pack'; import type { AztecAsyncArray } from '../interfaces/array.js'; import type { Value } from '../interfaces/common.js'; import type { AztecAsyncSingleton } from '../interfaces/singleton.js'; -import type { ReadTransaction } from './read_transaction.js'; import type { AztecLMDBStoreV2 } from './store.js'; -import { execInReadTx, execInWriteTx } from './tx-helpers.js'; +import { acquireReadTx, execInReadTx, execInWriteTx } from './tx-helpers.js'; import { deserializeKey, serializeKey } from './utils.js'; export class LMDBArray implements AztecAsyncArray { @@ -88,9 +87,7 @@ export class LMDBArray implements AztecAsyncArray { return; } - let tx: ReadTransaction | undefined = this.store.getCurrentWriteTx(); - const shouldClose = !tx; - tx ??= this.store.getReadTx(); + const { tx, shouldClose } = acquireReadTx(this.store); try { for await (const [key, val] of tx.iterate(serializeKey(this.prefix, 0), undefined, false, length)) { diff --git a/yarn-project/kv-store/src/lmdb-v2/map.ts b/yarn-project/kv-store/src/lmdb-v2/map.ts index d55286b7dac5..28913ea55404 100644 --- a/yarn-project/kv-store/src/lmdb-v2/map.ts +++ b/yarn-project/kv-store/src/lmdb-v2/map.ts @@ -2,9 +2,8 @@ import { Encoder } from 'msgpackr'; import type { Key, Range, Value } from '../interfaces/common.js'; import type { AztecAsyncMap } from '../interfaces/map.js'; -import type { ReadTransaction } from './read_transaction.js'; import type { AztecLMDBStoreV2 } from './store.js'; -import { execInReadTx, execInWriteTx } from './tx-helpers.js'; +import { acquireReadTx, execInReadTx, execInWriteTx } from './tx-helpers.js'; import { deserializeKey, maxKey, minKey, serializeKey } from './utils.js'; export class LMDBMap implements AztecAsyncMap { @@ -90,9 +89,7 @@ export class LMDBMap implements AztecAsyncMap; removeEntries: Array; @@ -101,6 +115,9 @@ export type LMDBRequestBody = { [LMDBMessageType.CLOSE]: void; [LMDBMessageType.COPY_STORE]: CopyStoreRequest; + + [LMDBMessageType.START_READ_TX]: void; + [LMDBMessageType.CLOSE_READ_TX]: CloseReadTxRequest; }; interface GetResponse { @@ -134,6 +151,10 @@ interface BoolResponse { ok: true; } +interface StartReadTxResponse { + tx: number; +} + interface StatsResponse { stats: Array<{ name: string; @@ -162,6 +183,9 @@ export type LMDBResponseBody = { [LMDBMessageType.CLOSE]: BoolResponse; [LMDBMessageType.COPY_STORE]: BoolResponse; + + [LMDBMessageType.START_READ_TX]: StartReadTxResponse; + [LMDBMessageType.CLOSE_READ_TX]: BoolResponse; }; export interface LMDBMessageChannel { diff --git a/yarn-project/kv-store/src/lmdb-v2/multi_map.ts b/yarn-project/kv-store/src/lmdb-v2/multi_map.ts index 39a1cd206ae5..e37a7c364509 100644 --- a/yarn-project/kv-store/src/lmdb-v2/multi_map.ts +++ b/yarn-project/kv-store/src/lmdb-v2/multi_map.ts @@ -3,9 +3,8 @@ import { MAXIMUM_KEY, toBufferKey } from 'ordered-binary'; import type { Key, Range, Value } from '../interfaces/common.js'; import type { AztecAsyncMultiMap } from '../interfaces/multi_map.js'; -import type { ReadTransaction } from './read_transaction.js'; import type { AztecLMDBStoreV2 } from './store.js'; -import { execInReadTx, execInWriteTx } from './tx-helpers.js'; +import { acquireReadTx, execInReadTx, execInWriteTx } from './tx-helpers.js'; import { deserializeKey, maxKey, minKey, serializeKey } from './utils.js'; export class LMDBMultiMap implements AztecAsyncMultiMap { @@ -83,9 +82,7 @@ export class LMDBMultiMap implements AztecAsyncM const startKey = range?.start ? serializeKey(this.prefix, range.start) : minKey(this.prefix); const endKey = range?.end ? serializeKey(this.prefix, range.end) : reverse ? maxKey(this.prefix) : undefined; - let tx: ReadTransaction | undefined = this.store.getCurrentWriteTx(); - const shouldClose = !tx; - tx ??= this.store.getReadTx(); + const { tx, shouldClose } = acquireReadTx(this.store); try { for await (const [key, vals] of tx.iterateIndex( diff --git a/yarn-project/kv-store/src/lmdb-v2/read_only_transaction.test.ts b/yarn-project/kv-store/src/lmdb-v2/read_only_transaction.test.ts new file mode 100644 index 000000000000..8d70c12f9ae8 --- /dev/null +++ b/yarn-project/kv-store/src/lmdb-v2/read_only_transaction.test.ts @@ -0,0 +1,229 @@ +import { promiseWithResolvers } from '@aztec/foundation/promise'; +import { sleep } from '@aztec/foundation/sleep'; + +import { openTmpStore } from './factory.js'; +import type { ReadTransaction } from './read_transaction.js'; +import type { AztecLMDBStoreV2 } from './store.js'; + +const testMaxReaders = 4; + +describe('AztecLMDBStoreV2 readOnlyTransaction', () => { + let store: AztecLMDBStoreV2; + + beforeEach(async () => { + store = await openTmpStore('test', true, 10 * 1024 * 1024, testMaxReaders, undefined); + }); + + afterEach(async () => { + await store.delete(); + }); + + it('keeps reads on one snapshot while a concurrent write commits', async () => { + const key = Buffer.from('foo'); + await store.transactionAsync(tx => tx.set(key, Buffer.from('v1'))); + + const opened = promiseWithResolvers(); + const writeCommitted = promiseWithResolvers(); + + const snapshotReads = store.readOnlyTransaction(async tx => { + const first = await tx.get(key); + opened.resolve(); + // The write below has to land while this snapshot is open, which is only possible because readers neither + // block the writer nor queue behind it. + await writeCommitted.promise; + const second = await tx.get(key); + return [first, second]; + }); + + await opened.promise; + await store.transactionAsync(tx => tx.set(key, Buffer.from('v2'))); + writeCommitted.resolve(); + + const [first, second] = await snapshotReads; + expect(Buffer.from(first!).toString()).toBe('v1'); + expect(Buffer.from(second!).toString()).toBe('v1'); + + // outside the snapshot the new value is visible + expect(Buffer.from((await store.getReadTx().get(key))!).toString()).toBe('v2'); + }); + + it('makes the snapshot visible to ambient container reads', async () => { + const map = store.openMap('ambient'); + await map.set('k', 'v1'); + + const opened = promiseWithResolvers(); + const writeCommitted = promiseWithResolvers(); + + const snapshotReads = store.readOnlyTransaction(async () => { + const first = await map.getAsync('k'); + opened.resolve(); + await writeCommitted.promise; + return [first, await map.getAsync('k'), await map.hasAsync('k')]; + }); + + await opened.promise; + await map.set('k', 'v2'); + writeCommitted.resolve(); + + await expect(snapshotReads).resolves.toEqual(['v1', 'v1', true]); + await expect(map.getAsync('k')).resolves.toBe('v2'); + }); + + it('does not observe rows committed after the snapshot was taken while iterating', async () => { + const map = store.openMap('iteration'); + await store.transactionAsync(async () => { + await map.set('a', '1'); + await map.set('b', '2'); + }); + + const opened = promiseWithResolvers(); + const writeCommitted = promiseWithResolvers(); + + const snapshotEntries = store.readOnlyTransaction(async () => { + // read once so the snapshot is definitely established before the write lands + await map.getAsync('a'); + opened.resolve(); + await writeCommitted.promise; + + const entries: [string, string][] = []; + for await (const entry of map.entriesAsync()) { + entries.push(entry); + } + return { entries, size: await map.sizeAsync() }; + }); + + await opened.promise; + await map.set('c', '3'); + writeCommitted.resolve(); + + await expect(snapshotEntries).resolves.toEqual({ + entries: [ + ['a', '1'], + ['b', '2'], + ], + size: 2, + }); + + // and the row is there once the snapshot is gone + await expect(map.sizeAsync()).resolves.toBe(3); + }); + + it('reuses the enclosing snapshot when nested', async () => { + const map = store.openMap('nested'); + await map.set('k', 'v1'); + + const opened = promiseWithResolvers(); + const writeCommitted = promiseWithResolvers(); + + const reads = store.readOnlyTransaction(async outerTx => { + await map.getAsync('k'); + opened.resolve(); + await writeCommitted.promise; + + return await store.readOnlyTransaction(async innerTx => ({ + sameTx: innerTx === outerTx, + value: await map.getAsync('k'), + })); + }); + + await opened.promise; + await map.set('k', 'v2'); + writeCommitted.resolve(); + + await expect(reads).resolves.toEqual({ sameTx: true, value: 'v1' }); + }); + + it('sees uncommitted writes when nested inside a write transaction', async () => { + const map = store.openMap('nested-write'); + await map.set('k', 'v1'); + + const result = await store.transactionAsync(writeTx => + store.readOnlyTransaction(async tx => { + await map.set('k', 'v2'); + return { sameTx: tx === writeTx, value: await map.getAsync('k') }; + }), + ); + + expect(result).toEqual({ sameTx: true, value: 'v2' }); + }); + + it('queues snapshots on the available reader slots instead of failing', async () => { + const map = store.openMap('slots'); + await map.set('k', 'v'); + + const release = promiseWithResolvers(); + const readerCount = testMaxReaders * 3; + const readers = Array.from({ length: readerCount }, () => + store.readOnlyTransaction(async () => { + await release.promise; + return map.getAsync('k'); + }), + ); + + // more snapshots were requested than there are reader slots, so some of these are still queued + await sleep(100); + release.resolve(); + + await expect(Promise.all(readers)).resolves.toEqual(Array.from({ length: readerCount }, () => 'v')); + }); + + it('iterates inside a snapshot even when every reader slot is taken', async () => { + const map = store.openMap('drained'); + await store.transactionAsync(async () => { + for (let i = 0; i < 20; i++) { + await map.set(String(i).padStart(2, '0'), String(i)); + } + }); + + // one snapshot per available reader slot: the store keeps one reader back for one-shot reads + const release = promiseWithResolvers(); + const opened: Promise[] = []; + const holders: Promise[] = []; + for (let i = 0; i < testMaxReaders - 1; i++) { + const isOpen = promiseWithResolvers(); + opened.push(isOpen.promise); + holders.push( + store.readOnlyTransaction(async () => { + isOpen.resolve(); + // Iterating needs a cursor bound to this snapshot. Were it to take a reader slot of its own it would + // block forever, because every slot is held by one of these snapshots. + let count = 0; + for await (const _ of map.entriesAsync()) { + count++; + } + await release.promise; + return count; + }), + ); + } + + await Promise.all(opened); + release.resolve(); + await expect(Promise.all(holders)).resolves.toEqual(Array.from({ length: testMaxReaders - 1 }, () => 20)); + + // every reader slot was handed back, so a fresh snapshot still opens afterwards + await expect(store.readOnlyTransaction(() => map.getAsync('05'))).resolves.toBe('5'); + }); + + it('rejects reads inside the callback once the store is closed', async () => { + const map = store.openMap('closing'); + await map.set('k', 'v'); + + const opened = promiseWithResolvers(); + const closed = promiseWithResolvers(); + + const snapshot = store.readOnlyTransaction(async (tx: ReadTransaction) => { + expect(await map.getAsync('k')).toBe('v'); + opened.resolve(); + await closed.promise; + await expect(tx.get(Buffer.from('anything'))).rejects.toThrow('Store is closed'); + await expect(map.getAsync('k')).rejects.toThrow('Store is closed'); + }); + + await opened.promise; + await store.close(); + closed.resolve(); + + await snapshot; + }); +}); diff --git a/yarn-project/kv-store/src/lmdb-v2/read_transaction.test.ts b/yarn-project/kv-store/src/lmdb-v2/read_transaction.test.ts index eb7059206141..1ee85fd614b6 100644 --- a/yarn-project/kv-store/src/lmdb-v2/read_transaction.test.ts +++ b/yarn-project/kv-store/src/lmdb-v2/read_transaction.test.ts @@ -31,6 +31,7 @@ describe('ReadTransaction', () => { expect(channel.sendMessage).toHaveBeenCalledWith(LMDBMessageType.GET, { db: Database.DATA, keys: [Buffer.from('test_key1')], + txId: null, }); getDeferred.resolve({ @@ -40,6 +41,18 @@ describe('ReadTransaction', () => { expect(await resp).toEqual(Buffer.from('foo')); }); + it('routes reads through the read transaction it was given', async () => { + const boundTx = new ReadTransaction(channel, 7); + channel.sendMessage.mockResolvedValue({ values: [[Buffer.from('foo')]] }); + + await expect(boundTx.get(Buffer.from('test_key1'))).resolves.toEqual(Buffer.from('foo')); + expect(channel.sendMessage).toHaveBeenCalledWith(LMDBMessageType.GET, { + db: Database.DATA, + keys: [Buffer.from('test_key1')], + txId: 7, + }); + }); + it('iterates the database', async () => { channel.sendMessage .mockResolvedValueOnce({ @@ -69,6 +82,7 @@ describe('ReadTransaction', () => { count: CURSOR_PAGE_SIZE, onePage: false, reverse: false, + txId: null, }); expect(channel.sendMessage).toHaveBeenCalledWith(LMDBMessageType.ADVANCE_CURSOR, { diff --git a/yarn-project/kv-store/src/lmdb-v2/read_transaction.ts b/yarn-project/kv-store/src/lmdb-v2/read_transaction.ts index 4806092d7e72..d9b1c364fb88 100644 --- a/yarn-project/kv-store/src/lmdb-v2/read_transaction.ts +++ b/yarn-project/kv-store/src/lmdb-v2/read_transaction.ts @@ -1,9 +1,17 @@ import { CURSOR_PAGE_SIZE, Database, type LMDBMessageChannel, LMDBMessageType } from './message.js'; +/** + * Reads against the store. When constructed with the id of a native read transaction, every read and iteration is + * routed through it, so they all observe the same snapshot; otherwise each operation reads whatever is committed at + * the time it runs. + */ export class ReadTransaction { protected open = true; - constructor(protected channel: LMDBMessageChannel) {} + constructor( + protected channel: LMDBMessageChannel, + protected readonly txId?: number, + ) {} public close(): void { if (!this.open) { @@ -20,13 +28,21 @@ export class ReadTransaction { public async get(key: Uint8Array): Promise { this.assertIsOpen(); - const response = await this.channel.sendMessage(LMDBMessageType.GET, { keys: [key], db: Database.DATA }); + const response = await this.channel.sendMessage(LMDBMessageType.GET, { + keys: [key], + db: Database.DATA, + txId: this.txId ?? null, + }); return response.values[0]?.[0] ?? undefined; } public async getIndex(key: Uint8Array): Promise { this.assertIsOpen(); - const response = await this.channel.sendMessage(LMDBMessageType.GET, { keys: [key], db: Database.INDEX }); + const response = await this.channel.sendMessage(LMDBMessageType.GET, { + keys: [key], + db: Database.INDEX, + txId: this.txId ?? null, + }); return response.values[0] ?? []; } @@ -74,6 +90,7 @@ export class ReadTransaction { count: typeof limit === 'number' ? Math.min(limit, CURSOR_PAGE_SIZE) : CURSOR_PAGE_SIZE, onePage: typeof limit === 'number' && limit < CURSOR_PAGE_SIZE, db, + txId: this.txId ?? null, }); cursor = response.cursor ?? undefined; @@ -133,6 +150,7 @@ export class ReadTransaction { count: 0, onePage: false, db, + txId: this.txId ?? null, }); cursor = response.cursor ?? undefined; diff --git a/yarn-project/kv-store/src/lmdb-v2/store.ts b/yarn-project/kv-store/src/lmdb-v2/store.ts index 51c1011ddc1e..c05365c61206 100644 --- a/yarn-project/kv-store/src/lmdb-v2/store.ts +++ b/yarn-project/kv-store/src/lmdb-v2/store.ts @@ -34,8 +34,11 @@ export class AztecLMDBStoreV2 implements AztecAsyncKVStore, LMDBMessageChannel { private open = false; private channel: MsgpackChannel; private writerCtx = new AsyncLocalStorage(); + private readerCtx = new AsyncLocalStorage(); private writerQueue = new SerialQueue(); private availableCursors: Semaphore; + // Cursors that took a reader slot of their own; cursors opened against a read-only transaction reuse its slot + private cursorsHoldingReaderSlot = new Set(); private constructor( private dataDir: string, @@ -105,6 +108,14 @@ export class AztecLMDBStoreV2 implements AztecAsyncKVStore, LMDBMessageChannel { return currentWrite; } + /** Returns the read-only transaction of the enclosing {@link readOnlyTransaction} call, if there is one. */ + public getCurrentReadTx(): ReadTransaction | undefined { + if (!this.open) { + throw new Error('Store is closed'); + } + return this.readerCtx.getStore(); + } + openMap(name: string): AztecAsyncMap { return new LMDBMap(this, name); } @@ -159,6 +170,58 @@ export class AztecLMDBStoreV2 implements AztecAsyncKVStore, LMDBMessageChannel { }); } + /** + * Runs the callback against a real LMDB read transaction, so every read inside it — whether through the supplied + * transaction or through a container such as a map, which picks it up ambiently — observes the same snapshot of the + * store. Readers never block the writer and are not serialized against it, so a write may well commit while the + * callback runs; the callback simply does not see it. + * + * Keep the callback short. An open snapshot prevents LMDB from reusing the pages it references, so the data file + * grows for as long as it is held. + * + * Nested calls reuse the enclosing transaction: inside a write transaction the callback sees that transaction's + * uncommitted writes, and inside another read-only transaction it shares its snapshot. + */ + async readOnlyTransaction>>( + callback: (tx: ReadTransaction) => Promise, + ): Promise { + if (!this.open) { + throw new Error('Store is closed'); + } + + const currentWrite = this.getCurrentWriteTx(); + if (currentWrite) { + return await callback(currentWrite); + } + + const currentRead = this.getCurrentReadTx(); + if (currentRead) { + return await callback(currentRead); + } + + // An open snapshot holds an LMDB reader slot for its whole lifetime, so it competes with cursors for them + await this.availableCursors.acquire(); + let txId: number | undefined; + try { + ({ tx: txId } = await this.sendMessage(LMDBMessageType.START_READ_TX, undefined)); + const tx = new ReadTransaction(this, txId); + try { + return await this.readerCtx.run(tx, callback, tx); + } finally { + tx.close(); + } + } finally { + if (typeof txId === 'number') { + // The store may have been closed underneath us, in which case the native side has already dropped every + // read transaction it was holding open. + await this.sendMessage(LMDBMessageType.CLOSE_READ_TX, { tx: txId }).catch(err => + this.log.warn(`Failed to close read-only transaction`, { err, txId }), + ); + } + this.availableCursors.release(); + } + } + clear(): Promise { return Promise.resolve(); } @@ -188,7 +251,13 @@ export class AztecLMDBStoreV2 implements AztecAsyncKVStore, LMDBMessageChannel { throw new Error('Store is closed'); } - if (msgType === LMDBMessageType.START_CURSOR) { + // A cursor bound to a read-only transaction iterates over a snapshot that already holds a reader slot, so it + // must not take one of its own. + const takesReaderSlot = + msgType === LMDBMessageType.START_CURSOR && + typeof (body as LMDBRequestBody[LMDBMessageType.START_CURSOR]).txId !== 'number'; + + if (takesReaderSlot) { await this.availableCursors.acquire(); } @@ -197,14 +266,20 @@ export class AztecLMDBStoreV2 implements AztecAsyncKVStore, LMDBMessageChannel { ({ response } = await this.channel.sendMessage(msgType, body)); return response; } finally { - if ( - (msgType === LMDBMessageType.START_CURSOR && response === undefined) || - msgType === LMDBMessageType.CLOSE_CURSOR || - // it's possible for a START_CURSOR command to not return a cursor (e.g. db is empty) - (msgType === LMDBMessageType.START_CURSOR && - typeof (response as LMDBResponseBody[LMDBMessageType.START_CURSOR]).cursor !== 'number') - ) { - this.availableCursors.release(); + if (takesReaderSlot) { + // the response is undefined if the message failed, and a START_CURSOR may legitimately return no cursor at + // all (e.g. the db is empty), in which case there is nothing left to release the slot later on + const cursor = (response as LMDBResponseBody[LMDBMessageType.START_CURSOR] | undefined)?.cursor; + if (typeof cursor === 'number') { + this.cursorsHoldingReaderSlot.add(cursor); + } else { + this.availableCursors.release(); + } + } else if (msgType === LMDBMessageType.CLOSE_CURSOR) { + const { cursor } = body as LMDBRequestBody[LMDBMessageType.CLOSE_CURSOR]; + if (this.cursorsHoldingReaderSlot.delete(cursor)) { + this.availableCursors.release(); + } } } } diff --git a/yarn-project/kv-store/src/lmdb-v2/tx-helpers.ts b/yarn-project/kv-store/src/lmdb-v2/tx-helpers.ts index 6679b7b17745..f8c46d732eef 100644 --- a/yarn-project/kv-store/src/lmdb-v2/tx-helpers.ts +++ b/yarn-project/kv-store/src/lmdb-v2/tx-helpers.ts @@ -11,18 +11,34 @@ export function execInWriteTx(store: AztecLMDBStoreV2, fn: (tx: WriteTransact } } +/** + * Picks the transaction an ambient read should run against: the enclosing write transaction so uncommitted writes are + * visible, else the enclosing read-only snapshot, else a fresh one-shot transaction. `shouldClose` is true only for + * that last case — the caller must not close a transaction it did not open. + */ +export function acquireReadTx(store: AztecLMDBStoreV2): { tx: ReadTransaction; shouldClose: boolean } { + const currentWrite = store.getCurrentWriteTx(); + if (currentWrite) { + return { tx: currentWrite, shouldClose: false }; + } + + const currentRead = store.getCurrentReadTx(); + if (currentRead) { + return { tx: currentRead, shouldClose: false }; + } + + return { tx: store.getReadTx(), shouldClose: true }; +} + export async function execInReadTx( store: AztecLMDBStoreV2, fn: (tx: ReadTransaction) => T | Promise, ): Promise { - const currentWrite = store.getCurrentWriteTx(); - if (currentWrite) { - return await fn(currentWrite); - } else { - const tx = store.getReadTx(); - try { - return await fn(tx); - } finally { + const { tx, shouldClose } = acquireReadTx(store); + try { + return await fn(tx); + } finally { + if (shouldClose) { tx.close(); } } diff --git a/yarn-project/kv-store/src/lmdb/store.ts b/yarn-project/kv-store/src/lmdb/store.ts index 47226942e9cb..b082cc17419d 100644 --- a/yarn-project/kv-store/src/lmdb/store.ts +++ b/yarn-project/kv-store/src/lmdb/store.ts @@ -146,6 +146,17 @@ export class AztecLmdbStore implements AztecKVStore, AztecAsyncKVStore { return await this.#rootDb.transaction(callback); } + /** + * Runs a callback against a consistent view of the store. + * @param callback - Function to execute against the snapshot + * @returns A promise that resolves to the return value of the callback + */ + async readOnlyTransaction(callback: () => Promise): Promise { + // This backend has no read-only snapshot that outlives a single operation, so the regular transaction is what + // gives the callback a consistent view. + return await this.#rootDb.transaction(callback); + } + /** * Clears all entries in the store & sub DBs atomically within a single transaction. */ diff --git a/yarn-project/kv-store/src/sqlite-opfs/store.ts b/yarn-project/kv-store/src/sqlite-opfs/store.ts index e1318f329813..a129cffec718 100644 --- a/yarn-project/kv-store/src/sqlite-opfs/store.ts +++ b/yarn-project/kv-store/src/sqlite-opfs/store.ts @@ -189,6 +189,13 @@ export class AztecSQLiteOPFSStore implements AztecAsyncKVStore { }); } + readOnlyTransaction(callback: () => Promise): Promise { + // SQLite here is single-connection, so there is no separate snapshot to open: running the callback inside a + // regular transaction is what gives its reads a consistent view. The cost over the LMDB backend is that this + // serializes against writers instead of running alongside them. + return this.transactionAsync(callback); + } + async clear(): Promise { await this.runAsync('DELETE FROM data'); } diff --git a/yarn-project/pxe/src/storage/backwards_compatibility_tests/store_spy.ts b/yarn-project/pxe/src/storage/backwards_compatibility_tests/store_spy.ts index efd616681f67..7a4c1c4b1a13 100644 --- a/yarn-project/pxe/src/storage/backwards_compatibility_tests/store_spy.ts +++ b/yarn-project/pxe/src/storage/backwards_compatibility_tests/store_spy.ts @@ -59,6 +59,7 @@ export function createStoreSpy(inner: AztecAsyncKVStore): StoreSpy { return inner.openCounter(name); }, transactionAsync: callback => inner.transactionAsync(callback), + readOnlyTransaction: callback => inner.readOnlyTransaction(callback), clear: () => inner.clear(), delete: () => inner.delete(), estimateSize: () => inner.estimateSize(),