Cache account path storage id (#2443)

The storage id is frequently accessed when executing contract code and
finding the path via the database requires several hops making the
process slow - here, we add a cache to keep the most recently used
account storage id:s in memory.

A possible future improvement would be to cache all account accesses so
that for example updating the balance doesn't cause several hikes.
This commit is contained in:
Jacek Sieka 2024-07-03 17:58:25 +02:00 committed by GitHub
parent 989f20a740
commit 443c6d1f8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 81 additions and 7 deletions

View File

@ -346,6 +346,7 @@ proc deleteAccountRecord*(
# Delete storage tree if present # Delete storage tree if present
if stoID.isValid: if stoID.isValid:
? db.delSubTreeImpl stoID ? db.delSubTreeImpl stoID
db.layersPutStoID(accPath, VertexID(0))
db.deleteImpl(hike).isOkOr: db.deleteImpl(hike).isOkOr:
return err(error[1]) return err(error[1])
@ -442,6 +443,7 @@ proc deleteStorageData*(
# De-register the deleted storage tree from the account record # De-register the deleted storage tree from the account record
let leaf = wpAcc.vtx.dup # Dup on modify let leaf = wpAcc.vtx.dup # Dup on modify
leaf.lData.stoID = VertexID(0) leaf.lData.stoID = VertexID(0)
db.layersPutStoID(accPath, VertexID(0))
db.layersPutVtx(VertexID(1), wpAcc.vid, leaf) db.layersPutVtx(VertexID(1), wpAcc.vid, leaf)
db.layersResKey(VertexID(1), wpAcc.vid) db.layersResKey(VertexID(1), wpAcc.vid)
ok(true) ok(true)
@ -472,6 +474,7 @@ proc deleteStorageTree*(
# De-register the deleted storage tree from the accounts record # De-register the deleted storage tree from the accounts record
let leaf = wpAcc.vtx.dup # Dup on modify let leaf = wpAcc.vtx.dup # Dup on modify
leaf.lData.stoID = VertexID(0) leaf.lData.stoID = VertexID(0)
db.layersPutStoID(accPath, VertexID(0))
db.layersPutVtx(VertexID(1), wpAcc.vid, leaf) db.layersPutVtx(VertexID(1), wpAcc.vid, leaf)
db.layersResKey(VertexID(1), wpAcc.vid) db.layersResKey(VertexID(1), wpAcc.vid)
ok() ok()

View File

@ -82,6 +82,16 @@ proc deltaPersistent*(
be.putLstFn(writeBatch, lSst) be.putLstFn(writeBatch, lSst)
? be.putEndFn writeBatch # Finalise write batch ? be.putEndFn writeBatch # Finalise write batch
# Copy back updated payloads - these only change when storage is written to
# the first time or when storage is removed completely
for accPath, stoID in db.balancer.accSids:
let accKey = accPath.to(AccountKey)
if stoID.isValid:
if not db.accSids.lruUpdate(accKey, stoID):
discard db.accSids.lruAppend(accKey, stoID, accLruSize)
else:
db.accSids.del(accKey)
# Update dudes and this descriptor # Update dudes and this descriptor
? updateSiblings.update().commit() ? updateSiblings.update().commit()
ok() ok()

View File

@ -23,6 +23,7 @@
import import
std/[hashes, sets, tables], std/[hashes, sets, tables],
stew/keyed_queue,
eth/common, eth/common,
results, results,
./aristo_constants, ./aristo_constants,
@ -33,7 +34,11 @@ from ./aristo_desc/desc_backend
# Not auto-exporting backend # Not auto-exporting backend
export export
aristo_constants, desc_error, desc_identifiers, desc_structural aristo_constants, desc_error, desc_identifiers, desc_structural, keyed_queue
const
accLruSize* = 1024 * 1024
# LRU cache size for accounts that have storage
type type
AristoTxRef* = ref object AristoTxRef* = ref object
@ -58,6 +63,12 @@ type
centre: AristoDbRef ## Link to peer with write permission centre: AristoDbRef ## Link to peer with write permission
peers: HashSet[AristoDbRef] ## List of all peers peers: HashSet[AristoDbRef] ## List of all peers
AccountKey* = distinct ref Hash256
# `ref` version of the account path / key
# `KeyedQueue` is inefficient for large keys, so we have to use this ref
# workaround to not experience a memory explosion in the account cache
# TODO rework KeyedQueue to deal with large keys and/or heterogenous lookup
AristoDbRef* = ref object AristoDbRef* = ref object
## Three tier database object supporting distributed instances. ## Three tier database object supporting distributed instances.
top*: LayerRef ## Database working layer, mutable top*: LayerRef ## Database working layer, mutable
@ -72,6 +83,13 @@ type
# Debugging data below, might go away in future # Debugging data below, might go away in future
xMap*: Table[HashKey,HashSet[VertexID]] ## For pretty printing/debugging xMap*: Table[HashKey,HashSet[VertexID]] ## For pretty printing/debugging
accSids*: KeyedQueue[AccountKey, VertexID]
## Account path to storage id cache, for contract accounts - storage is
## frequently accessed by account path when contracts interact with it -
## this cache ensures that we don't have to re-travers the storage trie
## path for every such interaction - a better solution would probably be
## to cache this in a type exposed to the high-level API
AristoDbAction* = proc(db: AristoDbRef) {.gcsafe, raises: [].} AristoDbAction* = proc(db: AristoDbRef) {.gcsafe, raises: [].}
## Generic call back function/closure. ## Generic call back function/closure.
@ -79,6 +97,17 @@ type
# Public helpers # Public helpers
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
template hash*(a: AccountKey): Hash =
mixin hash
hash((ref Hash256)(a)[])
template `==`*(a, b: AccountKey): bool =
mixin `==`
(ref Hash256)(a)[] == (ref Hash256)(b)[]
template to*(a: Hash256, T: type AccountKey): T =
AccountKey((ref Hash256)(data: a.data))
func getOrVoid*[W](tab: Table[W,VertexRef]; w: W): VertexRef = func getOrVoid*[W](tab: Table[W,VertexRef]; w: W): VertexRef =
tab.getOrDefault(w, VertexRef(nil)) tab.getOrDefault(w, VertexRef(nil))

View File

@ -112,6 +112,8 @@ type
kMap*: Table[VertexID,HashKey] ## Merkle hash key mapping kMap*: Table[VertexID,HashKey] ## Merkle hash key mapping
vTop*: VertexID ## Last used vertex ID vTop*: VertexID ## Last used vertex ID
accSids*: Table[Hash256, VertexID] ## Account path -> stoID
LayerFinalRef* = ref object LayerFinalRef* = ref object
## Final tables fully supersede tables on lower layers when stacked as a ## Final tables fully supersede tables on lower layers when stacked as a
## whole. Missing entries on a higher layers are the final state (for the ## whole. Missing entries on a higher layers are the final state (for the

View File

@ -17,7 +17,7 @@ import
std/typetraits, std/typetraits,
eth/common, eth/common,
results, results,
"."/[aristo_compute, aristo_desc, aristo_get, aristo_hike] "."/[aristo_compute, aristo_desc, aristo_get, aristo_layers, aristo_hike]
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Private functions # Private functions
@ -116,13 +116,22 @@ proc fetchAccountHike*(
ok(move(hike)) ok(move(hike))
proc fetchStorageID*( proc fetchStorageID*(
db: AristoDbRef; db: AristoDbRef;
accPath: Hash256; accPath: Hash256;
): Result[VertexID,AristoError] = ): Result[VertexID,AristoError] =
## Public helper function fro retrieving a storage (vertex) ID for a ## Public helper function fro retrieving a storage (vertex) ID for a
## given account. ## given account.
if (let stoID = db.layersGetStoID(accPath); stoID.isSome()):
if not stoID[].isValid():
return err(FetchPathNotFound)
return ok stoID[]
let accKey = accPath.to(AccountKey)
if (let stoID = db.accSids.lruFetch(accKey); stoID.isSome()):
return ok stoID[]
let let
payload = db.retrievePayload(VertexID(1), accPath.data).valueOr: payload = db.retrievePayload(VertexID(1), accPath.data).valueOr:
if error == FetchAccInaccessible: if error == FetchAccInaccessible:
@ -134,7 +143,9 @@ proc fetchStorageID*(
if not stoID.isValid: if not stoID.isValid:
return err(FetchPathNotFound) return err(FetchPathNotFound)
ok stoID # If we didn't find a cached storage ID in the layers, we must be using the
# database version which we cache here, even across database commits
ok db.accSids.lruAppend(accKey, stoID, accLruSize)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Public functions # Public functions

View File

@ -108,6 +108,16 @@ func layerGetProofVidOrVoid*(db: AristoDbRef; key: HashKey): VertexID =
## argument `key` refers to a link key of a registered proof node. ## argument `key` refers to a link key of a registered proof node.
db.top.final.fRpp.getOrVoid key db.top.final.fRpp.getOrVoid key
func layersGetStoID*(db: AristoDbRef; accPath: Hash256): Opt[VertexID] =
db.top.delta.accSids.withValue(accPath, item):
return Opt.some(item[])
for w in db.rstack:
w.delta.accSids.withValue(accPath, item):
return Opt.some(item[])
Opt.none(VertexID)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Public functions: setter variants # Public functions: setter variants
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -167,6 +177,9 @@ func layersPutProof*(
db.top.final.pPrf.incl vid db.top.final.pPrf.incl vid
db.layersPutProof(vid, key) db.layersPutProof(vid, key)
func layersPutStoID*(db: AristoDbRef; accPath: Hash256; stoID: VertexID) =
db.top.delta.accSids[accPath] = stoID
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Public functions # Public functions
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -183,7 +196,8 @@ func layersMergeOnto*(src: LayerRef; trg: var LayerObj) =
for (vid,key) in src.delta.kMap.pairs: for (vid,key) in src.delta.kMap.pairs:
trg.delta.kMap[vid] = key trg.delta.kMap[vid] = key
trg.delta.vTop = src.delta.vTop trg.delta.vTop = src.delta.vTop
for (accPath,stoID) in src.delta.accSids.pairs:
trg.delta.accSids[accPath] = stoID
func layersCc*(db: AristoDbRef; level = high(int)): LayerRef = func layersCc*(db: AristoDbRef; level = high(int)): LayerRef =
## Provide a collapsed copy of layers up to a particular transaction level. ## Provide a collapsed copy of layers up to a particular transaction level.
@ -199,7 +213,9 @@ func layersCc*(db: AristoDbRef; level = high(int)): LayerRef =
delta: LayerDeltaRef( delta: LayerDeltaRef(
sTab: layers[0].delta.sTab.dup, # explicit dup for ref values sTab: layers[0].delta.sTab.dup, # explicit dup for ref values
kMap: layers[0].delta.kMap, kMap: layers[0].delta.kMap,
vTop: layers[^1].delta.vTop)) vTop: layers[^1].delta.vTop,
accSids: layers[0].delta.accSids,
))
# Consecutively merge other layers on top # Consecutively merge other layers on top
for n in 1 ..< layers.len: for n in 1 ..< layers.len:
@ -207,6 +223,8 @@ func layersCc*(db: AristoDbRef; level = high(int)): LayerRef =
result.delta.sTab[vid] = vtx result.delta.sTab[vid] = vtx
for (vid,key) in layers[n].delta.kMap.pairs: for (vid,key) in layers[n].delta.kMap.pairs:
result.delta.kMap[vid] = key result.delta.kMap[vid] = key
for (accPath,stoID) in layers[n].delta.accSids.pairs:
result.delta.accSids[accPath] = stoID
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Public iterators # Public iterators

View File

@ -136,6 +136,7 @@ proc mergeStorageData*(
# Make sure that there is an account that refers to that storage trie # Make sure that there is an account that refers to that storage trie
let leaf = wpAcc.vtx.dup # Dup on modify let leaf = wpAcc.vtx.dup # Dup on modify
leaf.lData.stoID = useID leaf.lData.stoID = useID
db.layersPutStoID(accPath, useID)
db.layersPutVtx(VertexID(1), wpAcc.vid, leaf) db.layersPutVtx(VertexID(1), wpAcc.vid, leaf)
db.layersResKey(VertexID(1), wpAcc.vid) db.layersResKey(VertexID(1), wpAcc.vid)
return ok() return ok()

View File

@ -419,7 +419,7 @@ proc mergePayloadUpdate(
if vid in db.pPrf: if vid in db.pPrf:
return err(MergeLeafProofModeLock) return err(MergeLeafProofModeLock)
# Update accounts storage root which is handled implicitely # Update accounts storage root which is handled implicitly
if hike.root == VertexID(1): if hike.root == VertexID(1):
payload.stoID = leafLeg.wp.vtx.lData.stoID payload.stoID = leafLeg.wp.vtx.lData.stoID