Jacek Sieka caca11b30b
Simplify txFrame protocol, improve persist performance (#3077)
* Simplify txFrame protocol, improve persist performance

To prepare forked-layers for further surgery to avoid the nesting tax,
the commit/rollback style of interacting must first be adjusted, since
it does not provide a point in time where the frame is "done" and goes
from being actively written to, to simply waiting to be persisted or
discarded.

A collateral benefit of this change is that the scheme removes some
complexity from the process by moving the "last saved block number" into
txframe along with the actual state changes thus reducing the risk that
they go "out of sync" and removing the "commit" consolidation
responsibility from ForkedChain.

* commit/rollback become checkpoint/dispose - since these are pure
in-memory constructs, there's less error handling and there's no real
"rollback" involved - dispose better implies that the instance cannot be
used and we can more aggressively clear the memory it uses
* simplified block number handling that moves to become part of txFrame
just like the data that the block number references
* avoid reparenting step by replacing the base instead of keeping a
singleton instance
* persist builds the set of changes from the bottom which helps avoid
moving changes in the top layers through each ancestor level of the
frame stack
* when using an in-memory database in tests, allow the instance to be
passed around to enable testing persist and reload logic
2025-02-17 01:51:56 +00:00

685 lines
23 KiB
Nim

# Nimbus
# Copyright (c) 2023-2025 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
{.push raises: [].}
import
std/typetraits,
eth/common/[accounts, base, hashes],
../../constants,
../[kvt, aristo],
./base/[api_tracking, base_config, base_desc, base_helpers]
export
CoreDbAccRef,
CoreDbAccount,
CoreDbCtxRef,
CoreDbErrorCode,
CoreDbError,
CoreDbKvtRef,
CoreDbPersistentTypes,
CoreDbRef,
CoreDbTxRef,
CoreDbType
when CoreDbEnableApiTracking:
import
chronicles
logScope:
topics = "core_db"
const
logTxt = "API"
when CoreDbEnableProfiling:
export
CoreDbFnInx,
CoreDbProfListRef
when CoreDbEnableCaptJournal:
import
./backend/aristo_trace
type
CoreDbCaptRef* = distinct TraceLogInstRef
func `$`(p: CoreDbCaptRef): string =
if p.distinctBase.isNil: "<nil>" else: "<capt>"
else:
import
../aristo/[
aristo_delete, aristo_desc, aristo_fetch, aristo_merge, aristo_part,
aristo_persist, aristo_tx_frame],
../kvt/[kvt_desc, kvt_utils, kvt_persist, kvt_tx_frame]
# ------------------------------------------------------------------------------
# Public context constructors and administration
# ------------------------------------------------------------------------------
proc ctx*(db: CoreDbRef): CoreDbCtxRef =
## Get the defauly context. This is a base descriptor which provides the
## KVT, MPT, the accounts descriptors as well as the transaction descriptor.
## They are kept all in sync, i.e. `persistent()` will store exactly this
## context.
##
db.defCtx
proc baseTxFrame*(db: CoreDbRef): CoreDbTxRef =
## The base tx frame is a staging are for reading and writing "almost"
## directly from/to the database without using any pending frames - when a
## transaction created using `beginTxFrame` is committed, it ultimately ends
## up in the base txframe before being persisted to the database with a
## persist call.
CoreDbTxRef(
ctx: db.ctx,
aTx: db.ctx.parent.ariApi.call(baseTxFrame, db.ctx.mpt),
kTx: db.ctx.parent.kvtApi.call(baseTxFrame, db.ctx.kvt))
# ------------------------------------------------------------------------------
# Public base descriptor methods
# ------------------------------------------------------------------------------
proc finish*(db: CoreDbRef; eradicate = false) =
## Database destructor. If the argument `eradicate` is set `false`, the
## database is left as-is and only the in-memory handlers are cleaned up.
##
## Otherwise the destructor is allowed to remove the database. This feature
## depends on the backend database. Currently, only the `AristoDbRocks` type
## backend removes the database on `true`.
##
db.setTrackNewApi BaseFinishFn
CoreDbKvtRef(db.ctx).call(finish, db.ctx.kvt, eradicate)
CoreDbAccRef(db.ctx).call(finish, db.ctx.mpt, eradicate)
db.ifTrackNewApi: debug logTxt, api, elapsed
proc `$$`*(e: CoreDbError): string =
## Pretty print error symbol
##
e.toStr()
proc persist*(
db: CoreDbRef;
txFrame: CoreDbTxRef;
) =
## This function persists changes up to and including the given frame to the
## database.
##
db.setTrackNewApi BasePersistFn
# TODO these backend functions coud maybe be hidden behind an abstraction
# layer - or... the abstraction layer could be removed from everywhere
# else since it's not really needed
let
kvtBatch = db.defCtx.kvt.backend.putBegFn()
mptBatch = db.defCtx.mpt.backend.putBegFn()
if kvtBatch.isOk() and mptBatch.isOk():
# TODO the `persist` api stages changes but does not actually persist - a
# separate "actually-write" api is needed so the changes from both
# kvt and ari can be staged and then written together - for this to
# happen, we need to expose the shared nature of the write batch
# to here and perform a single atomic write.
# Because there is nothing in place to handle partial failures (ie
# kvt changes written to memory but not to disk because of an aristo
# error), we have to panic instead.
CoreDbKvtRef(db.ctx).call(persist, db.ctx.kvt, kvtBatch[], txFrame.kTx)
CoreDbAccRef(db.ctx).call(persist, db.ctx.mpt, mptBatch[], txFrame.aTx)
db.defCtx.kvt.backend.putEndFn(kvtBatch[]).isOkOr:
raiseAssert $api & ": " & $error
db.defCtx.mpt.backend.putEndFn(mptBatch[]).isOkOr:
raiseAssert $api & ": " & $error
else:
discard kvtBatch.expect($api & ": should always be able to create batch")
discard mptBatch.expect($api & ": should always be able to create batch")
db.ifTrackNewApi: debug logTxt, api, elapsed, blockNumber, result
proc stateBlockNumber*(db: CoreDbTxRef): BlockNumber =
## This function returns the block number stored with the latest `persist()`
## directive.
##
db.setTrackNewApi BaseStateBlockNumberFn
result = block:
let rc = db.ctx.parent.ariApi.call(fetchLastCheckpoint, db.aTx)
if rc.isOk:
rc.value.BlockNumber
else:
0u64
db.ifTrackNewApi: debug logTxt, api, elapsed, result
proc verify*(
db: CoreDbRef | CoreDbAccRef;
proof: openArray[seq[byte]];
root: Hash32;
path: Hash32;
): CoreDbRc[Opt[seq[byte]]] =
## Variant of `verify()`.
template mpt: untyped =
when db is CoreDbRef:
CoreDbAccRef(db.defCtx)
else:
db
mpt.setTrackNewApi BaseVerifyFn
result = block:
let rc = mpt.call(partUntwigPath, proof, root, path)
if rc.isOk:
ok(rc.value)
else:
err(rc.error.toError($api, ProofVerify))
mpt.ifTrackNewApi: debug logTxt, api, elapsed, result
# ------------------------------------------------------------------------------
# Public key-value table methods
# ------------------------------------------------------------------------------
proc getKvt*(ctx: CoreDbCtxRef): CoreDbKvtRef =
## This function retrieves the common base object shared with other KVT
## descriptors. Any changes are immediately visible to subscribers.
## On destruction (when the constructed object gets out of scope), changes
## are not saved to the backend database but are still cached and available.
##
CoreDbKvtRef(ctx)
# ----------- KVT ---------------
proc get*(kvt: CoreDbTxRef; key: openArray[byte]): CoreDbRc[seq[byte]] =
## This function always returns a non-empty `seq[byte]` or an error code.
kvt.setTrackNewApi KvtGetFn
result = block:
let rc = kvt.ctx.parent.kvtApi.call(get, kvt.kTx, key)
if rc.isOk:
ok(rc.value)
elif rc.error == GetNotFound:
err(rc.error.toError($api, KvtNotFound))
else:
err(rc.error.toError $api)
kvt.ifTrackNewApi: debug logTxt, api, elapsed, key=key.toStr, result
proc getOrEmpty*(kvt: CoreDbTxRef; key: openArray[byte]): CoreDbRc[seq[byte]] =
## Variant of `get()` returning an empty `seq[byte]` if the key is not found
## on the database.
##
kvt.setTrackNewApi KvtGetOrEmptyFn
result = block:
let rc = kvt.ctx.parent.kvtApi.call(get, kvt.kTx, key)
if rc.isOk:
ok(rc.value)
elif rc.error == GetNotFound:
CoreDbRc[seq[byte]].ok(EmptyBlob)
else:
err(rc.error.toError $api)
kvt.ifTrackNewApi: debug logTxt, api, elapsed, key=key.toStr, result
proc len*(kvt: CoreDbTxRef; key: openArray[byte]): CoreDbRc[int] =
## This function returns the size of the value associated with `key`.
kvt.setTrackNewApi KvtLenFn
result = block:
let rc = kvt.ctx.parent.kvtApi.call(len, kvt.kTx, key)
if rc.isOk:
ok(rc.value)
elif rc.error == GetNotFound:
err(rc.error.toError($api, KvtNotFound))
else:
err(rc.error.toError $api)
kvt.ifTrackNewApi: debug logTxt, api, elapsed, key=key.toStr, result
proc del*(kvt: CoreDbTxRef; key: openArray[byte]): CoreDbRc[void] =
kvt.setTrackNewApi KvtDelFn
result = block:
let rc = kvt.ctx.parent.kvtApi.call(del, kvt.kTx, key)
if rc.isOk:
ok()
else:
err(rc.error.toError $api)
kvt.ifTrackNewApi: debug logTxt, api, elapsed, key=key.toStr, result
proc put*(
kvt: CoreDbTxRef;
key: openArray[byte];
val: openArray[byte];
): CoreDbRc[void] =
kvt.setTrackNewApi KvtPutFn
result = block:
let rc = kvt.ctx.parent.kvtApi.call(put, kvt.kTx, key, val)
if rc.isOk:
ok()
else:
err(rc.error.toError $api)
kvt.ifTrackNewApi:
debug logTxt, api, elapsed, key=key.toStr, val=val.toLenStr, result
proc hasKeyRc*(kvt: CoreDbTxRef; key: openArray[byte]): CoreDbRc[bool] =
## For the argument `key` return `true` if `get()` returned a value on
## that argument, `false` if it returned `GetNotFound`, and an error
## otherwise.
##
kvt.setTrackNewApi KvtHasKeyRcFn
result = block:
let rc = kvt.ctx.parent.kvtApi.call(hasKeyRc, kvt.kTx, key)
if rc.isOk:
ok(rc.value)
else:
err(rc.error.toError $api)
kvt.ifTrackNewApi: debug logTxt, api, elapsed, key=key.toStr, result
proc hasKey*(kvt: CoreDbTxRef; key: openArray[byte]): bool =
## Simplified version of `hasKeyRc` where `false` is returned instead of
## an error.
##
## This function prototype is in line with the `hasKey` function for
## `Tables`.
##
kvt.setTrackNewApi KvtHasKeyFn
result = kvt.ctx.parent.kvtApi.call(hasKeyRc, kvt.kTx, key).valueOr: false
kvt.ifTrackNewApi: debug logTxt, api, elapsed, key=key.toStr, result
# ------------------------------------------------------------------------------
# Public methods for accounts
# ------------------------------------------------------------------------------
proc getAccounts*(ctx: CoreDbCtxRef): CoreDbAccRef =
## Accounts column constructor, will defect on failure.
##
ctx.setTrackNewApi CtxGetAccountsFn
result = CoreDbAccRef(ctx)
ctx.ifTrackNewApi: debug logTxt, api, elapsed
# ----------- accounts ---------------
proc proof*(
acc: CoreDbTxRef;
accPath: Hash32;
): CoreDbRc[(seq[seq[byte]],bool)] =
## On the accounts MPT, collect the nodes along the `accPath` interpreted as
## path. Return these path nodes as a chain of rlp-encoded blobs followed
## by a bool value which is `true` if the `key` path exists in the database,
## and `false` otherwise. In the latter case, the chain of rlp-encoded blobs
## are the nodes proving that the `key` path does not exist.
##
acc.setTrackNewApi AccProofFn
result = block:
let rc = acc.ctx.parent.ariApi.call(partAccountTwig, acc.aTx, accPath)
if rc.isOk:
ok(rc.value)
else:
err(rc.error.toError($api, ProofCreate))
acc.ifTrackNewApi: debug logTxt, api, elapsed, result
proc fetch*(
acc: CoreDbTxRef;
accPath: Hash32;
): CoreDbRc[CoreDbAccount] =
## Fetch the account data record for the particular account indexed by
## the key `accPath`.
##
acc.setTrackNewApi AccFetchFn
result = block:
let rc = acc.ctx.parent.ariApi.call(fetchAccountRecord, acc.aTx, accPath)
if rc.isOk:
ok(rc.value)
elif rc.error == FetchPathNotFound:
err(rc.error.toError($api, AccNotFound))
else:
err(rc.error.toError $api)
acc.ifTrackNewApi: debug logTxt, api, elapsed, accPath=($$accPath), result
proc delete*(
acc: CoreDbTxRef;
accPath: Hash32;
): CoreDbRc[void] =
## Delete the particular account indexed by the key `accPath`. This
## will also destroy an associated storage area.
##
acc.setTrackNewApi AccDeleteFn
result = block:
let rc = acc.ctx.parent.ariApi.call(deleteAccountRecord, acc.aTx, accPath)
if rc.isOk:
ok()
elif rc.error == DelPathNotFound:
# TODO: Would it be conseqient to just return `ok()` here?
err(rc.error.toError($api, AccNotFound))
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath), result
proc clearStorage*(
acc: CoreDbTxRef;
accPath: Hash32;
): CoreDbRc[void] =
## Delete all data slots from the storage area associated with the
## particular account indexed by the key `accPath`.
##
acc.setTrackNewApi AccClearStorageFn
result = block:
let rc = acc.ctx.parent.ariApi.call(deleteStorageTree, acc.aTx, accPath)
if rc.isOk or rc.error in {DelStoRootMissing,DelStoAccMissing}:
ok()
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath), result
proc merge*(
acc: CoreDbTxRef;
accPath: Hash32;
accRec: CoreDbAccount;
): CoreDbRc[void] =
## Add or update the argument account data record `account`. Note that the
## `account` argument uniquely idendifies the particular account address.
##
acc.setTrackNewApi AccMergeFn
result = block:
let rc = acc.ctx.parent.ariApi.call(mergeAccountRecord, acc.aTx, accPath, accRec)
if rc.isOk:
ok()
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath), result
proc hasPath*(
acc: CoreDbTxRef;
accPath: Hash32;
): CoreDbRc[bool] =
## Would be named `contains` if it returned `bool` rather than `Result[]`.
##
acc.setTrackNewApi AccHasPathFn
result = block:
let rc = acc.ctx.parent.ariApi.call(hasPathAccount, acc.aTx, accPath)
if rc.isOk:
ok(rc.value)
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath), result
proc getStateRoot*(acc: CoreDbTxRef): CoreDbRc[Hash32] =
## This function retrieves the Merkle state hash of the accounts
## column (if available.)
acc.setTrackNewApi AccStateFn
result = block:
let rc = acc.ctx.parent.ariApi.call(fetchStateRoot, acc.aTx)
if rc.isOk:
ok(rc.value)
else:
err(rc.error.toError $api)
acc.ifTrackNewApi: debug logTxt, api, elapsed, result
# ------------ storage ---------------
proc slotProof*(
acc: CoreDbTxRef;
accPath: Hash32;
stoPath: Hash32;
): CoreDbRc[(seq[seq[byte]],bool)] =
## On the storage MPT related to the argument account `acPath`, collect the
## nodes along the `stoPath` interpreted as path. Return these path nodes as
## a chain of rlp-encoded blobs followed by a bool value which is `true` if
## the `key` path exists in the database, and `false` otherwise. In the
## latter case, the chain of rlp-encoded blobs are the nodes proving that
## the `key` path does not exist.
##
## Note that the function always returns an error unless the `accPath` is
## valid.
##
acc.setTrackNewApi AccSlotProofFn
result = block:
let rc = acc.ctx.parent.ariApi.call(partStorageTwig, acc.aTx, accPath, stoPath)
if rc.isOk:
ok(rc.value)
else:
err(rc.error.toError($api, ProofCreate))
acc.ifTrackNewApi: debug logTxt, api, elapsed, result
proc slotFetch*(
acc: CoreDbTxRef;
accPath: Hash32;
stoPath: Hash32;
): CoreDbRc[UInt256] =
## Like `fetch()` but with cascaded index `(accPath,slot)`.
acc.setTrackNewApi AccSlotFetchFn
result = block:
let rc = acc.ctx.parent.ariApi.call(fetchStorageData, acc.aTx, accPath, stoPath)
if rc.isOk:
ok(rc.value)
elif rc.error == FetchPathNotFound:
err(rc.error.toError($api, StoNotFound))
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath),
stoPath=($$stoPath), result
proc slotDelete*(
acc: CoreDbTxRef;
accPath: Hash32;
stoPath: Hash32;
): CoreDbRc[void] =
## Like `delete()` but with cascaded index `(accPath,slot)`.
acc.setTrackNewApi AccSlotDeleteFn
result = block:
let rc = acc.ctx.parent.ariApi.call(deleteStorageData, acc.aTx, accPath, stoPath)
if rc.isOk or rc.error == DelStoRootMissing:
# The second `if` clause is insane but legit: A storage column was
# announced for an account but no data have been added, yet.
ok()
elif rc.error == DelPathNotFound:
err(rc.error.toError($api, StoNotFound))
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath),
stoPath=($$stoPath), result
proc slotHasPath*(
acc: CoreDbTxRef;
accPath: Hash32;
stoPath: Hash32;
): CoreDbRc[bool] =
## Like `hasPath()` but with cascaded index `(accPath,slot)`.
acc.setTrackNewApi AccSlotHasPathFn
result = block:
let rc = acc.ctx.parent.ariApi.call(hasPathStorage, acc.aTx, accPath, stoPath)
if rc.isOk:
ok(rc.value)
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath),
stoPath=($$stoPath), result
proc slotMerge*(
acc: CoreDbTxRef;
accPath: Hash32;
stoPath: Hash32;
stoData: UInt256;
): CoreDbRc[void] =
## Like `merge()` but with cascaded index `(accPath,slot)`.
acc.setTrackNewApi AccSlotMergeFn
result = block:
let rc = acc.ctx.parent.ariApi.call(mergeStorageData, acc.aTx, accPath, stoPath, stoData)
if rc.isOk:
ok()
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath),
stoPath=($$stoPath), stoData, result
proc slotStorageRoot*(
acc: CoreDbTxRef;
accPath: Hash32;
): CoreDbRc[Hash32] =
## This function retrieves the Merkle state hash of the storage data
## column (if available) related to the account indexed by the key
## `accPath`.`.
##
acc.setTrackNewApi AccSlotStorageRootFn
result = block:
let rc = acc.ctx.parent.ariApi.call(fetchStorageRoot, acc.aTx, accPath)
if rc.isOk:
ok(rc.value)
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath), result
proc slotStorageEmpty*(
acc: CoreDbTxRef;
accPath: Hash32;
): CoreDbRc[bool] =
## This function returns `true` if the storage data column is empty or
## missing.
##
acc.setTrackNewApi AccSlotStorageEmptyFn
result = block:
let rc = acc.ctx.parent.ariApi.call(hasStorageData, acc.aTx, accPath)
if rc.isOk:
ok(not rc.value)
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath), result
proc slotStorageEmptyOrVoid*(
acc: CoreDbTxRef;
accPath: Hash32;
): bool =
## Convenience wrapper, returns `true` where `slotStorageEmpty()` would fail.
acc.setTrackNewApi AccSlotStorageEmptyOrVoidFn
result = block:
let rc = acc.ctx.parent.ariApi.call(hasStorageData, acc.aTx, accPath)
if rc.isOk:
not rc.value
else:
true
acc.ifTrackNewApi:
debug logTxt, api, elapsed, accPath=($$accPath), result
# ------------- other ----------------
proc recast*(
acc: CoreDbTxRef;
accPath: Hash32;
accRec: CoreDbAccount;
): CoreDbRc[Account] =
## Complete the argument `accRec` to the portable Ethereum representation
## of an account statement. This conversion may fail if the storage colState
## hash (see `slotStorageRoot()` above) is currently unavailable.
##
acc.setTrackNewApi AccRecastFn
let rc = acc.ctx.parent.ariApi.call(fetchStorageRoot, acc.aTx, accPath)
result = block:
if rc.isOk:
ok Account(
nonce: accRec.nonce,
balance: accRec.balance,
codeHash: accRec.codeHash,
storageRoot: rc.value)
else:
err(rc.error.toError $api)
acc.ifTrackNewApi:
let storageRoot = if rc.isOk: $$(rc.value) else: "n/a"
debug logTxt, api, elapsed, accPath=($$accPath), storageRoot, result
# ------------------------------------------------------------------------------
# Public transaction related methods
# ------------------------------------------------------------------------------
proc txFrameBegin*(ctx: CoreDbCtxRef, parent: CoreDbTxRef): CoreDbTxRef =
## Constructor
##
ctx.setTrackNewApi BaseNewTxFn
let
kTx = CoreDbKvtRef(ctx).call(txFrameBegin, ctx.kvt, if parent != nil: parent.kTx else: nil)
aTx = CoreDbAccRef(ctx).call(txFrameBegin, ctx.mpt, if parent != nil: parent.aTx else: nil)
result = ctx.bless CoreDbTxRef(kTx: kTx, aTx: aTx)
ctx.ifTrackNewApi:
let newLevel = CoreDbAccRef(ctx).call(level, ctx.mpt)
debug logTxt, api, elapsed, newLevel
proc checkpoint*(tx: CoreDbTxRef, blockNumber: BlockNumber) =
tx.setTrackNewApi TxCommitFn:
let prvLevel {.used.} = CoreDbAccRef(tx.ctx).call(level, tx.aTx)
CoreDbAccRef(tx.ctx).call(checkpoint, tx.aTx, blockNumber)
tx.ifTrackNewApi: debug logTxt, api, elapsed, prvLevel
proc dispose*(tx: CoreDbTxRef) =
tx.setTrackNewApi TxRollbackFn:
let prvLevel {.used.} = CoreDbAccRef(tx.ctx).call(level, tx.aTx)
CoreDbAccRef(tx.ctx).call(dispose, tx.aTx)
CoreDbKvtRef(tx.ctx).call(dispose, tx.kTx)
tx[].reset()
tx.ifTrackNewApi: debug logTxt, api, elapsed, prvLevel
proc txFrameBegin*(tx: CoreDbTxRef): CoreDbTxRef =
tx.ctx.txFrameBegin(tx)
# ------------------------------------------------------------------------------
# Public tracer methods
# ------------------------------------------------------------------------------
when CoreDbEnableCaptJournal:
proc pushCapture*(db: CoreDbRef): CoreDbCaptRef =
## ..
##
db.setTrackNewApi BasePushCaptureFn
if db.tracerHook.isNil:
db.tracerHook = TraceRecorderRef.init(db)
else:
TraceRecorderRef(db.tracerHook).push()
result = TraceRecorderRef(db.tracerHook).topInst().CoreDbCaptRef
db.ifTrackNewApi: debug logTxt, api, elapsed, result
proc level*(cpt: CoreDbCaptRef): int =
## Getter, returns the positive number of stacked instances.
##
let log = cpt.distinctBase
log.db.setTrackNewApi CptLevelFn
result = log.level()
log.db.ifTrackNewApi: debug logTxt, api, elapsed, result
proc kvtLog*(cpt: CoreDbCaptRef): seq[(seq[byte],seq[byte])] =
## Getter, returns the `Kvt` logger list for the argument instance.
##
let log = cpt.distinctBase
log.db.setTrackNewApi CptKvtLogFn
result = log.kvtLogBlobs()
log.db.ifTrackNewApi: debug logTxt, api, elapsed
proc pop*(cpt: CoreDbCaptRef) =
## Explicitely stop recording the current tracer instance and reset to
## previous level.
##
let db = cpt.distinctBase.db
db.setTrackNewApi CptPopFn
if not cpt.distinctBase.pop():
TraceRecorderRef(db.tracerHook).restore()
db.tracerHook = TraceRecorderRef(nil)
db.ifTrackNewApi: debug logTxt, api, elapsed, cpt
proc stopCapture*(db: CoreDbRef) =
## Discard capture instances. This function is equivalent to `pop()`-ing
## all instances.
##
db.setTrackNewApi CptStopCaptureFn
if not db.tracerHook.isNil:
TraceRecorderRef(db.tracerHook).restore()
db.tracerHook = TraceRecorderRef(nil)
db.ifTrackNewApi: debug logTxt, api, elapsed
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------