Skip to content

Commit cbb4fad

Browse files
authored
Merge pull request #6044 from kantai/feat/faster-readonly-marf
Perf: faster readonly MARF
2 parents 5efe165 + b1545ee commit cbb4fad

File tree

5 files changed

+179
-17
lines changed

5 files changed

+179
-17
lines changed

stackslib/src/chainstate/burn/db/sortdb.rs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
// You should have received a copy of the GNU General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17+
use std::cell::RefCell;
1718
use std::cmp::Ordering;
1819
use std::collections::{HashMap, HashSet};
1920
use std::io::{ErrorKind, Write};
2021
use std::ops::{Deref, DerefMut};
2122
use std::str::FromStr;
23+
use std::sync::{Arc, LazyLock, Mutex};
2224
use std::{cmp, fmt, fs};
2325

26+
use clarity::util::lru_cache::LruCache;
2427
use clarity::vm::ast::ASTRules;
2528
use clarity::vm::costs::ExecutionCost;
2629
use clarity::vm::representations::{ClarityName, ContractName};
@@ -95,6 +98,9 @@ pub const REWARD_WINDOW_END: u64 = 144 * 90 + REWARD_WINDOW_START;
9598

9699
pub type BlockHeaderCache = HashMap<ConsensusHash, (Option<BlockHeaderHash>, ConsensusHash)>;
97100

101+
static DESCENDANCY_CACHE: LazyLock<Arc<Mutex<LruCache<(SortitionId, BlockHeaderHash), bool>>>> =
102+
LazyLock::new(|| Arc::new(Mutex::new(LruCache::new(2000))));
103+
98104
pub enum FindIter<R> {
99105
Found(R),
100106
Continue,
@@ -1112,12 +1118,46 @@ pub trait SortitionHandle {
11121118
test_debug!("No snapshot at height {}", block_at_burn_height);
11131119
db_error::NotFoundError
11141120
})?;
1121+
let top_sortition_id = sn.sortition_id;
1122+
1123+
let mut cache = DESCENDANCY_CACHE
1124+
.lock()
1125+
.expect("FATAL: lock poisoned in SortitionDB");
11151126

11161127
while sn.block_height >= earliest_block_height {
1128+
match cache.get(&(sn.sortition_id, potential_ancestor.clone())) {
1129+
Ok(Some(result)) => {
1130+
if sn.sortition_id != top_sortition_id {
1131+
if let Err(_) = cache
1132+
.insert_clean((top_sortition_id, potential_ancestor.clone()), result)
1133+
{
1134+
*cache = LruCache::new(2000);
1135+
}
1136+
}
1137+
return Ok(result);
1138+
}
1139+
// not cached, don't need to do anything.
1140+
Ok(None) => {}
1141+
// cache is broken, create a new one
1142+
Err(_) => {
1143+
*cache = LruCache::new(2000);
1144+
}
1145+
}
1146+
11171147
if !sn.sortition {
1148+
if let Err(_) =
1149+
cache.insert_clean((top_sortition_id, potential_ancestor.clone()), false)
1150+
{
1151+
*cache = LruCache::new(2000);
1152+
}
11181153
return Ok(false);
11191154
}
11201155
if &sn.winning_stacks_block_hash == potential_ancestor {
1156+
if let Err(_) =
1157+
cache.insert_clean((top_sortition_id, potential_ancestor.clone()), true)
1158+
{
1159+
*cache = LruCache::new(2000);
1160+
}
11211161
return Ok(true);
11221162
}
11231163

@@ -1153,6 +1193,9 @@ pub trait SortitionHandle {
11531193
}
11541194
}
11551195
}
1196+
if let Err(_) = cache.insert_clean((top_sortition_id, potential_ancestor.clone()), false) {
1197+
*cache = LruCache::new(2000);
1198+
}
11561199
return Ok(false);
11571200
}
11581201
}
@@ -2028,15 +2071,15 @@ impl<'a> SortitionHandleConn<'a> {
20282071
connection: &'a SortitionDBConn<'a>,
20292072
chain_tip: &SortitionId,
20302073
) -> Result<SortitionHandleConn<'a>, db_error> {
2031-
Ok(SortitionHandleConn {
2032-
context: SortitionHandleContext {
2074+
Ok(SortitionHandleConn::new(
2075+
&connection.index,
2076+
SortitionHandleContext {
20332077
chain_tip: chain_tip.clone(),
20342078
first_block_height: connection.context.first_block_height,
20352079
pox_constants: connection.context.pox_constants.clone(),
20362080
dryrun: connection.context.dryrun,
20372081
},
2038-
index: connection.index,
2039-
})
2082+
))
20402083
}
20412084

20422085
fn get_tip_indexed(&self, key: &str) -> Result<Option<String>, db_error> {
@@ -3723,15 +3766,15 @@ impl SortitionDBTx<'_> {
37233766

37243767
impl SortitionDBConn<'_> {
37253768
pub fn as_handle<'b>(&'b self, chain_tip: &SortitionId) -> SortitionHandleConn<'b> {
3726-
SortitionHandleConn {
3727-
index: self.index,
3728-
context: SortitionHandleContext {
3769+
SortitionHandleConn::new(
3770+
&self.index,
3771+
SortitionHandleContext {
37293772
first_block_height: self.context.first_block_height.clone(),
37303773
chain_tip: chain_tip.clone(),
37313774
pox_constants: self.context.pox_constants.clone(),
37323775
dryrun: self.context.dryrun,
37333776
},
3734-
}
3777+
)
37353778
}
37363779

37373780
/// Given a burnchain consensus hash,

stackslib/src/chainstate/stacks/index/marf.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use stacks_common::types::chainstate::{BlockHeaderHash, TrieHash, TRIEHASH_ENCOD
2525
use stacks_common::util::hash::Sha512Trunc256Sum;
2626
use stacks_common::util::log;
2727

28+
use super::storage::ReopenedTrieStorageConnection;
2829
use crate::chainstate::stacks::index::bits::{get_leaf_hash, get_node_hash, read_root_hash};
2930
use crate::chainstate::stacks::index::node::{
3031
clear_backptr, is_backptr, set_backptr, CursorError, TrieCursor, TrieNode, TrieNode16,
@@ -251,6 +252,20 @@ impl<T: MarfTrieId> MarfConnection<T> for MarfTransaction<'_, T> {
251252
}
252253
}
253254

255+
impl<T: MarfTrieId> MarfConnection<T> for ReopenedTrieStorageConnection<'_, T> {
256+
fn with_conn<F, R>(&mut self, exec: F) -> R
257+
where
258+
F: FnOnce(&mut TrieStorageConnection<T>) -> R,
259+
{
260+
let mut conn = self.connection();
261+
exec(&mut conn)
262+
}
263+
264+
fn sqlite_conn(&self) -> &Connection {
265+
self.db_conn()
266+
}
267+
}
268+
254269
impl<T: MarfTrieId> MarfConnection<T> for MARF<T> {
255270
fn with_conn<F, R>(&mut self, exec: F) -> R
256271
where
@@ -1620,6 +1635,21 @@ impl<T: MarfTrieId> MARF<T> {
16201635
})
16211636
}
16221637

1638+
/// Build a read-only storage connection which can be used for reads without modifying the
1639+
/// calling MARF struct (i.e., the tip pointer is only changed in the connection)
1640+
/// but reusing self's existing SQLite Connection (avoiding the overhead of
1641+
/// `reopen_readonly`).
1642+
pub fn reopen_connection(&self) -> Result<ReopenedTrieStorageConnection<'_, T>, Error> {
1643+
if self.open_chain_tip.is_some() {
1644+
error!(
1645+
"MARF at {} is already in the process of writing",
1646+
&self.storage.db_path
1647+
);
1648+
return Err(Error::InProgressError);
1649+
}
1650+
self.storage.reopen_connection()
1651+
}
1652+
16231653
/// Get the root trie hash at a particular block
16241654
pub fn get_root_hash_at(&mut self, block_hash: &T) -> Result<TrieHash, Error> {
16251655
self.storage.connection().get_root_hash_at(block_hash)

stackslib/src/chainstate/stacks/index/storage.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,50 @@ impl<T: MarfTrieId> TrieStorageTransientData<T> {
13101310
}
13111311
}
13121312

1313+
pub struct ReopenedTrieStorageConnection<'a, T: MarfTrieId> {
1314+
pub db_path: &'a str,
1315+
db: &'a Connection,
1316+
blobs: Option<TrieFile>,
1317+
data: TrieStorageTransientData<T>,
1318+
cache: TrieCache<T>,
1319+
bench: TrieBenchmark,
1320+
pub hash_calculation_mode: TrieHashCalculationMode,
1321+
1322+
/// row ID of a trie that represents unconfirmed state (i.e. trie state that will never become
1323+
/// part of the MARF, but nevertheless represents a persistent scratch space). If this field
1324+
/// is Some(..), then the storage connection here was used to (re-)open an unconfirmed trie
1325+
/// (via `open_unconfirmed()` or `open_block()` when `self.unconfirmed()` is `true`), or used
1326+
/// to create an unconfirmed trie (via `extend_to_unconfirmed_block()`).
1327+
unconfirmed_block_id: Option<u32>,
1328+
1329+
// used in testing in order to short-circuit block-height lookups
1330+
// when the trie struct is tested outside of marf.rs usage
1331+
#[cfg(test)]
1332+
pub test_genesis_block: Option<T>,
1333+
}
1334+
1335+
impl<'a, T: MarfTrieId> ReopenedTrieStorageConnection<'a, T> {
1336+
pub fn db_conn(&self) -> &Connection {
1337+
self.db
1338+
}
1339+
1340+
pub fn connection(&mut self) -> TrieStorageConnection<'_, T> {
1341+
TrieStorageConnection {
1342+
db: SqliteConnection::ConnRef(&self.db),
1343+
db_path: self.db_path,
1344+
data: &mut self.data,
1345+
blobs: self.blobs.as_mut(),
1346+
cache: &mut self.cache,
1347+
bench: &mut self.bench,
1348+
hash_calculation_mode: self.hash_calculation_mode,
1349+
unconfirmed_block_id: None,
1350+
1351+
#[cfg(test)]
1352+
test_genesis_block: &mut self.test_genesis_block,
1353+
}
1354+
}
1355+
}
1356+
13131357
impl<T: MarfTrieId> TrieFileStorage<T> {
13141358
pub fn connection(&mut self) -> TrieStorageConnection<'_, T> {
13151359
TrieStorageConnection {
@@ -1327,6 +1371,54 @@ impl<T: MarfTrieId> TrieFileStorage<T> {
13271371
}
13281372
}
13291373

1374+
/// Build a read-only storage connection which can be used for reads without modifying the
1375+
/// calling TrieFileStorage struct (i.e., the tip pointer is only changed in the connection)
1376+
/// but reusing the TrieFileStorage's existing SQLite Connection (avoiding the overhead of
1377+
/// `reopen_readonly`).
1378+
pub fn reopen_connection(&self) -> Result<ReopenedTrieStorageConnection<'_, T>, Error> {
1379+
let data = TrieStorageTransientData {
1380+
uncommitted_writes: self.data.uncommitted_writes.clone(),
1381+
cur_block: self.data.cur_block.clone(),
1382+
cur_block_id: self.data.cur_block_id.clone(),
1383+
1384+
read_count: 0,
1385+
read_backptr_count: 0,
1386+
read_node_count: 0,
1387+
read_leaf_count: 0,
1388+
1389+
write_count: 0,
1390+
write_node_count: 0,
1391+
write_leaf_count: 0,
1392+
1393+
trie_ancestor_hash_bytes_cache: None,
1394+
1395+
readonly: true,
1396+
unconfirmed: self.unconfirmed(),
1397+
};
1398+
// perf note: should we attempt to clone the cache
1399+
let cache = TrieCache::default();
1400+
let blobs = if self.blobs.is_some() {
1401+
Some(TrieFile::from_db_path(&self.db_path, true)?)
1402+
} else {
1403+
None
1404+
};
1405+
let bench = TrieBenchmark::new();
1406+
let hash_calculation_mode = self.hash_calculation_mode;
1407+
let unconfirmed_block_id = None;
1408+
Ok(ReopenedTrieStorageConnection {
1409+
db_path: &self.db_path,
1410+
db: &self.db,
1411+
blobs,
1412+
data,
1413+
cache,
1414+
bench,
1415+
hash_calculation_mode,
1416+
unconfirmed_block_id,
1417+
#[cfg(test)]
1418+
test_genesis_block: self.test_genesis_block.clone(),
1419+
})
1420+
}
1421+
13301422
pub fn transaction(&mut self) -> Result<TrieStorageTransaction<'_, T>, Error> {
13311423
if self.readonly() {
13321424
return Err(Error::ReadOnlyError);

stackslib/src/clarity_vm/database/marf.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,7 @@ impl MarfedKV {
263263
}
264264

265265
pub fn index_conn<C>(&self, context: C) -> IndexDBConn<'_, C, StacksBlockId> {
266-
IndexDBConn {
267-
index: &self.marf,
268-
context,
269-
}
266+
IndexDBConn::new(&self.marf, context)
270267
}
271268
}
272269

stackslib/src/util_lib/db.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -613,13 +613,13 @@ impl<'a, C, T: MarfTrieId> IndexDBConn<'a, C, T> {
613613
ancestor_block_hash: &T,
614614
tip_block_hash: &T,
615615
) -> Result<Option<u64>, Error> {
616-
get_ancestor_block_height(self.index, ancestor_block_hash, tip_block_hash)
616+
get_ancestor_block_height(&self.index, ancestor_block_hash, tip_block_hash)
617617
}
618618

619619
/// Get a value from the fork index
620620
pub fn get_indexed(&self, header_hash: &T, key: &str) -> Result<Option<String>, Error> {
621-
let mut ro_index = self.index.reopen_readonly()?;
622-
get_indexed(&mut ro_index, header_hash, key)
621+
let mut connection = self.index.reopen_connection()?;
622+
get_indexed(&mut connection, header_hash, key)
623623
}
624624

625625
pub fn conn(&self) -> &DBConn {
@@ -727,7 +727,7 @@ pub fn get_ancestor_block_hash<T: MarfTrieId>(
727727
tip_block_hash: &T,
728728
) -> Result<Option<T>, Error> {
729729
assert!(block_height <= u32::MAX as u64);
730-
let mut read_only = index.reopen_readonly()?;
730+
let mut read_only = index.reopen_connection()?;
731731
let bh = read_only.get_block_at_height(block_height as u32, tip_block_hash)?;
732732
Ok(bh)
733733
}
@@ -738,7 +738,7 @@ pub fn get_ancestor_block_height<T: MarfTrieId>(
738738
ancestor_block_hash: &T,
739739
tip_block_hash: &T,
740740
) -> Result<Option<u64>, Error> {
741-
let mut read_only = index.reopen_readonly()?;
741+
let mut read_only = index.reopen_connection()?;
742742
let height_opt = read_only
743743
.get_block_height(ancestor_block_hash, tip_block_hash)?
744744
.map(|height| height as u64);

0 commit comments

Comments
 (0)