From b68f5a034f4803756a2ce69ffaf303ec0504bcda Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:28:05 +0100 Subject: [PATCH 01/12] rust 1.94 fmt --- .../vm/analysis/type_checker/v2_05/tests/contracts.rs | 4 ++-- stacks-common/src/util/secp256k1/native.rs | 9 ++++----- stacks-common/src/util/secp256k1/wasm.rs | 7 +++---- stacks-node/src/tests/neon_integrations.rs | 4 ++-- stacks-node/src/tests/signer/multiversion.rs | 5 ++++- stacks-node/src/tests/stackerdb.rs | 3 ++- stackslib/src/chainstate/tests/mod.rs | 3 +-- stackslib/src/clarity_vm/database/ephemeral.rs | 3 +-- stackslib/src/clarity_vm/database/marf.rs | 3 +-- stackslib/src/net/api/getsortition.rs | 4 ++-- stackslib/src/net/api/poststackerdbchunk.rs | 3 +-- stackslib/src/net/chat.rs | 3 +-- stackslib/src/net/codec.rs | 3 +-- stackslib/src/net/connection.rs | 3 +-- stackslib/src/net/http/response.rs | 3 ++- stackslib/src/net/inv/epoch2x.rs | 3 +-- stackslib/src/net/mod.rs | 7 ++++--- stackslib/src/net/p2p.rs | 7 +++---- stackslib/src/net/poll.rs | 5 ++--- 19 files changed, 38 insertions(+), 44 deletions(-) diff --git a/clarity/src/vm/analysis/type_checker/v2_05/tests/contracts.rs b/clarity/src/vm/analysis/type_checker/v2_05/tests/contracts.rs index 2f52025543f..9670c1d0091 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/tests/contracts.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/tests/contracts.rs @@ -14,9 +14,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use assert_json_diff::assert_json_eq; +use assert_json_diff::{self, assert_json_eq}; +use serde_json; use stacks_common::types::StacksEpochId; -use {assert_json_diff, serde_json}; use crate::vm::ClarityVersion; use crate::vm::analysis::contract_interface_builder::build_contract_interface; diff --git a/stacks-common/src/util/secp256k1/native.rs b/stacks-common/src/util/secp256k1/native.rs index e6b7452061d..49d969311b5 100644 --- a/stacks-common/src/util/secp256k1/native.rs +++ b/stacks-common/src/util/secp256k1/native.rs @@ -14,15 +14,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use ::secp256k1; use ::secp256k1::ecdsa::{ RecoverableSignature as LibSecp256k1RecoverableSignature, RecoveryId as LibSecp256k1RecoveryID, Signature as LibSecp256k1Signature, }; pub use ::secp256k1::Error; use ::secp256k1::{ - constants as LibSecp256k1Constants, Error as LibSecp256k1Error, Message as LibSecp256k1Message, - PublicKey as LibSecp256k1PublicKey, Secp256k1, SecretKey as LibSecp256k1PrivateKey, + self, constants as LibSecp256k1Constants, Error as LibSecp256k1Error, + Message as LibSecp256k1Message, PublicKey as LibSecp256k1PublicKey, Secp256k1, + SecretKey as LibSecp256k1PrivateKey, }; use serde::de::{Deserialize, Error as de_Error}; use serde::Serialize; @@ -441,8 +441,7 @@ pub fn secp256k1_verify( #[cfg(test)] mod tests { use rand::RngCore as _; - use secp256k1; - use secp256k1::{PublicKey as LibSecp256k1PublicKey, Secp256k1}; + use secp256k1::{self, PublicKey as LibSecp256k1PublicKey, Secp256k1}; use super::*; use crate::util::get_epoch_time_ms; diff --git a/stacks-common/src/util/secp256k1/wasm.rs b/stacks-common/src/util/secp256k1/wasm.rs index 55b5266bfd5..03414641485 100644 --- a/stacks-common/src/util/secp256k1/wasm.rs +++ b/stacks-common/src/util/secp256k1/wasm.rs @@ -14,15 +14,14 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use ::libsecp256k1; use ::libsecp256k1::curve::Scalar; pub use ::libsecp256k1::Error; -#[cfg(not(feature = "wasm-deterministic"))] -use ::libsecp256k1::{Error as LibSecp256k1Error, Message as LibSecp256k1Message}; use ::libsecp256k1::{ - PublicKey as LibSecp256k1PublicKey, RecoveryId as LibSecp256k1RecoveryId, + self, PublicKey as LibSecp256k1PublicKey, RecoveryId as LibSecp256k1RecoveryId, SecretKey as LibSecp256k1PrivateKey, Signature as LibSecp256k1Signature, ECMULT_GEN_CONTEXT, }; +#[cfg(not(feature = "wasm-deterministic"))] +use ::libsecp256k1::{Error as LibSecp256k1Error, Message as LibSecp256k1Message}; use serde::de::{Deserialize, Error as de_Error}; use serde::Serialize; diff --git a/stacks-node/src/tests/neon_integrations.rs b/stacks-node/src/tests/neon_integrations.rs index d7d1e4b2cea..0477953404e 100644 --- a/stacks-node/src/tests/neon_integrations.rs +++ b/stacks-node/src/tests/neon_integrations.rs @@ -287,8 +287,8 @@ pub mod test_observer { use stacks::net::api::postblock_proposal::BlockValidateResponse; use stacks::util::hash::hex_bytes; use stacks_common::types::chainstate::StacksBlockId; - use warp::Filter; - use {tokio, warp}; + use tokio; + use warp::{self, Filter}; use crate::event_dispatcher::{MinedBlockEvent, MinedMicroblockEvent, MinedNakamotoBlockEvent}; use crate::Config; diff --git a/stacks-node/src/tests/signer/multiversion.rs b/stacks-node/src/tests/signer/multiversion.rs index acb5a20bda6..aff6dbc0ee4 100644 --- a/stacks-node/src/tests/signer/multiversion.rs +++ b/stacks-node/src/tests/signer/multiversion.rs @@ -20,19 +20,22 @@ use libsigner::v0::messages::{ SignerMessageMetadata, }; use libsigner::v0::signer_state::{MinerState, ReplayTransactionSet, SignerStateMachine}; +use libsigner_v3_3_0_0_5; use libsigner_v3_3_0_0_5::v0::messages::SignerMessage as OldSignerMessage; +use signer_v3_3_0_0_5_0; use signer_v3_3_0_0_5_0::v0::signer_state::SUPPORTED_SIGNER_PROTOCOL_VERSION as OldSupportedVersion; use stacks::chainstate::stacks::StacksTransaction; use stacks::util::hash::{Hash160, Sha512Trunc256Sum}; use stacks::util::secp256k1::{MessageSignature, Secp256k1PrivateKey}; use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId}; +use stacks_common_v3_3_0_0_5; use stacks_common_v3_3_0_0_5::codec::StacksMessageCodec as OldStacksMessageCodec; use stacks_signer::runloop::{RewardCycleInfo, State, StateInfo}; use stacks_signer::v0::signer_state::{ LocalStateMachine, SUPPORTED_SIGNER_PROTOCOL_VERSION as NewSupportedVersion, }; use stacks_signer::v0::SpawnedSigner; -use {libsigner_v3_3_0_0_5, signer_v3_3_0_0_5_0, stacks_common_v3_3_0_0_5, stacks_v3_3_0_0_5}; +use stacks_v3_3_0_0_5; use super::SpawnedSignerTrait; use crate::stacks_common::codec::StacksMessageCodec; diff --git a/stacks-node/src/tests/stackerdb.rs b/stacks-node/src/tests/stackerdb.rs index 2fc840d4f5b..491506208fd 100644 --- a/stacks-node/src/tests/stackerdb.rs +++ b/stacks-node/src/tests/stackerdb.rs @@ -17,12 +17,13 @@ use std::{env, thread}; use clarity::vm::types::QualifiedContractIdentifier; +use reqwest; +use serde_json; use stacks::chainstate::stacks::StacksPrivateKey; use stacks::config::{EventKeyType, InitialBalance}; use stacks::libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; use stacks_common::types::chainstate::StacksAddress; use stacks_common::util::hash::Sha512Trunc256Sum; -use {reqwest, serde_json}; use crate::burnchains::bitcoin::core_controller::BitcoinCoreController; use crate::burnchains::BurnchainController; diff --git a/stackslib/src/chainstate/tests/mod.rs b/stackslib/src/chainstate/tests/mod.rs index 7c86e8fc838..792400bb0a5 100644 --- a/stackslib/src/chainstate/tests/mod.rs +++ b/stackslib/src/chainstate/tests/mod.rs @@ -35,8 +35,7 @@ use clarity::vm::costs::ExecutionCost; use clarity::vm::database::STXBalance; use clarity::vm::types::*; use clarity::vm::ContractName; -use rand; -use rand::{thread_rng, Rng}; +use rand::{self, thread_rng, Rng}; use stacks_common::address::*; use stacks_common::deps_common::bitcoin::network::serialize::BitcoinHash; use stacks_common::types::StacksEpochId; diff --git a/stackslib/src/clarity_vm/database/ephemeral.rs b/stackslib/src/clarity_vm/database/ephemeral.rs index 448afeb7ab2..3a9aff56dc3 100644 --- a/stackslib/src/clarity_vm/database/ephemeral.rs +++ b/stackslib/src/clarity_vm/database/ephemeral.rs @@ -23,8 +23,7 @@ use clarity::vm::database::sqlite::{ use clarity::vm::database::{ClarityBackingStore, SpecialCaseHandler, SqliteConnection}; use clarity::vm::errors::{RuntimeError, VmExecutionError, VmInternalError}; use clarity::vm::types::QualifiedContractIdentifier; -use rusqlite; -use rusqlite::Connection; +use rusqlite::{self, Connection}; use stacks_common::codec::StacksMessageCodec; use stacks_common::types::chainstate::{BlockHeaderHash, StacksBlockId, TrieHash}; use stacks_common::types::sqlite::NO_PARAMS; diff --git a/stackslib/src/clarity_vm/database/marf.rs b/stackslib/src/clarity_vm/database/marf.rs index 9f11720b7a9..9f8a2d3993d 100644 --- a/stackslib/src/clarity_vm/database/marf.rs +++ b/stackslib/src/clarity_vm/database/marf.rs @@ -26,8 +26,7 @@ use clarity::vm::database::sqlite::{ use clarity::vm::database::{ClarityBackingStore, SpecialCaseHandler, SqliteConnection}; use clarity::vm::errors::{IncomparableError, RuntimeError, VmExecutionError, VmInternalError}; use clarity::vm::types::QualifiedContractIdentifier; -use rusqlite; -use rusqlite::Connection; +use rusqlite::{self, Connection}; use stacks_common::codec::StacksMessageCodec; use stacks_common::types::chainstate::{BlockHeaderHash, StacksBlockId, TrieHash}; diff --git a/stackslib/src/net/api/getsortition.rs b/stackslib/src/net/api/getsortition.rs index 365b6588cd9..d79faf711dc 100644 --- a/stackslib/src/net/api/getsortition.rs +++ b/stackslib/src/net/api/getsortition.rs @@ -15,14 +15,14 @@ use clarity::types::chainstate::VRFSeed; use regex::{Captures, Regex}; -use serde::Serialize; +use serde::{self, Serialize}; +use serde_json; use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, SortitionId, StacksBlockId, }; use stacks_common::types::net::PeerHost; use stacks_common::util::hash::Hash160; use stacks_common::util::serde_serializers::{prefix_hex, prefix_opt_hex}; -use {serde, serde_json}; use crate::chainstate::burn::db::sortdb::SortitionDB; use crate::chainstate::burn::BlockSnapshot; diff --git a/stackslib/src/net/api/poststackerdbchunk.rs b/stackslib/src/net/api/poststackerdbchunk.rs index dc3981268cf..3d2ea15c300 100644 --- a/stackslib/src/net/api/poststackerdbchunk.rs +++ b/stackslib/src/net/api/poststackerdbchunk.rs @@ -18,8 +18,7 @@ use clarity::vm::representations::{CONTRACT_NAME_REGEX_STRING, STANDARD_PRINCIPA use clarity::vm::types::QualifiedContractIdentifier; use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; use regex::{Captures, Regex}; -use serde_json; -use serde_json::json; +use serde_json::{self, json}; use stacks_common::codec::MAX_MESSAGE_LEN; use stacks_common::types::net::PeerHost; use stacks_common::util::secp256k1::MessageSignature; diff --git a/stackslib/src/net/chat.rs b/stackslib/src/net/chat.rs index 84bb94dbc8e..5ad242fbfee 100644 --- a/stackslib/src/net/chat.rs +++ b/stackslib/src/net/chat.rs @@ -20,8 +20,7 @@ use std::net::SocketAddr; use std::{cmp, mem}; use clarity::vm::types::QualifiedContractIdentifier; -use rand; -use rand::{thread_rng, Rng}; +use rand::{self, thread_rng, Rng}; use stacks_common::types::net::PeerAddress; use stacks_common::types::StacksPublicKeyBuffer; use stacks_common::util::hash::to_hex; diff --git a/stackslib/src/net/codec.rs b/stackslib/src/net/codec.rs index a94ddce6898..3e2b5eeba85 100644 --- a/stackslib/src/net/codec.rs +++ b/stackslib/src/net/codec.rs @@ -21,8 +21,7 @@ use std::io::Read; use clarity::vm::types::QualifiedContractIdentifier; use clarity::vm::ContractName; -use rand; -use rand::Rng; +use rand::{self, Rng}; use sha2::{Digest, Sha512_256}; use stacks_common::bitvec::BitVec; use stacks_common::codec::{ diff --git a/stackslib/src/net/connection.rs b/stackslib/src/net/connection.rs index 60367f66608..b3382666522 100644 --- a/stackslib/src/net/connection.rs +++ b/stackslib/src/net/connection.rs @@ -1550,8 +1550,7 @@ mod test { use std::sync::{Arc, Mutex}; use std::{io, thread}; - use rand; - use rand::RngCore; + use rand::{self, RngCore}; use stacks_common::util::secp256k1::*; use stacks_common::util::*; diff --git a/stackslib/src/net/http/response.rs b/stackslib/src/net/http/response.rs index 1428352e3e8..7cf3ec8d58c 100644 --- a/stackslib/src/net/http/response.rs +++ b/stackslib/src/net/http/response.rs @@ -18,6 +18,8 @@ use std::collections::{BTreeMap, HashSet}; use std::fmt; use std::io::{Read, Write}; +use serde; +use serde_json; use stacks_common::codec::{Error as CodecError, StacksMessageCodec}; use stacks_common::deps_common::httparse; use stacks_common::util::chunked_encoding::{ @@ -25,7 +27,6 @@ use stacks_common::util::chunked_encoding::{ }; use stacks_common::util::hash::to_hex; use stacks_common::util::pipe::PipeWrite; -use {serde, serde_json}; use crate::net::http::common::{ HttpReservedHeader, HTTP_PREAMBLE_MAX_ENCODED_SIZE, HTTP_PREAMBLE_MAX_NUM_HEADERS, diff --git a/stackslib/src/net/inv/epoch2x.rs b/stackslib/src/net/inv/epoch2x.rs index 312b67a8c05..03f3c81b119 100644 --- a/stackslib/src/net/inv/epoch2x.rs +++ b/stackslib/src/net/inv/epoch2x.rs @@ -18,9 +18,8 @@ use std::cmp; use std::collections::{HashMap, HashSet}; use p2p::DropSource; -use rand; use rand::seq::SliceRandom; -use rand::thread_rng; +use rand::{self, thread_rng}; use stacks_common::types::chainstate::{BlockHeaderHash, PoxId}; use stacks_common::util::get_epoch_time_secs; diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index 0c788d76157..86d9baf4b0e 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -23,6 +23,7 @@ use clarity::vm::errors::{ClarityTypeError, VmExecutionError}; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; use libstackerdb::{Error as libstackerdb_error, StackerDBChunkData}; use p2p::{DropReason, DropSource}; +use rusqlite; use serde::{Deserialize, Serialize}; use stacks_common::bitvec::BitVec; use stacks_common::codec::{Error as codec_error, StacksMessageCodec}; @@ -34,7 +35,7 @@ use stacks_common::types::StacksPublicKeyBuffer; use stacks_common::util::get_epoch_time_secs; use stacks_common::util::hash::{Hash160, Sha256Sum}; use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey}; -use {rusqlite, url}; +use url; use self::dns::*; use crate::burnchains::{Error as burnchain_error, Txid}; @@ -2250,14 +2251,14 @@ pub mod test { use clarity::types::sqlite::NO_PARAMS; use clarity::vm::costs::ExecutionCost; use clarity::vm::types::*; - use rand::RngCore; + use mio; + use rand::{self, RngCore}; use stacks_common::codec::StacksMessageCodec; use stacks_common::deps_common::bitcoin::network::serialize::BitcoinHash; use stacks_common::types::StacksEpochId; use stacks_common::util::hash::*; use stacks_common::util::secp256k1::*; use stacks_common::util::vrf::*; - use {mio, rand}; use super::*; use crate::burnchains::bitcoin::indexer::BitcoinIndexer; diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs index a4d7d99ce34..8d9344987ea 100644 --- a/stackslib/src/net/p2p.rs +++ b/stackslib/src/net/p2p.rs @@ -21,7 +21,7 @@ use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TryRecvError, TrySendE use std::thread::JoinHandle; use clarity::vm::types::QualifiedContractIdentifier; -use mio::net as mio_net; +use mio::{self, net as mio_net}; use rand::prelude::*; use rand::thread_rng; use stacks_common::consts::{FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH}; @@ -31,7 +31,7 @@ use stacks_common::types::StacksEpochId; use stacks_common::util::hash::to_hex; use stacks_common::util::secp256k1::Secp256k1PublicKey; use stacks_common::util::{get_epoch_time_ms, get_epoch_time_secs}; -use {mio, url}; +use url; use crate::burnchains::db::{BurnchainDB, BurnchainHeaderReader}; use crate::burnchains::{Burnchain, BurnchainView}; @@ -5568,8 +5568,7 @@ mod test { use std::{thread, time}; use clarity::util::sleep_ms; - use rand; - use rand::RngCore; + use rand::{self, RngCore}; use stacks_common::types::chainstate::BurnchainHeaderHash; use super::*; diff --git a/stackslib/src/net/poll.rs b/stackslib/src/net/poll.rs index 2589fed0801..ff4abfd7b3e 100644 --- a/stackslib/src/net/poll.rs +++ b/stackslib/src/net/poll.rs @@ -20,10 +20,9 @@ use std::net::{Shutdown, SocketAddr}; use std::time::Duration; use std::{io, time}; -use mio::{net as mio_net, PollOpt, Ready, Token}; -use rand::RngCore; +use mio::{self, net as mio_net, PollOpt, Ready, Token}; +use rand::{self, RngCore}; use stacks_common::util::sleep_ms; -use {mio, rand}; use crate::net::Error as net_error; From cc76c236268e6bec1f9c7cbad4956f0ffa141c8a Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:46:21 +0100 Subject: [PATCH 02/12] new ResidentBytes trait and impls for memory footprint tracking --- clarity-types/src/lib.rs | 1 + clarity-types/src/resident_bytes.rs | 553 ++++++++++++++++++++++++++ clarity-types/src/types/mod.rs | 5 + clarity/src/vm/callables.rs | 12 + clarity/src/vm/contexts.rs | 17 + clarity/src/vm/contracts.rs | 272 +++++++++++++ clarity/src/vm/database/structures.rs | 25 ++ clarity/src/vm/types/signatures.rs | 7 + stacks-common/src/util/macros.rs | 5 + 9 files changed, 897 insertions(+) create mode 100644 clarity-types/src/resident_bytes.rs diff --git a/clarity-types/src/lib.rs b/clarity-types/src/lib.rs index 6910c9f4d0a..0a4d38f1de5 100644 --- a/clarity-types/src/lib.rs +++ b/clarity-types/src/lib.rs @@ -28,6 +28,7 @@ pub mod diagnostic; pub mod errors; pub mod execution_cost; pub mod representations; +pub mod resident_bytes; pub mod token; pub mod types; diff --git a/clarity-types/src/resident_bytes.rs b/clarity-types/src/resident_bytes.rs new file mode 100644 index 00000000000..d3f4acd751f --- /dev/null +++ b/clarity-types/src/resident_bytes.rs @@ -0,0 +1,553 @@ +// Copyright (C) 2026 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::mem::size_of; +use std::sync::Arc; + +use crate::representations::{ + ClarityName, ContractName, SymbolicExpression, SymbolicExpressionType, TraitDefinition, +}; +use crate::types::signatures::{ + BufferLength, CallableSubtype, ListTypeData, SequenceSubtype, StringSubtype, StringUTF8Length, + TupleTypeSignature, TypeSignature, +}; +use crate::types::{ + ASCIIData, BuffData, CallableData, CharType, FunctionIdentifier, ListData, OptionalData, + PrincipalData, QualifiedContractIdentifier, ResponseData, SequenceData, StandardPrincipalData, + TraitIdentifier, TupleData, UTF8Data, Value, +}; + +// Rust's BTreeMap uses B=6 (hardcoded): each node holds up to CAPACITY = 2*B-1 = 11 entries: +// * LeafNode layout: parent ptr (8) + parent_idx (2) + len (2) + padding (~4) + keys: +// [MaybeUninit; 11] + vals: [MaybeUninit; 11]. +// * Allocator header adds ~16 bytes per node. +// * Total per-node overhead (metadata + allocator): ~32 bytes. Average fill factor ~2/3 → ~7 +// entries per node. + +/// Maximum entries per BTree node (B=6 in Rust's implementation → 2*B-1 = 11). +const BTREE_NODE_CAPACITY: usize = 11; +/// Estimated average entries per node in a steady-state B-tree (~2/3 fill). +const BTREE_AVERAGE_FILL: usize = 7; +/// Per-node overhead: metadata (parent ptr + idx + len + padding) + allocator header. +const BTREE_NODE_OVERHEAD: usize = 32; +/// Estimated overhead for Arc: strong + weak counts + allocation header. +const ARC_OVERHEAD: usize = 16; + +/// Reports the approximate in-memory footprint of an instance, in bytes. +/// +/// See module-level documentation for the two-method design. +pub trait ResidentBytes { + /// Total in-memory footprint: inline `size_of` + heap allocations. + /// + /// This is the method callers should use. It has a provided default + /// implementation; implementors only need to implement [`heap_bytes`]. + fn resident_bytes(&self) -> usize { + std::mem::size_of_val(self) + self.heap_bytes() + } + + /// Heap allocations only, beyond the inline `size_of`. + /// + /// Container types call this on their children to avoid double-counting + /// inline sizes that are already part of the container's backing allocation. + fn heap_bytes(&self) -> usize; +} + +impl ResidentBytes for String { + fn heap_bytes(&self) -> usize { + self.capacity() + } +} + +impl ResidentBytes for Vec { + fn heap_bytes(&self) -> usize { + // Backing array: capacity slots (inline size per slot) + let backing = self.capacity() * size_of::(); + // Children's heap allocations + let children: usize = self.iter().map(|v| v.heap_bytes()).sum(); + backing + children + } +} + +impl ResidentBytes for Box { + fn heap_bytes(&self) -> usize { + // Box heap-allocates the pointee: its inline size + its own heap + size_of::() + (**self).heap_bytes() + } +} + +impl ResidentBytes for Option { + fn heap_bytes(&self) -> usize { + match self { + // For Some, the T is inline in the Option — only count T's heap + Some(v) => v.heap_bytes(), + None => 0, + } + } +} + +impl ResidentBytes for Arc { + fn heap_bytes(&self) -> usize { + // Arc heap-allocates: header (strong + weak counts, ~16 bytes) + T inline + T's heap + 16 + size_of::() + (**self).heap_bytes() + } +} + +impl ResidentBytes for HashMap { + fn heap_bytes(&self) -> usize { + // Backing array: capacity * (entry inline size + ~1 byte control metadata) + let backing = self.capacity() * (size_of::<(K, V)>() + 1); + // Children's heap allocations (only for occupied entries) + let children: usize = self + .iter() + .map(|(k, v)| k.heap_bytes() + v.heap_bytes()) + .sum(); + backing + children + } +} + +impl ResidentBytes for BTreeMap { + fn heap_bytes(&self) -> usize { + if self.is_empty() { + return 0; // Empty BTreeMaps do not allocate on the heap. + } + + // Node count from average fill, not max capacity + let nodes = self.len().div_ceil(BTREE_AVERAGE_FILL); + // Keys and values are separate arrays in the node, not (K, V) tuples + let node_size = BTREE_NODE_OVERHEAD + + (BTREE_NODE_CAPACITY * size_of::()) + + (BTREE_NODE_CAPACITY * size_of::()); + let structural = nodes * node_size; + + let children: usize = self + .iter() + .map(|(k, v)| k.heap_bytes() + v.heap_bytes()) + .sum(); + structural + children + } +} + +impl ResidentBytes for BTreeSet { + fn heap_bytes(&self) -> usize { + if self.is_empty() { + return 0; + } + + // BTreeSet is backed by BTreeMap — vals array is zero-size + let nodes = self.len().div_ceil(BTREE_AVERAGE_FILL); + let node_size = BTREE_NODE_OVERHEAD + (BTREE_NODE_CAPACITY * size_of::()); + let structural = nodes * node_size; + let children: usize = self.iter().map(|v| v.heap_bytes()).sum(); + structural + children + } +} + +impl ResidentBytes for HashSet { + fn heap_bytes(&self) -> usize { + let backing = self.capacity() * (size_of::() + 1); + let children: usize = self.iter().map(|v| v.heap_bytes()).sum(); + backing + children + } +} + +impl ResidentBytes for (A, B) { + fn heap_bytes(&self) -> usize { + self.0.heap_bytes() + self.1.heap_bytes() + } +} + +// Primitive types: no heap allocation +impl ResidentBytes for bool { + fn heap_bytes(&self) -> usize { + 0 + } +} +impl ResidentBytes for u8 { + fn heap_bytes(&self) -> usize { + 0 + } +} +impl ResidentBytes for u32 { + fn heap_bytes(&self) -> usize { + 0 + } +} +impl ResidentBytes for u64 { + fn heap_bytes(&self) -> usize { + 0 + } +} +impl ResidentBytes for u128 { + fn heap_bytes(&self) -> usize { + 0 + } +} +impl ResidentBytes for i128 { + fn heap_bytes(&self) -> usize { + 0 + } +} + +impl ResidentBytes for Value { + fn heap_bytes(&self) -> usize { + match self { + Value::Int(_) | Value::UInt(_) | Value::Bool(_) => 0, + Value::Sequence(data) => data.heap_bytes(), + Value::Principal(data) => data.heap_bytes(), + Value::Tuple(data) => data.heap_bytes(), + Value::Optional(data) => data.heap_bytes(), + Value::Response(data) => data.heap_bytes(), + Value::CallableContract(data) => data.heap_bytes(), + } + } +} + +impl ResidentBytes for SequenceData { + fn heap_bytes(&self) -> usize { + match self { + SequenceData::Buffer(buf) => buf.heap_bytes(), + SequenceData::List(list) => list.heap_bytes(), + SequenceData::String(char_type) => char_type.heap_bytes(), + } + } +} + +impl ResidentBytes for BuffData { + fn heap_bytes(&self) -> usize { + self.data.heap_bytes() + } +} + +impl ResidentBytes for ListData { + fn heap_bytes(&self) -> usize { + self.data.heap_bytes() + self.type_signature.heap_bytes() + } +} + +impl ResidentBytes for CharType { + fn heap_bytes(&self) -> usize { + match self { + CharType::ASCII(data) => data.heap_bytes(), + CharType::UTF8(data) => data.heap_bytes(), + } + } +} + +impl ResidentBytes for ASCIIData { + fn heap_bytes(&self) -> usize { + self.data.heap_bytes() + } +} + +impl ResidentBytes for UTF8Data { + fn heap_bytes(&self) -> usize { + // Vec>: outer vec backing + each inner vec's backing + let outer = self.data.capacity() * size_of::>(); + let inner: usize = self.data.iter().map(|v| v.capacity()).sum(); + outer + inner + } +} + +impl ResidentBytes for TupleData { + fn heap_bytes(&self) -> usize { + self.type_signature.heap_bytes() + self.data_map.heap_bytes() + } +} + +impl ResidentBytes for OptionalData { + fn heap_bytes(&self) -> usize { + self.data.heap_bytes() + } +} + +impl ResidentBytes for ResponseData { + fn heap_bytes(&self) -> usize { + self.data.heap_bytes() + } +} + +impl ResidentBytes for CallableData { + fn heap_bytes(&self) -> usize { + self.contract_identifier.heap_bytes() + self.trait_identifier.heap_bytes() + } +} + +impl ResidentBytes for PrincipalData { + fn heap_bytes(&self) -> usize { + match self { + PrincipalData::Standard(data) => data.heap_bytes(), + PrincipalData::Contract(data) => data.heap_bytes(), + } + } +} + +impl ResidentBytes for StandardPrincipalData { + fn heap_bytes(&self) -> usize { + 0 // Fixed-size: u8 + [u8; 20], no heap allocation + } +} + +impl ResidentBytes for QualifiedContractIdentifier { + fn heap_bytes(&self) -> usize { + self.issuer.heap_bytes() + self.name.heap_bytes() + } +} + +impl ResidentBytes for ClarityName { + fn heap_bytes(&self) -> usize { + self.heap_capacity() + } +} + +impl ResidentBytes for ContractName { + fn heap_bytes(&self) -> usize { + self.heap_capacity() + } +} + +impl ResidentBytes for TraitIdentifier { + fn heap_bytes(&self) -> usize { + self.name.heap_bytes() + self.contract_identifier.heap_bytes() + } +} + +impl ResidentBytes for FunctionIdentifier { + fn heap_bytes(&self) -> usize { + self.heap_capacity() + } +} + +impl ResidentBytes for TypeSignature { + fn heap_bytes(&self) -> usize { + match self { + TypeSignature::NoType + | TypeSignature::IntType + | TypeSignature::UIntType + | TypeSignature::BoolType + | TypeSignature::PrincipalType => 0, + TypeSignature::SequenceType(subtype) => subtype.heap_bytes(), + TypeSignature::TupleType(tuple_sig) => tuple_sig.heap_bytes(), + TypeSignature::OptionalType(inner) => inner.heap_bytes(), + TypeSignature::ResponseType(inner) => inner.heap_bytes(), + TypeSignature::CallableType(subtype) => subtype.heap_bytes(), + TypeSignature::ListUnionType(set) => set.heap_bytes(), + TypeSignature::TraitReferenceType(trait_id) => trait_id.heap_bytes(), + } + } +} + +impl ResidentBytes for TupleTypeSignature { + fn heap_bytes(&self) -> usize { + // TupleTypeSignature wraps Arc>. get_type_map() + // returns &BTreeMap — count Arc overhead + map header + contents. + let map_header = size_of::>(); + ARC_OVERHEAD + map_header + self.get_type_map().heap_bytes() + } +} + +impl ResidentBytes for SequenceSubtype { + fn heap_bytes(&self) -> usize { + match self { + SequenceSubtype::BufferType(len) => len.heap_bytes(), + SequenceSubtype::ListType(list) => list.heap_bytes(), + SequenceSubtype::StringType(string) => string.heap_bytes(), + } + } +} + +impl ResidentBytes for ListTypeData { + fn heap_bytes(&self) -> usize { + // max_len: u32 (no heap), entry_type: Box + size_of::() + self.get_list_item_type().heap_bytes() + } +} + +impl ResidentBytes for StringSubtype { + fn heap_bytes(&self) -> usize { + 0 // Both variants (ASCII, UTF8) contain only u32 newtypes + } +} + +impl ResidentBytes for BufferLength { + fn heap_bytes(&self) -> usize { + 0 // u32 newtype + } +} + +impl ResidentBytes for StringUTF8Length { + fn heap_bytes(&self) -> usize { + 0 // u32 newtype + } +} + +impl ResidentBytes for CallableSubtype { + fn heap_bytes(&self) -> usize { + match self { + CallableSubtype::Principal(id) => id.heap_bytes(), + CallableSubtype::Trait(trait_id) => trait_id.heap_bytes(), + } + } +} + +impl ResidentBytes for SymbolicExpression { + fn heap_bytes(&self) -> usize { + // Only production fields: expr + id. + // developer-mode fields (span, pre_comments, end_line_comment, post_comments) + // are excluded for deterministic sizing between dev and prod builds. + self.expr.heap_bytes() + // id is u64 — no heap allocation + } +} + +impl ResidentBytes for SymbolicExpressionType { + fn heap_bytes(&self) -> usize { + match self { + SymbolicExpressionType::AtomValue(value) + | SymbolicExpressionType::LiteralValue(value) => value.heap_bytes(), + SymbolicExpressionType::Atom(name) => name.heap_bytes(), + SymbolicExpressionType::List(exprs) => exprs.heap_bytes(), + SymbolicExpressionType::Field(trait_id) => trait_id.heap_bytes(), + SymbolicExpressionType::TraitReference(name, defn) => { + name.heap_bytes() + defn.heap_bytes() + } + } + } +} + +impl ResidentBytes for TraitDefinition { + fn heap_bytes(&self) -> usize { + match self { + TraitDefinition::Defined(id) | TraitDefinition::Imported(id) => id.heap_bytes(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_primitive_values_include_inline_size() { + // resident_bytes() includes size_of::() even for scalar variants + let int_size = Value::Int(42).resident_bytes(); + assert!( + int_size >= size_of::(), + "Int resident_bytes ({int_size}) should be >= size_of::()" + ); + assert_eq!(Value::Int(42).heap_bytes(), 0); + } + + #[test] + fn test_u64_resident_bytes() { + let v: u64 = 42; + assert_eq!(v.resident_bytes(), 8); + assert_eq!(v.heap_bytes(), 0); + } + + #[test] + fn test_string_resident_bytes() { + let s = String::from("hello world"); + assert!(s.resident_bytes() >= size_of::() + 11); + assert!(s.heap_bytes() >= 11); + } + + #[test] + fn test_vec_resident_bytes() { + let v: Vec = vec![1, 2, 3, 4, 5]; + // heap: capacity * size_of::() = 5 * 8 = 40 bytes (no child heap) + assert!(v.heap_bytes() >= 40); + // total: size_of::>() + heap + assert!(v.resident_bytes() >= size_of::>() + 40); + } + + #[test] + fn test_hashmap_resident_bytes() { + let mut m: HashMap = HashMap::new(); + m.insert("key1".into(), 1); + m.insert("key2".into(), 2); + // Should include: HashMap header + backing array + key string heaps + assert!(m.resident_bytes() > size_of::>()); + } + + #[test] + fn test_optional_none() { + let opt: Option> = None; + assert_eq!(opt.heap_bytes(), 0); + // resident_bytes includes size_of::>>() + assert!(opt.resident_bytes() >= size_of::>>()); + } + + #[test] + fn test_optional_some_counts_content() { + let opt = OptionalData { + data: Some(Box::new(Value::Int(42))), + }; + // heap: Box (size_of::() + 0 heap) + assert!(opt.heap_bytes() > 0); + } + + #[test] + fn test_clarity_name_includes_inline_and_heap() { + let name = ClarityName::try_from("my-variable".to_string()).unwrap(); + // heap: String buffer capacity + assert!(name.heap_bytes() >= 11); + // total: size_of::() + heap + assert!(name.resident_bytes() > name.heap_bytes()); + } + + #[test] + fn test_sequence_buffer_counts_vec() { + let buf = BuffData { + data: vec![0u8; 100], + }; + assert!(buf.heap_bytes() >= 100); + } + + #[test] + fn test_list_data_recursive() { + let list = ListData { + data: vec![Value::Int(1), Value::Int(2), Value::Int(3)], + type_signature: ListTypeData::new_list(TypeSignature::IntType, 10).unwrap(), + }; + // heap: Vec backing (3 * size_of::()) + ListTypeData (Box) + assert!(list.heap_bytes() > 0); + } + + #[test] + fn test_symbolic_expression_list_recursive() { + let inner = SymbolicExpression::atom(ClarityName::try_from("x".to_string()).unwrap()); + let list = SymbolicExpression::list(vec![inner.clone(), inner.clone(), inner]); + // Should recursively count the Vec backing + each child's ClarityName heap + assert!(list.heap_bytes() > 0); + assert!(list.resident_bytes() > list.heap_bytes()); + } + + #[test] + fn test_type_signature_scalar_heap_is_zero() { + assert_eq!(TypeSignature::IntType.heap_bytes(), 0); + assert_eq!(TypeSignature::BoolType.heap_bytes(), 0); + // But resident_bytes includes size_of::() + assert!(TypeSignature::IntType.resident_bytes() > 0); + } + + #[test] + fn test_type_signature_optional_recursive() { + let sig = TypeSignature::OptionalType(Box::new(TypeSignature::IntType)); + // heap: Box (size_of::() + 0) + assert!(sig.heap_bytes() > 0); + assert!(sig.resident_bytes() > sig.heap_bytes()); + } +} diff --git a/clarity-types/src/types/mod.rs b/clarity-types/src/types/mod.rs index 9dc7af4ff3b..70e7c1b2529 100644 --- a/clarity-types/src/types/mod.rs +++ b/clarity-types/src/types/mod.rs @@ -1782,6 +1782,11 @@ impl fmt::Display for FunctionIdentifier { } impl FunctionIdentifier { + /// Returns the heap capacity of the backing `String` buffer. + pub fn heap_capacity(&self) -> usize { + self.identifier.capacity() + } + pub fn new_native_function(name: &str) -> FunctionIdentifier { let identifier = format!("_native_:{name}"); FunctionIdentifier { identifier } diff --git a/clarity/src/vm/callables.rs b/clarity/src/vm/callables.rs index 76d9021018c..e962fed3311 100644 --- a/clarity/src/vm/callables.rs +++ b/clarity/src/vm/callables.rs @@ -17,6 +17,7 @@ use std::collections::BTreeMap; use clarity_types::representations::ClarityName; +use clarity_types::resident_bytes::ResidentBytes; pub use clarity_types::types::FunctionIdentifier; use stacks_common::types::StacksEpochId; @@ -76,6 +77,17 @@ pub struct DefinedFunction { body: SymbolicExpression, } +impl ResidentBytes for DefinedFunction { + fn heap_bytes(&self) -> usize { + self.identifier.heap_bytes() + + self.name.heap_bytes() + + self.arg_types.heap_bytes() + + self.arguments.heap_bytes() + + self.body.heap_bytes() + // define_type is a fieldless enum — no heap allocation + } +} + /// This enum handles the actual invocation of the method /// implementing a native function. Each variant handles /// different expected number of arguments. diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index 26599b0aad2..62703435517 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -21,6 +21,7 @@ use std::time::{Duration, Instant}; use clarity_types::errors::{ParseError, ParseErrorKind}; use clarity_types::representations::ClarityName; +use clarity_types::resident_bytes::ResidentBytes; use serde::Serialize; use serde_json::json; use stacks_common::types::StacksEpochId; @@ -324,6 +325,22 @@ pub struct ContractContext { clarity_version: ClarityVersion, } +impl ResidentBytes for ContractContext { + fn heap_bytes(&self) -> usize { + self.contract_identifier.heap_bytes() + + self.variables.heap_bytes() + + self.functions.heap_bytes() + + self.defined_traits.heap_bytes() + + self.implemented_traits.heap_bytes() + + self.persisted_names.heap_bytes() + + self.meta_data_map.heap_bytes() + + self.meta_data_var.heap_bytes() + + self.meta_nft.heap_bytes() + + self.meta_ft.heap_bytes() + // data_size: u64, clarity_version: enum — inline, covered by size_of::() + } +} + pub struct LocalContext<'a> { pub function_context: Option<&'a LocalContext<'a>>, pub parent: Option<&'a LocalContext<'a>>, diff --git a/clarity/src/vm/contracts.rs b/clarity/src/vm/contracts.rs index c3654883055..847ec4ec3dc 100644 --- a/clarity/src/vm/contracts.rs +++ b/clarity/src/vm/contracts.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use clarity_types::resident_bytes::ResidentBytes; use stacks_common::types::StacksEpochId; use crate::vm::ast::ContractAST; @@ -28,6 +29,12 @@ pub struct Contract { pub contract_context: ContractContext, } +impl ResidentBytes for Contract { + fn heap_bytes(&self) -> usize { + self.contract_context.heap_bytes() + } +} + // AARON: this is an increasingly useless wrapper around a ContractContext struct. // will probably be removed soon. impl Contract { @@ -54,3 +61,268 @@ impl Contract { self.contract_context.canonicalize_types(epoch) } } + +#[cfg(test)] +mod tests { + use std::mem::size_of; + + use clarity_types::resident_bytes::ResidentBytes; + use stacks_common::consts::CHAIN_ID_TESTNET; + use stacks_common::types::StacksEpochId; + + use crate::vm::GlobalContext; + use crate::vm::ast::build_ast; + use crate::vm::contexts::ContractContext; + use crate::vm::contracts::Contract; + use crate::vm::costs::LimitedCostTracker; + use crate::vm::database::MemoryBackingStore; + use crate::vm::types::QualifiedContractIdentifier; + use crate::vm::version::ClarityVersion; + + fn expected_contract_context_heap_bytes(contract: &Contract) -> usize { + let contract_context = &contract.contract_context; + + contract_context.contract_identifier.heap_bytes() + + contract_context.variables.heap_bytes() + + contract_context.functions.heap_bytes() + + contract_context.defined_traits.heap_bytes() + + contract_context.implemented_traits.heap_bytes() + + contract_context.persisted_names.heap_bytes() + + contract_context.meta_data_map.heap_bytes() + + contract_context.meta_data_var.heap_bytes() + + contract_context.meta_nft.heap_bytes() + + contract_context.meta_ft.heap_bytes() + } + + #[track_caller] + fn assert_contract_bytes_match_context_fields(contract: &Contract) { + let expected_heap = expected_contract_context_heap_bytes(contract); + + assert_eq!(contract.contract_context.heap_bytes(), expected_heap); + assert_eq!(contract.heap_bytes(), expected_heap); + assert_eq!( + contract.resident_bytes(), + size_of::() + expected_heap + ); + } + + fn initialize_contract_with_store( + marf: &mut MemoryBackingStore, + source: &str, + contract_name: &str, + ) -> Contract { + let version = ClarityVersion::Clarity2; + let epoch = StacksEpochId::Epoch21; + let contract_identifier = QualifiedContractIdentifier::local(contract_name).unwrap(); + let conn = marf.as_clarity_db(); + let mut global_context = GlobalContext::new( + false, + CHAIN_ID_TESTNET, + conn, + LimitedCostTracker::new_free(), + epoch, + ); + let contract_ast = build_ast(&contract_identifier, source, &mut (), version, epoch) + .expect("contract source should parse"); + + global_context + .execute(|g| { + Contract::initialize_from_ast(contract_identifier, &contract_ast, None, g, version) + }) + .expect("contract source should initialize") + } + + fn initialize_contract(source: &str, contract_name: &str) -> Contract { + let mut marf = MemoryBackingStore::new(); + initialize_contract_with_store(&mut marf, source, contract_name) + } + + #[test] + fn contract_resident_bytes_matches_empty_contract_context_exactly() { + let contract_identifier = + QualifiedContractIdentifier::local("resident-bytes-empty").unwrap(); + let contract = Contract { + contract_context: ContractContext::new(contract_identifier, ClarityVersion::Clarity2), + }; + + assert!(contract.contract_context.variables.is_empty()); + assert!(contract.contract_context.functions.is_empty()); + assert!(contract.contract_context.defined_traits.is_empty()); + assert!(contract.contract_context.implemented_traits.is_empty()); + assert!(contract.contract_context.persisted_names.is_empty()); + assert!(contract.contract_context.meta_data_map.is_empty()); + assert!(contract.contract_context.meta_data_var.is_empty()); + assert!(contract.contract_context.meta_nft.is_empty()); + assert!(contract.contract_context.meta_ft.is_empty()); + + assert_contract_bytes_match_context_fields(&contract); + assert_eq!( + contract.heap_bytes(), + contract.contract_context.contract_identifier.heap_bytes() + ); + } + + #[test] + fn contract_resident_bytes_matches_initialized_contract_context_exactly() { + let contract = initialize_contract( + r#" + (define-data-var counter uint u0) + (define-map balances { owner: principal } { amount: uint, memo: (string-ascii 32) }) + (define-constant label "resident-bytes") + + (define-private (helper (amount uint)) + (begin + (var-set counter (+ (var-get counter) amount)) + (ok (var-get counter)))) + + (define-read-only (lookup (owner principal)) + (default-to { amount: u0, memo: "none" } + (map-get? balances { owner: owner }))) + + (define-public (store (owner principal) (amount uint)) + (begin + (map-set balances { owner: owner } { amount: amount, memo: "cache-entry" }) + (try! (helper amount)) + (ok true))) + "#, + "resident-bytes-rich", + ); + + assert_eq!(contract.contract_context.variables.len(), 1); + assert_eq!(contract.contract_context.functions.len(), 3); + assert_eq!(contract.contract_context.meta_data_map.len(), 1); + assert_eq!(contract.contract_context.meta_data_var.len(), 1); + assert!(contract.contract_context.persisted_names.len() >= 2); + + assert_contract_bytes_match_context_fields(&contract); + + // Magnitude check: a contract with 3 functions, a map, a var, and a constant + // must have substantial heap allocation beyond the bare struct size. + assert!( + contract.resident_bytes() > size_of::() + 1000, + "rich contract resident_bytes ({}) should exceed struct size + 1000", + contract.resident_bytes() + ); + } + + #[test] + fn contract_resident_bytes_exercises_ft_nft_and_traits() { + let contract = initialize_contract( + r#" + (define-fungible-token gold) + (define-fungible-token silver u1000000) + (define-non-fungible-token deed uint) + (define-non-fungible-token badge { class: uint, level: uint }) + (define-trait transferable ( + (transfer (uint principal principal) (response bool uint)) + (get-balance (principal) (response uint uint)))) + "#, + "resident-bytes-ft-nft-trait", + ); + + assert_eq!(contract.contract_context.meta_ft.len(), 2); + assert_eq!(contract.contract_context.meta_nft.len(), 2); + assert_eq!(contract.contract_context.defined_traits.len(), 1); + + assert_contract_bytes_match_context_fields(&contract); + + // meta_nft contains a tuple key type (badge) — verify it contributes heap bytes + let nft_heap: usize = contract + .contract_context + .meta_nft + .values() + .map(|m| m.heap_bytes()) + .sum(); + assert!( + nft_heap > 0, + "NFT metadata with tuple key type should have non-zero heap bytes" + ); + + // defined_traits contains function signatures — verify they contribute heap bytes + let trait_heap: usize = contract + .contract_context + .defined_traits + .values() + .map(|m| m.heap_bytes()) + .sum(); + assert!( + trait_heap > 0, + "defined traits with function signatures should have non-zero heap bytes" + ); + } + + #[test] + fn contract_resident_bytes_exercises_implemented_traits() { + let mut marf = MemoryBackingStore::new(); + + // First contract defines the trait + let _trait_contract = initialize_contract_with_store( + &mut marf, + r#" + (define-trait transferable ( + (transfer (uint principal principal) (response bool uint)))) + "#, + "trait-definer", + ); + + // Second contract implements the trait (requires the first to be deployed) + let impl_contract = initialize_contract_with_store( + &mut marf, + r#" + (impl-trait .trait-definer.transferable) + (define-public (transfer (id uint) (from principal) (to principal)) + (ok true)) + "#, + "trait-impl", + ); + + assert_eq!(impl_contract.contract_context.implemented_traits.len(), 1); + + assert_contract_bytes_match_context_fields(&impl_contract); + + // implemented_traits contains a TraitIdentifier — verify non-zero heap + let impl_heap = impl_contract + .contract_context + .implemented_traits + .heap_bytes(); + assert!( + impl_heap > 0, + "implemented_traits with a TraitIdentifier should have non-zero heap bytes" + ); + } + + #[test] + fn contract_resident_bytes_grows_with_additional_initialized_content() { + let single_function = initialize_contract( + r#" + (define-public (echo (value uint)) + (ok value)) + "#, + "resident-bytes-single-fn", + ); + let many_functions = initialize_contract( + r#" + (define-private (double (value uint)) (+ value value)) + (define-private (triple (value uint)) (+ value (+ value value))) + (define-read-only (project (value uint)) + { original: value, doubled: (double value), tripled: (triple value) }) + (define-public (accumulate (a uint) (b uint) (c uint)) + (let ( + (first (double a)) + (second (triple b)) + (third (+ c u7))) + (ok (+ first (+ second third))))) + "#, + "resident-bytes-many-fns", + ); + + assert_contract_bytes_match_context_fields(&single_function); + assert_contract_bytes_match_context_fields(&many_functions); + assert!( + many_functions.contract_context.functions.len() + > single_function.contract_context.functions.len() + ); + assert!(many_functions.heap_bytes() > single_function.heap_bytes()); + assert!(many_functions.resident_bytes() > single_function.resident_bytes()); + } +} diff --git a/clarity/src/vm/database/structures.rs b/clarity/src/vm/database/structures.rs index 9741f3fb4ff..622556f6cfa 100644 --- a/clarity/src/vm/database/structures.rs +++ b/clarity/src/vm/database/structures.rs @@ -16,6 +16,7 @@ use std::io::Write; +use clarity_types::resident_bytes::ResidentBytes; use serde::Deserialize; use stacks_common::util::hash::{hex_bytes, to_hex}; @@ -105,6 +106,30 @@ pub struct DataVariableMetadata { clarity_serializable!(DataVariableMetadata); +impl ResidentBytes for FungibleTokenMetadata { + fn heap_bytes(&self) -> usize { + 0 // Option — no heap allocation + } +} + +impl ResidentBytes for NonFungibleTokenMetadata { + fn heap_bytes(&self) -> usize { + self.key_type.heap_bytes() + } +} + +impl ResidentBytes for DataMapMetadata { + fn heap_bytes(&self) -> usize { + self.key_type.heap_bytes() + self.value_type.heap_bytes() + } +} + +impl ResidentBytes for DataVariableMetadata { + fn heap_bytes(&self) -> usize { + self.value_type.heap_bytes() + } +} + #[derive(Serialize, Deserialize)] pub struct ContractMetadata { pub contract: Contract, diff --git a/clarity/src/vm/types/signatures.rs b/clarity/src/vm/types/signatures.rs index a47c14ecf35..fa439e51e0f 100644 --- a/clarity/src/vm/types/signatures.rs +++ b/clarity/src/vm/types/signatures.rs @@ -19,6 +19,7 @@ use std::fmt; use clarity_types::ClarityTypeError; use clarity_types::errors::analysis::{CommonCheckErrorKind, StaticCheckErrorKind}; +use clarity_types::resident_bytes::ResidentBytes; pub use clarity_types::types::Value; pub use clarity_types::types::signatures::{ AssetIdentifier, BufferLength, CallableSubtype, ListTypeData, SequenceSubtype, StringSubtype, @@ -40,6 +41,12 @@ pub struct FunctionSignature { pub returns: TypeSignature, } +impl ResidentBytes for FunctionSignature { + fn heap_bytes(&self) -> usize { + self.args.heap_bytes() + self.returns.heap_bytes() + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FixedFunction { pub args: Vec, diff --git a/stacks-common/src/util/macros.rs b/stacks-common/src/util/macros.rs index f3cc789bc4f..4fae3c918b1 100644 --- a/stacks-common/src/util/macros.rs +++ b/stacks-common/src/util/macros.rs @@ -233,6 +233,11 @@ macro_rules! guarded_string { pub fn is_empty(&self) -> bool { self.len() == 0 } + + /// Returns the heap capacity of the backing `String` buffer. + pub fn heap_capacity(&self) -> usize { + self.0.capacity() + } } impl Deref for $Name { From 8a2979559e4acbba1549400003f314dc1badedc1 Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:35:22 +0100 Subject: [PATCH 03/12] improve memory approximation calculations for btree/hash map/set and others --- clarity-types/src/resident_bytes.rs | 173 ++++++++++++++++++++++------ 1 file changed, 135 insertions(+), 38 deletions(-) diff --git a/clarity-types/src/resident_bytes.rs b/clarity-types/src/resident_bytes.rs index d3f4acd751f..84647776498 100644 --- a/clarity-types/src/resident_bytes.rs +++ b/clarity-types/src/resident_bytes.rs @@ -17,6 +17,8 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::mem::size_of; use std::sync::Arc; +#[cfg(feature = "developer-mode")] +use crate::representations::Span; use crate::representations::{ ClarityName, ContractName, SymbolicExpression, SymbolicExpressionType, TraitDefinition, }; @@ -30,38 +32,92 @@ use crate::types::{ TraitIdentifier, TupleData, UTF8Data, Value, }; -// Rust's BTreeMap uses B=6 (hardcoded): each node holds up to CAPACITY = 2*B-1 = 11 entries: -// * LeafNode layout: parent ptr (8) + parent_idx (2) + len (2) + padding (~4) + keys: -// [MaybeUninit; 11] + vals: [MaybeUninit; 11]. -// * Allocator header adds ~16 bytes per node. -// * Total per-node overhead (metadata + allocator): ~32 bytes. Average fill factor ~2/3 → ~7 -// entries per node. - -/// Maximum entries per BTree node (B=6 in Rust's implementation → 2*B-1 = 11). -const BTREE_NODE_CAPACITY: usize = 11; -/// Estimated average entries per node in a steady-state B-tree (~2/3 fill). -const BTREE_AVERAGE_FILL: usize = 7; -/// Per-node overhead: metadata (parent ptr + idx + len + padding) + allocator header. -const BTREE_NODE_OVERHEAD: usize = 32; -/// Estimated overhead for Arc: strong + weak counts + allocation header. +/// Estimated overhead for `Arc`: `strong + weak counts + allocation header`. const ARC_OVERHEAD: usize = 16; +// The `btree` and `hashmap` modules below contain heuristic constants derived from std's internal +// implementations (as of Rust 1.94 / hashbrown 0.15). They provide reasonable estimates of +// structural overhead, not exact byte counts. + +/// Layout constants for `std::collections::BTreeMap` / `BTreeSet`. +/// +/// Rust's BTreeMap uses `B=6` (hardcoded). Each node holds up to `CAPACITY = 2*B-1 = 11` entries: +/// * **LeafNode** layout: parent ptr (8) + parent_idx (2) + len (2) + padding (~4) + keys: +/// `[MaybeUninit; 11]` + vals: `[MaybeUninit; 11]`. +/// * **InternalNode** layout: LeafNode fields + edges: `[MaybeUninit>; 12]`. +/// * Allocator header adds ~16 bytes per node. +/// * Total per-node overhead (metadata + allocator): ~32 bytes. Average fill factor ~2/3 → ~7 +/// entries per node. Internal nodes at average fill have ~8 children. +mod btree { + use std::mem::size_of; + + /// Maximum entries per node (`B=6` → `2*B-1 = 11`). + pub const NODE_CAPACITY: usize = 11; + /// Estimated average entries per node in a steady-state B-tree (~2/3 fill). + pub const AVERAGE_FILL: usize = 7; + /// Average children per internal node at ~2/3 fill. + pub const AVG_FANOUT: usize = AVERAGE_FILL + 1; + /// Per-node overhead: `(parent ptr + idx + len + padding) + allocator header`. + pub const NODE_OVERHEAD: usize = 32; + /// Additional per-node size for internal nodes: `[MaybeUninit>; CAPACITY + 1]`. + pub const EDGE_ARRAY_SIZE: usize = (NODE_CAPACITY + 1) * size_of::(); + + /// Estimate total BTree node count (leaves + internal) and how many are internal. + pub fn node_counts(len: usize) -> (usize, usize) { + let leaves = len.div_ceil(AVERAGE_FILL); + let mut internal = 0; + let mut children_at_level = leaves; + while children_at_level > 1 { + let parents = children_at_level.div_ceil(AVG_FANOUT); + internal += parents; + children_at_level = parents; + } + (leaves + internal, internal) + } +} + +/// Layout constants for `std::collections::HashMap` / `HashSet`. +/// +/// std's HashMap has been backed by hashbrown since Rust 1.36. These constants reflect hashbrown +/// internals that are not exposed through any std API. +/// +/// * `hashbrown` targets a 7/8 max load factor: it allocates more buckets than `capacity()` +/// reports. `capacity()` returns the number of insertions before reallocation, not the bucket +/// count. Actual buckets ~= `ceil(capacity * LOAD_FACTOR_INV_NUM / LOAD_FACTOR_INV_DEN)`. +/// * Each bucket has a 1-byte control tag. The control array is padded by `Group::WIDTH` bytes (16 +/// on platforms with 128-bit SIMD, 8 otherwise) for SIMD probing at the end of the table. +/// * `hashbrown` also aligns `buckets * entry_size` up to `ctrl_align` (max of entry alignment and +/// Group alignment) before placing control bytes. We don't model this padding — for the types +/// used in Clarity, bucket counts are powers of 2 and entry alignments are <=8, so the gap is +/// typically zero. +mod hashmap { + /// Inverse of hashbrown's max load factor (7/8), as a fraction: `buckets ~= (capacity * 8/7)`. + pub const LOAD_FACTOR_INV_NUM: usize = 8; + pub const LOAD_FACTOR_INV_DEN: usize = 7; + /// Conservative upper bound for SIMD group width padding appended to the control byte array. + /// hashbrown's actual `Group::WIDTH` varies by target (4, 8, or 16 bytes); 16 is the max + /// (SSE2 path on x86_64) and overestimates by at most 12 bytes on other platforms. + pub const CONTROL_GROUP_PADDING: usize = 16; + + // NOTE: +} + /// Reports the approximate in-memory footprint of an instance, in bytes. /// /// See module-level documentation for the two-method design. pub trait ResidentBytes { - /// Total in-memory footprint: inline `size_of` + heap allocations. + /// Total in-memory footprint: inline [`size_of()`](size_of) + heap allocations. /// - /// This is the method callers should use. It has a provided default - /// implementation; implementors only need to implement [`heap_bytes`]. + /// This is the method callers should use. It has a provided default implementation; + /// implementors only need to implement [`heap_bytes()`](Self::heap_bytes). fn resident_bytes(&self) -> usize { std::mem::size_of_val(self) + self.heap_bytes() } - /// Heap allocations only, beyond the inline `size_of`. + /// Heap allocations only, beyond the inline [`size_of()`](size_of). /// - /// Container types call this on their children to avoid double-counting - /// inline sizes that are already part of the container's backing allocation. + /// Container types call this on their children to avoid double-counting inline sizes that are + /// already part of the container's backing allocation. fn heap_bytes(&self) -> usize; } @@ -75,8 +131,11 @@ impl ResidentBytes for Vec { fn heap_bytes(&self) -> usize { // Backing array: capacity slots (inline size per slot) let backing = self.capacity() * size_of::(); + // Children's heap allocations let children: usize = self.iter().map(|v| v.heap_bytes()).sum(); + + // Total heap backing + children } } @@ -107,13 +166,22 @@ impl ResidentBytes for Arc { impl ResidentBytes for HashMap { fn heap_bytes(&self) -> usize { - // Backing array: capacity * (entry inline size + ~1 byte control metadata) - let backing = self.capacity() * (size_of::<(K, V)>() + 1); + let cap = self.capacity(); + if cap == 0 { + // HashMap::new() does not allocate until first insert. + return 0; + } + + let buckets = + (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); + let backing = buckets * size_of::<(K, V)>() + buckets + hashmap::CONTROL_GROUP_PADDING; + // Children's heap allocations (only for occupied entries) let children: usize = self .iter() .map(|(k, v)| k.heap_bytes() + v.heap_bytes()) .sum(); + backing + children } } @@ -124,18 +192,21 @@ impl ResidentBytes for BTreeMap { return 0; // Empty BTreeMaps do not allocate on the heap. } - // Node count from average fill, not max capacity - let nodes = self.len().div_ceil(BTREE_AVERAGE_FILL); - // Keys and values are separate arrays in the node, not (K, V) tuples - let node_size = BTREE_NODE_OVERHEAD - + (BTREE_NODE_CAPACITY * size_of::()) - + (BTREE_NODE_CAPACITY * size_of::()); - let structural = nodes * node_size; + let (total_nodes, internal_nodes) = btree::node_counts(self.len()); + + // Base node size (shared by leaf and internal): overhead + key/value arrays + let leaf_size = btree::NODE_OVERHEAD + + (btree::NODE_CAPACITY * size_of::()) + + (btree::NODE_CAPACITY * size_of::()); + // Internal nodes additionally carry an edge pointer array + let structural = total_nodes * leaf_size + internal_nodes * btree::EDGE_ARRAY_SIZE; + // Children's heap allocations (only for occupied entries) let children: usize = self .iter() .map(|(k, v)| k.heap_bytes() + v.heap_bytes()) .sum(); + structural + children } } @@ -146,10 +217,11 @@ impl ResidentBytes for BTreeSet { return 0; } + let (total_nodes, internal_nodes) = btree::node_counts(self.len()); + // BTreeSet is backed by BTreeMap — vals array is zero-size - let nodes = self.len().div_ceil(BTREE_AVERAGE_FILL); - let node_size = BTREE_NODE_OVERHEAD + (BTREE_NODE_CAPACITY * size_of::()); - let structural = nodes * node_size; + let leaf_size = btree::NODE_OVERHEAD + (btree::NODE_CAPACITY * size_of::()); + let structural = total_nodes * leaf_size + internal_nodes * btree::EDGE_ARRAY_SIZE; let children: usize = self.iter().map(|v| v.heap_bytes()).sum(); structural + children } @@ -157,7 +229,14 @@ impl ResidentBytes for BTreeSet { impl ResidentBytes for HashSet { fn heap_bytes(&self) -> usize { - let backing = self.capacity() * (size_of::() + 1); + let cap = self.capacity(); + if cap == 0 { + return 0; + } + + let buckets = + (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); + let backing = buckets * size_of::() + buckets + hashmap::CONTROL_GROUP_PADDING; let children: usize = self.iter().map(|v| v.heap_bytes()).sum(); backing + children } @@ -169,7 +248,8 @@ impl ResidentBytes for (A, B) { } } -// Primitive types: no heap allocation +// Primitive types: no heap allocation (stack-only) + impl ResidentBytes for bool { fn heap_bytes(&self) -> usize { 0 @@ -402,13 +482,29 @@ impl ResidentBytes for CallableSubtype { } } +#[cfg(feature = "developer-mode")] +impl ResidentBytes for Span { + fn heap_bytes(&self) -> usize { + 0 // 4 × u32, all inline + } +} + impl ResidentBytes for SymbolicExpression { fn heap_bytes(&self) -> usize { - // Only production fields: expr + id. - // developer-mode fields (span, pre_comments, end_line_comment, post_comments) - // are excluded for deterministic sizing between dev and prod builds. - self.expr.heap_bytes() + #[allow(unused_mut)] + let mut total = self.expr.heap_bytes(); // id is u64 — no heap allocation + + #[cfg(feature = "developer-mode")] + { + // span is inline (no heap), but pre_comments, end_line_comment, and + // post_comments have heap allocations via Vec/String. + total += self.pre_comments.heap_bytes(); + total += self.end_line_comment.heap_bytes(); + total += self.post_comments.heap_bytes(); + } + + total } } @@ -537,6 +633,7 @@ mod tests { #[test] fn test_type_signature_scalar_heap_is_zero() { + // Heap bytes for scalar types should be zero assert_eq!(TypeSignature::IntType.heap_bytes(), 0); assert_eq!(TypeSignature::BoolType.heap_bytes(), 0); // But resident_bytes includes size_of::() From 936d775f474286073957887bf589296e5451fb5a Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:17:25 +0100 Subject: [PATCH 04/12] add tests for everything (code coverage) --- clarity-types/src/resident_bytes.rs | 704 +++++++++++++++++++++++----- 1 file changed, 587 insertions(+), 117 deletions(-) diff --git a/clarity-types/src/resident_bytes.rs b/clarity-types/src/resident_bytes.rs index 84647776498..5b1bcc88bf5 100644 --- a/clarity-types/src/resident_bytes.rs +++ b/clarity-types/src/resident_bytes.rs @@ -74,6 +74,23 @@ mod btree { } (leaves + internal, internal) } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn btree_node_counts() { + // 7 entries: 1 leaf, 0 internal + assert_eq!(node_counts(7), (1, 0)); + // 12 entries: 2 leaves + 1 internal root + let (total, internal) = node_counts(12); + assert_eq!(total, 3); + assert_eq!(internal, 1); + // 0 entries edge case + assert_eq!(node_counts(0), (0, 0)); + } + } } /// Layout constants for `std::collections::HashMap` / `HashSet`. @@ -84,8 +101,9 @@ mod btree { /// * `hashbrown` targets a 7/8 max load factor: it allocates more buckets than `capacity()` /// reports. `capacity()` returns the number of insertions before reallocation, not the bucket /// count. Actual buckets ~= `ceil(capacity * LOAD_FACTOR_INV_NUM / LOAD_FACTOR_INV_DEN)`. -/// * Each bucket has a 1-byte control tag. The control array is padded by `Group::WIDTH` bytes (16 -/// on platforms with 128-bit SIMD, 8 otherwise) for SIMD probing at the end of the table. +/// * Each bucket has a 1-byte control tag. The control array is padded by `Group::WIDTH` bytes +/// (4, 8, or 16 depending on target SIMD support) for probing at the end of the table. +/// We use 16 as a conservative upper bound. /// * `hashbrown` also aligns `buckets * entry_size` up to `ctrl_align` (max of entry alignment and /// Group alignment) before placing control bytes. We don't model this padding — for the types /// used in Clarity, bucket counts are powers of 2 and entry alignments are <=8, so the gap is @@ -172,8 +190,7 @@ impl ResidentBytes for HashMap { return 0; } - let buckets = - (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); + let buckets = (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); let backing = buckets * size_of::<(K, V)>() + buckets + hashmap::CONTROL_GROUP_PADDING; // Children's heap allocations (only for occupied entries) @@ -234,8 +251,7 @@ impl ResidentBytes for HashSet { return 0; } - let buckets = - (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); + let buckets = (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); let backing = buckets * size_of::() + buckets + hashmap::CONTROL_GROUP_PADDING; let children: usize = self.iter().map(|v| v.heap_bytes()).sum(); backing + children @@ -535,116 +551,570 @@ impl ResidentBytes for TraitDefinition { mod tests { use super::*; - #[test] - fn test_primitive_values_include_inline_size() { - // resident_bytes() includes size_of::() even for scalar variants - let int_size = Value::Int(42).resident_bytes(); - assert!( - int_size >= size_of::(), - "Int resident_bytes ({int_size}) should be >= size_of::()" - ); - assert_eq!(Value::Int(42).heap_bytes(), 0); - } - - #[test] - fn test_u64_resident_bytes() { - let v: u64 = 42; - assert_eq!(v.resident_bytes(), 8); - assert_eq!(v.heap_bytes(), 0); - } - - #[test] - fn test_string_resident_bytes() { - let s = String::from("hello world"); - assert!(s.resident_bytes() >= size_of::() + 11); - assert!(s.heap_bytes() >= 11); - } - - #[test] - fn test_vec_resident_bytes() { - let v: Vec = vec![1, 2, 3, 4, 5]; - // heap: capacity * size_of::() = 5 * 8 = 40 bytes (no child heap) - assert!(v.heap_bytes() >= 40); - // total: size_of::>() + heap - assert!(v.resident_bytes() >= size_of::>() + 40); - } - - #[test] - fn test_hashmap_resident_bytes() { - let mut m: HashMap = HashMap::new(); - m.insert("key1".into(), 1); - m.insert("key2".into(), 2); - // Should include: HashMap header + backing array + key string heaps - assert!(m.resident_bytes() > size_of::>()); - } - - #[test] - fn test_optional_none() { - let opt: Option> = None; - assert_eq!(opt.heap_bytes(), 0); - // resident_bytes includes size_of::>>() - assert!(opt.resident_bytes() >= size_of::>>()); - } - - #[test] - fn test_optional_some_counts_content() { - let opt = OptionalData { - data: Some(Box::new(Value::Int(42))), - }; - // heap: Box (size_of::() + 0 heap) - assert!(opt.heap_bytes() > 0); - } - - #[test] - fn test_clarity_name_includes_inline_and_heap() { - let name = ClarityName::try_from("my-variable".to_string()).unwrap(); - // heap: String buffer capacity - assert!(name.heap_bytes() >= 11); - // total: size_of::() + heap - assert!(name.resident_bytes() > name.heap_bytes()); - } - - #[test] - fn test_sequence_buffer_counts_vec() { - let buf = BuffData { - data: vec![0u8; 100], - }; - assert!(buf.heap_bytes() >= 100); - } - - #[test] - fn test_list_data_recursive() { - let list = ListData { - data: vec![Value::Int(1), Value::Int(2), Value::Int(3)], - type_signature: ListTypeData::new_list(TypeSignature::IntType, 10).unwrap(), - }; - // heap: Vec backing (3 * size_of::()) + ListTypeData (Box) - assert!(list.heap_bytes() > 0); - } - - #[test] - fn test_symbolic_expression_list_recursive() { - let inner = SymbolicExpression::atom(ClarityName::try_from("x".to_string()).unwrap()); - let list = SymbolicExpression::list(vec![inner.clone(), inner.clone(), inner]); - // Should recursively count the Vec backing + each child's ClarityName heap - assert!(list.heap_bytes() > 0); - assert!(list.resident_bytes() > list.heap_bytes()); - } - - #[test] - fn test_type_signature_scalar_heap_is_zero() { - // Heap bytes for scalar types should be zero - assert_eq!(TypeSignature::IntType.heap_bytes(), 0); - assert_eq!(TypeSignature::BoolType.heap_bytes(), 0); - // But resident_bytes includes size_of::() - assert!(TypeSignature::IntType.resident_bytes() > 0); - } - - #[test] - fn test_type_signature_optional_recursive() { - let sig = TypeSignature::OptionalType(Box::new(TypeSignature::IntType)); - // heap: Box (size_of::() + 0) - assert!(sig.heap_bytes() > 0); - assert!(sig.resident_bytes() > sig.heap_bytes()); + mod primitives { + use super::*; + + #[test] + fn primitive_heap_bytes_zero() { + assert_eq!(true.heap_bytes(), 0); + assert_eq!(0u8.heap_bytes(), 0); + assert_eq!(0u32.heap_bytes(), 0); + assert_eq!(0u64.heap_bytes(), 0); + assert_eq!(0u128.heap_bytes(), 0); + assert_eq!(0i128.heap_bytes(), 0); + } + + #[test] + fn u64_resident_bytes() { + let v: u64 = 42; + assert_eq!(v.resident_bytes(), 8); + assert_eq!(v.heap_bytes(), 0); + } + } + + mod std_containers { + use super::*; + + #[test] + fn string() { + let s = String::from("hello world"); + assert!(s.resident_bytes() >= size_of::() + 11); + assert!(s.heap_bytes() >= 11); + } + + #[test] + fn vec() { + let v: Vec = vec![1, 2, 3, 4, 5]; + assert!(v.heap_bytes() >= 40); + assert!(v.resident_bytes() >= size_of::>() + 40); + } + + #[test] + fn boxed() { + let b = Box::new(String::from("boxed")); + assert!(b.heap_bytes() >= size_of::() + 5); + } + + #[test] + fn option_none() { + let opt: Option> = None; + assert_eq!(opt.heap_bytes(), 0); + assert!(opt.resident_bytes() >= size_of::>>()); + } + + #[test] + fn option_some() { + let opt: Option> = Some(Box::new(Value::Int(42))); + assert!(opt.heap_bytes() >= size_of::()); + } + + #[test] + fn arc() { + let a = Arc::new(String::from("hello")); + assert!(a.heap_bytes() >= ARC_OVERHEAD + size_of::() + 5); + } + + #[test] + fn tuple_pair() { + let t = ("hello".to_string(), 42u64); + assert!(t.heap_bytes() >= 5); + assert_eq!(42u64.heap_bytes(), 0); + } + + #[test] + fn hashmap() { + let mut m: HashMap = HashMap::new(); + m.insert("key1".into(), 1); + m.insert("key2".into(), 2); + + let cap = m.capacity(); + let buckets = + (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); + // Structural lower bound: buckets * entry_size + control bytes + let min_structural = buckets * size_of::<(String, u64)>() + buckets; + // Child heap: each key String has at least 4 bytes of heap + let min_child_heap = 2 * 4; + assert!( + m.heap_bytes() >= min_structural + min_child_heap, + "heap_bytes {} < expected minimum {}", + m.heap_bytes(), + min_structural + min_child_heap, + ); + } + + #[test] + fn hashmap_empty() { + let m: HashMap = HashMap::new(); + assert_eq!(m.heap_bytes(), 0); + } + + #[test] + fn hashset() { + let mut s: HashSet = HashSet::new(); + for i in 0..10 { + s.insert(i); + } + + let cap = s.capacity(); + let buckets = + (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); + let min_structural = buckets * size_of::() + buckets; + assert!( + s.heap_bytes() >= min_structural, + "heap_bytes {} < expected minimum {}", + s.heap_bytes(), + min_structural, + ); + } + + #[test] + fn hashset_empty() { + let s: HashSet = HashSet::new(); + assert_eq!(s.heap_bytes(), 0); + } + + #[test] + fn btreemap() { + let mut m = BTreeMap::new(); + for i in 0..20u64 { + m.insert(i, i); + } + + let (total_nodes, internal_nodes) = btree::node_counts(20); + let leaf_size = btree::NODE_OVERHEAD + + btree::NODE_CAPACITY * size_of::() + + btree::NODE_CAPACITY * size_of::(); + let min_structural = total_nodes * leaf_size + internal_nodes * btree::EDGE_ARRAY_SIZE; + assert!( + m.heap_bytes() >= min_structural, + "heap_bytes {} < expected minimum {}", + m.heap_bytes(), + min_structural, + ); + // Must account for internal nodes (20 entries > single-leaf capacity of 11) + assert!(internal_nodes >= 1); + } + + #[test] + fn btreemap_empty() { + let m: BTreeMap = BTreeMap::new(); + assert_eq!(m.heap_bytes(), 0); + } + + #[test] + fn btreeset() { + let s: BTreeSet = (0..15).collect(); + + let (total_nodes, _) = btree::node_counts(15); + let leaf_size = btree::NODE_OVERHEAD + btree::NODE_CAPACITY * size_of::(); + assert!( + s.heap_bytes() >= total_nodes * leaf_size, + "heap_bytes {} < expected minimum {}", + s.heap_bytes(), + total_nodes * leaf_size, + ); + } + + #[test] + fn btreeset_empty() { + let s: BTreeSet = BTreeSet::new(); + assert_eq!(s.heap_bytes(), 0); + } + } + + mod clarity_values { + use super::*; + + #[test] + fn int_uint_bool_no_heap() { + assert_eq!(Value::Int(42).heap_bytes(), 0); + assert_eq!(Value::UInt(42).heap_bytes(), 0); + assert_eq!(Value::Bool(true).heap_bytes(), 0); + let int_size = Value::Int(42).resident_bytes(); + assert!( + int_size >= size_of::(), + "Int resident_bytes ({int_size}) should be >= size_of::()" + ); + } + + #[test] + fn sequence_buffer() { + let buf = BuffData { + data: vec![0u8; 100], + }; + assert!(buf.heap_bytes() >= 100); + } + + #[test] + fn sequence_ascii() { + let v = Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { + data: vec![b'a', b'b', b'c'], + }))); + assert!(v.heap_bytes() >= 3); + } + + #[test] + fn sequence_utf8() { + let v = Value::Sequence(SequenceData::String(CharType::UTF8(UTF8Data { + data: vec![vec![0xC3, 0xA9], vec![0xC3, 0xB1]], + }))); + assert!(v.heap_bytes() > 0); + } + + #[test] + fn list_data() { + let list = ListData { + data: vec![Value::Int(1), Value::Int(2), Value::Int(3)], + type_signature: ListTypeData::new_list(TypeSignature::IntType, 10).unwrap(), + }; + assert!(list.heap_bytes() > 0); + } + + #[test] + fn principal_standard() { + let v = Value::Principal(PrincipalData::Standard(StandardPrincipalData::transient())); + assert_eq!(v.heap_bytes(), 0); + } + + #[test] + fn principal_contract() { + let v = Value::Principal(PrincipalData::Contract( + QualifiedContractIdentifier::transient(), + )); + assert!(v.heap_bytes() > 0); + } + + #[test] + fn tuple() { + let tuple = TupleData::from_data(vec![ + ( + ClarityName::try_from("a".to_string()).unwrap(), + Value::Int(1), + ), + ( + ClarityName::try_from("b".to_string()).unwrap(), + Value::Bool(true), + ), + ]) + .unwrap(); + assert!(Value::Tuple(tuple).heap_bytes() > 0); + } + + #[test] + fn optional() { + let opt = OptionalData { + data: Some(Box::new(Value::Int(42))), + }; + assert!(opt.heap_bytes() > 0); + } + + #[test] + fn response() { + let ok = Value::Response(ResponseData { + committed: true, + data: Box::new(Value::Int(42)), + }); + let err = Value::Response(ResponseData { + committed: false, + data: Box::new(Value::Bool(false)), + }); + assert!(ok.heap_bytes() >= size_of::()); + assert!(err.heap_bytes() >= size_of::()); + } + + #[test] + fn callable_contract() { + let v = Value::CallableContract(CallableData { + contract_identifier: QualifiedContractIdentifier::transient(), + trait_identifier: None, + }); + assert!(v.heap_bytes() > 0); + } + } + + mod clarity_identifiers { + use super::*; + + #[test] + fn clarity_name() { + let name = ClarityName::try_from("my-variable".to_string()).unwrap(); + assert!(name.heap_bytes() >= 11); + assert!(name.resident_bytes() > name.heap_bytes()); + } + + #[test] + fn contract_name() { + let name = ContractName::try_from("my-contract".to_string()).unwrap(); + assert!(name.heap_bytes() >= 11); + } + + #[test] + fn standard_principal_data() { + let p = StandardPrincipalData::transient(); + assert_eq!(p.heap_bytes(), 0); + } + + #[test] + fn qualified_contract_identifier() { + let id = QualifiedContractIdentifier::transient(); + assert!(id.heap_bytes() > 0); + } + + #[test] + fn trait_identifier() { + let id = TraitIdentifier::new( + StandardPrincipalData::transient(), + ContractName::try_from("contract".to_string()).unwrap(), + ClarityName::try_from("my-trait".to_string()).unwrap(), + ); + assert!(id.heap_bytes() > 0); + } + + #[test] + fn function_identifier() { + let fid = FunctionIdentifier::new_native_function("map"); + assert!(fid.heap_bytes() > 0); + } + } + + mod type_signatures { + use super::*; + + #[test] + fn scalar_no_heap() { + assert_eq!(TypeSignature::IntType.heap_bytes(), 0); + assert_eq!(TypeSignature::UIntType.heap_bytes(), 0); + assert_eq!(TypeSignature::BoolType.heap_bytes(), 0); + assert_eq!(TypeSignature::PrincipalType.heap_bytes(), 0); + assert_eq!(TypeSignature::NoType.heap_bytes(), 0); + assert!(TypeSignature::IntType.resident_bytes() > 0); + } + + #[test] + fn optional() { + let sig = TypeSignature::OptionalType(Box::new(TypeSignature::IntType)); + assert!(sig.heap_bytes() > 0); + assert!(sig.resident_bytes() > sig.heap_bytes()); + } + + #[test] + fn response() { + let sig = TypeSignature::ResponseType(Box::new(( + TypeSignature::IntType, + TypeSignature::BoolType, + ))); + assert!(sig.heap_bytes() > 0); + } + + #[test] + fn sequence() { + let sig = TypeSignature::SequenceType(SequenceSubtype::BufferType( + BufferLength::try_from(64u32).unwrap(), + )); + assert_eq!(sig.heap_bytes(), 0); + } + + #[test] + fn tuple() { + let sig = TypeSignature::TupleType( + TupleTypeSignature::try_from(vec![( + ClarityName::try_from("f".to_string()).unwrap(), + TypeSignature::IntType, + )]) + .unwrap(), + ); + assert!(sig.heap_bytes() > 0); + } + + #[test] + fn callable() { + let sig = TypeSignature::CallableType(CallableSubtype::Principal( + QualifiedContractIdentifier::transient(), + )); + assert!(sig.heap_bytes() > 0); + } + + #[test] + fn list_union() { + let mut set = BTreeSet::new(); + set.insert(CallableSubtype::Principal( + QualifiedContractIdentifier::transient(), + )); + let sig = TypeSignature::ListUnionType(set); + assert!(sig.heap_bytes() > 0); + } + + #[test] + fn trait_reference() { + let id = TraitIdentifier::new( + StandardPrincipalData::transient(), + ContractName::try_from("c".to_string()).unwrap(), + ClarityName::try_from("t".to_string()).unwrap(), + ); + let sig = TypeSignature::TraitReferenceType(id); + assert!(sig.heap_bytes() > 0); + } + + #[test] + fn tuple_type_signature() { + let sig = TupleTypeSignature::try_from(vec![ + ( + ClarityName::try_from("x".to_string()).unwrap(), + TypeSignature::IntType, + ), + ( + ClarityName::try_from("y".to_string()).unwrap(), + TypeSignature::BoolType, + ), + ]) + .unwrap(); + assert!(sig.heap_bytes() > ARC_OVERHEAD); + } + + #[test] + fn sequence_subtype() { + assert_eq!( + SequenceSubtype::BufferType(BufferLength::try_from(32u32).unwrap()).heap_bytes(), + 0, + ); + let list = SequenceSubtype::ListType( + ListTypeData::new_list(TypeSignature::IntType, 5).unwrap(), + ); + assert!(list.heap_bytes() > 0); + assert_eq!( + SequenceSubtype::StringType(StringSubtype::ASCII( + BufferLength::try_from(10u32).unwrap() + )) + .heap_bytes(), + 0, + ); + } + + #[test] + fn string_subtype_no_heap() { + assert_eq!( + StringSubtype::ASCII(BufferLength::try_from(10u32).unwrap()).heap_bytes(), + 0 + ); + assert_eq!( + StringSubtype::UTF8(StringUTF8Length::try_from(10u32).unwrap()).heap_bytes(), + 0 + ); + } + + #[test] + fn buffer_length_no_heap() { + assert_eq!(BufferLength::try_from(100u32).unwrap().heap_bytes(), 0); + } + + #[test] + fn string_utf8_length_no_heap() { + assert_eq!(StringUTF8Length::try_from(100u32).unwrap().heap_bytes(), 0); + } + + #[test] + fn callable_subtype_principal() { + let sub = CallableSubtype::Principal(QualifiedContractIdentifier::transient()); + assert!(sub.heap_bytes() > 0); + } + + #[test] + fn callable_subtype_trait() { + let id = TraitIdentifier::new( + StandardPrincipalData::transient(), + ContractName::try_from("c".to_string()).unwrap(), + ClarityName::try_from("t".to_string()).unwrap(), + ); + assert!(CallableSubtype::Trait(id).heap_bytes() > 0); + } + } + + mod symbolic_expressions { + use super::*; + + #[test] + fn atom() { + let inner = SymbolicExpression::atom(ClarityName::try_from("x".to_string()).unwrap()); + let list = SymbolicExpression::list(vec![inner.clone(), inner.clone(), inner]); + assert!(list.heap_bytes() > 0); + assert!(list.resident_bytes() > list.heap_bytes()); + } + + #[test] + fn atom_value() { + let expr = SymbolicExpression::atom_value(Value::Int(1)); + assert_eq!(expr.heap_bytes(), 0); + } + + #[test] + fn literal_value() { + let expr = SymbolicExpression::literal_value(Value::Bool(true)); + assert_eq!(expr.heap_bytes(), 0); + } + + #[test] + fn field() { + let id = TraitIdentifier::new( + StandardPrincipalData::transient(), + ContractName::try_from("c".to_string()).unwrap(), + ClarityName::try_from("f".to_string()).unwrap(), + ); + let expr = SymbolicExpression::field(id); + assert!(expr.heap_bytes() > 0); + } + + #[test] + fn trait_reference() { + let id = TraitIdentifier::new( + StandardPrincipalData::transient(), + ContractName::try_from("c".to_string()).unwrap(), + ClarityName::try_from("t".to_string()).unwrap(), + ); + let expr = SymbolicExpression::trait_reference( + ClarityName::try_from("name".to_string()).unwrap(), + TraitDefinition::Defined(id), + ); + assert!(expr.heap_bytes() > 0); + } + + #[test] + fn trait_definition() { + let id = TraitIdentifier::new( + StandardPrincipalData::transient(), + ContractName::try_from("c".to_string()).unwrap(), + ClarityName::try_from("t".to_string()).unwrap(), + ); + let defined = TraitDefinition::Defined(id.clone()); + let imported = TraitDefinition::Imported(id); + assert!(defined.heap_bytes() > 0); + assert_eq!(defined.heap_bytes(), imported.heap_bytes()); + } + } + + #[cfg(feature = "developer-mode")] + mod developer_mode { + use super::*; + + #[test] + fn symbolic_expression_comment_fields() { + let mut expr = + SymbolicExpression::atom(ClarityName::try_from("x".to_string()).unwrap()); + expr.pre_comments = vec![ + ("comment1".to_string(), Span::zero()), + ("comment2".to_string(), Span::zero()), + ]; + expr.end_line_comment = Some("end comment".to_string()); + expr.post_comments = vec![("post".to_string(), Span::zero())]; + + let with_comments = expr.heap_bytes(); + let plain = SymbolicExpression::atom(ClarityName::try_from("x".to_string()).unwrap()); + assert!(with_comments > plain.heap_bytes()); + } + + #[test] + fn span_no_heap() { + let s = Span::zero(); + assert_eq!(s.heap_bytes(), 0); + } } } From ce2b29ca63dfeed552c90b49fbe9e9a70fb55f40 Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:29:42 +0100 Subject: [PATCH 05/12] undo accidental rustfmt --- .../type_checker/v2_05/tests/contracts.rs | 4 ++-- clarity/src/vm/contracts.rs | 15 +++++++++------ stacks-common/src/util/secp256k1/native.rs | 9 +++++---- stacks-common/src/util/secp256k1/wasm.rs | 7 ++++--- stacks-node/src/tests/neon_integrations.rs | 4 ++-- stacks-node/src/tests/signer/multiversion.rs | 5 +---- stacks-node/src/tests/stackerdb.rs | 3 +-- stackslib/src/chainstate/tests/mod.rs | 3 ++- stackslib/src/clarity_vm/database/ephemeral.rs | 3 ++- stackslib/src/clarity_vm/database/marf.rs | 3 ++- stackslib/src/net/api/getsortition.rs | 4 ++-- stackslib/src/net/api/poststackerdbchunk.rs | 3 ++- stackslib/src/net/chat.rs | 3 ++- stackslib/src/net/codec.rs | 3 ++- stackslib/src/net/connection.rs | 3 ++- stackslib/src/net/http/response.rs | 3 +-- stackslib/src/net/inv/epoch2x.rs | 3 ++- stackslib/src/net/mod.rs | 3 +-- stackslib/src/net/p2p.rs | 7 ++++--- stackslib/src/net/poll.rs | 5 +++-- 20 files changed, 51 insertions(+), 42 deletions(-) diff --git a/clarity/src/vm/analysis/type_checker/v2_05/tests/contracts.rs b/clarity/src/vm/analysis/type_checker/v2_05/tests/contracts.rs index 9670c1d0091..2f52025543f 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/tests/contracts.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/tests/contracts.rs @@ -14,9 +14,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use assert_json_diff::{self, assert_json_eq}; -use serde_json; +use assert_json_diff::assert_json_eq; use stacks_common::types::StacksEpochId; +use {assert_json_diff, serde_json}; use crate::vm::ClarityVersion; use crate::vm::analysis::contract_interface_builder::build_contract_interface; diff --git a/clarity/src/vm/contracts.rs b/clarity/src/vm/contracts.rs index 847ec4ec3dc..e301cf55e60 100644 --- a/clarity/src/vm/contracts.rs +++ b/clarity/src/vm/contracts.rs @@ -82,6 +82,8 @@ mod tests { fn expected_contract_context_heap_bytes(contract: &Contract) -> usize { let contract_context = &contract.contract_context; + // This is a bit rigid and will break if we change ContractContext's fields, but will catch + // if we forget to include a field in the resident_bytes calculation. contract_context.contract_identifier.heap_bytes() + contract_context.variables.heap_bytes() + contract_context.functions.heap_bytes() @@ -106,6 +108,7 @@ mod tests { ); } + #[track_caller] fn initialize_contract_with_store( marf: &mut MemoryBackingStore, source: &str, @@ -138,7 +141,7 @@ mod tests { } #[test] - fn contract_resident_bytes_matches_empty_contract_context_exactly() { + fn resident_bytes_matches_empty_contract_context() { let contract_identifier = QualifiedContractIdentifier::local("resident-bytes-empty").unwrap(); let contract = Contract { @@ -163,7 +166,7 @@ mod tests { } #[test] - fn contract_resident_bytes_matches_initialized_contract_context_exactly() { + fn resident_bytes_covers_all_fields_in_rich_contract() { let contract = initialize_contract( r#" (define-data-var counter uint u0) @@ -206,7 +209,7 @@ mod tests { } #[test] - fn contract_resident_bytes_exercises_ft_nft_and_traits() { + fn resident_bytes_counts_ft_nft_and_traits() { let contract = initialize_contract( r#" (define-fungible-token gold) @@ -252,7 +255,7 @@ mod tests { } #[test] - fn contract_resident_bytes_exercises_implemented_traits() { + fn resident_bytes_counts_implemented_traits() { let mut marf = MemoryBackingStore::new(); // First contract defines the trait @@ -280,7 +283,7 @@ mod tests { assert_contract_bytes_match_context_fields(&impl_contract); - // implemented_traits contains a TraitIdentifier — verify non-zero heap + // implemented_traits contains a TraitIdentifier; verify non-zero heap let impl_heap = impl_contract .contract_context .implemented_traits @@ -292,7 +295,7 @@ mod tests { } #[test] - fn contract_resident_bytes_grows_with_additional_initialized_content() { + fn resident_bytes_grows_with_additional_initialized_content() { let single_function = initialize_contract( r#" (define-public (echo (value uint)) diff --git a/stacks-common/src/util/secp256k1/native.rs b/stacks-common/src/util/secp256k1/native.rs index 49d969311b5..e6b7452061d 100644 --- a/stacks-common/src/util/secp256k1/native.rs +++ b/stacks-common/src/util/secp256k1/native.rs @@ -14,15 +14,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use ::secp256k1; use ::secp256k1::ecdsa::{ RecoverableSignature as LibSecp256k1RecoverableSignature, RecoveryId as LibSecp256k1RecoveryID, Signature as LibSecp256k1Signature, }; pub use ::secp256k1::Error; use ::secp256k1::{ - self, constants as LibSecp256k1Constants, Error as LibSecp256k1Error, - Message as LibSecp256k1Message, PublicKey as LibSecp256k1PublicKey, Secp256k1, - SecretKey as LibSecp256k1PrivateKey, + constants as LibSecp256k1Constants, Error as LibSecp256k1Error, Message as LibSecp256k1Message, + PublicKey as LibSecp256k1PublicKey, Secp256k1, SecretKey as LibSecp256k1PrivateKey, }; use serde::de::{Deserialize, Error as de_Error}; use serde::Serialize; @@ -441,7 +441,8 @@ pub fn secp256k1_verify( #[cfg(test)] mod tests { use rand::RngCore as _; - use secp256k1::{self, PublicKey as LibSecp256k1PublicKey, Secp256k1}; + use secp256k1; + use secp256k1::{PublicKey as LibSecp256k1PublicKey, Secp256k1}; use super::*; use crate::util::get_epoch_time_ms; diff --git a/stacks-common/src/util/secp256k1/wasm.rs b/stacks-common/src/util/secp256k1/wasm.rs index 03414641485..55b5266bfd5 100644 --- a/stacks-common/src/util/secp256k1/wasm.rs +++ b/stacks-common/src/util/secp256k1/wasm.rs @@ -14,14 +14,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use ::libsecp256k1; use ::libsecp256k1::curve::Scalar; pub use ::libsecp256k1::Error; +#[cfg(not(feature = "wasm-deterministic"))] +use ::libsecp256k1::{Error as LibSecp256k1Error, Message as LibSecp256k1Message}; use ::libsecp256k1::{ - self, PublicKey as LibSecp256k1PublicKey, RecoveryId as LibSecp256k1RecoveryId, + PublicKey as LibSecp256k1PublicKey, RecoveryId as LibSecp256k1RecoveryId, SecretKey as LibSecp256k1PrivateKey, Signature as LibSecp256k1Signature, ECMULT_GEN_CONTEXT, }; -#[cfg(not(feature = "wasm-deterministic"))] -use ::libsecp256k1::{Error as LibSecp256k1Error, Message as LibSecp256k1Message}; use serde::de::{Deserialize, Error as de_Error}; use serde::Serialize; diff --git a/stacks-node/src/tests/neon_integrations.rs b/stacks-node/src/tests/neon_integrations.rs index 0477953404e..d7d1e4b2cea 100644 --- a/stacks-node/src/tests/neon_integrations.rs +++ b/stacks-node/src/tests/neon_integrations.rs @@ -287,8 +287,8 @@ pub mod test_observer { use stacks::net::api::postblock_proposal::BlockValidateResponse; use stacks::util::hash::hex_bytes; use stacks_common::types::chainstate::StacksBlockId; - use tokio; - use warp::{self, Filter}; + use warp::Filter; + use {tokio, warp}; use crate::event_dispatcher::{MinedBlockEvent, MinedMicroblockEvent, MinedNakamotoBlockEvent}; use crate::Config; diff --git a/stacks-node/src/tests/signer/multiversion.rs b/stacks-node/src/tests/signer/multiversion.rs index aff6dbc0ee4..acb5a20bda6 100644 --- a/stacks-node/src/tests/signer/multiversion.rs +++ b/stacks-node/src/tests/signer/multiversion.rs @@ -20,22 +20,19 @@ use libsigner::v0::messages::{ SignerMessageMetadata, }; use libsigner::v0::signer_state::{MinerState, ReplayTransactionSet, SignerStateMachine}; -use libsigner_v3_3_0_0_5; use libsigner_v3_3_0_0_5::v0::messages::SignerMessage as OldSignerMessage; -use signer_v3_3_0_0_5_0; use signer_v3_3_0_0_5_0::v0::signer_state::SUPPORTED_SIGNER_PROTOCOL_VERSION as OldSupportedVersion; use stacks::chainstate::stacks::StacksTransaction; use stacks::util::hash::{Hash160, Sha512Trunc256Sum}; use stacks::util::secp256k1::{MessageSignature, Secp256k1PrivateKey}; use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId}; -use stacks_common_v3_3_0_0_5; use stacks_common_v3_3_0_0_5::codec::StacksMessageCodec as OldStacksMessageCodec; use stacks_signer::runloop::{RewardCycleInfo, State, StateInfo}; use stacks_signer::v0::signer_state::{ LocalStateMachine, SUPPORTED_SIGNER_PROTOCOL_VERSION as NewSupportedVersion, }; use stacks_signer::v0::SpawnedSigner; -use stacks_v3_3_0_0_5; +use {libsigner_v3_3_0_0_5, signer_v3_3_0_0_5_0, stacks_common_v3_3_0_0_5, stacks_v3_3_0_0_5}; use super::SpawnedSignerTrait; use crate::stacks_common::codec::StacksMessageCodec; diff --git a/stacks-node/src/tests/stackerdb.rs b/stacks-node/src/tests/stackerdb.rs index 491506208fd..2fc840d4f5b 100644 --- a/stacks-node/src/tests/stackerdb.rs +++ b/stacks-node/src/tests/stackerdb.rs @@ -17,13 +17,12 @@ use std::{env, thread}; use clarity::vm::types::QualifiedContractIdentifier; -use reqwest; -use serde_json; use stacks::chainstate::stacks::StacksPrivateKey; use stacks::config::{EventKeyType, InitialBalance}; use stacks::libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; use stacks_common::types::chainstate::StacksAddress; use stacks_common::util::hash::Sha512Trunc256Sum; +use {reqwest, serde_json}; use crate::burnchains::bitcoin::core_controller::BitcoinCoreController; use crate::burnchains::BurnchainController; diff --git a/stackslib/src/chainstate/tests/mod.rs b/stackslib/src/chainstate/tests/mod.rs index 792400bb0a5..7c86e8fc838 100644 --- a/stackslib/src/chainstate/tests/mod.rs +++ b/stackslib/src/chainstate/tests/mod.rs @@ -35,7 +35,8 @@ use clarity::vm::costs::ExecutionCost; use clarity::vm::database::STXBalance; use clarity::vm::types::*; use clarity::vm::ContractName; -use rand::{self, thread_rng, Rng}; +use rand; +use rand::{thread_rng, Rng}; use stacks_common::address::*; use stacks_common::deps_common::bitcoin::network::serialize::BitcoinHash; use stacks_common::types::StacksEpochId; diff --git a/stackslib/src/clarity_vm/database/ephemeral.rs b/stackslib/src/clarity_vm/database/ephemeral.rs index 3a9aff56dc3..448afeb7ab2 100644 --- a/stackslib/src/clarity_vm/database/ephemeral.rs +++ b/stackslib/src/clarity_vm/database/ephemeral.rs @@ -23,7 +23,8 @@ use clarity::vm::database::sqlite::{ use clarity::vm::database::{ClarityBackingStore, SpecialCaseHandler, SqliteConnection}; use clarity::vm::errors::{RuntimeError, VmExecutionError, VmInternalError}; use clarity::vm::types::QualifiedContractIdentifier; -use rusqlite::{self, Connection}; +use rusqlite; +use rusqlite::Connection; use stacks_common::codec::StacksMessageCodec; use stacks_common::types::chainstate::{BlockHeaderHash, StacksBlockId, TrieHash}; use stacks_common::types::sqlite::NO_PARAMS; diff --git a/stackslib/src/clarity_vm/database/marf.rs b/stackslib/src/clarity_vm/database/marf.rs index 9f8a2d3993d..9f11720b7a9 100644 --- a/stackslib/src/clarity_vm/database/marf.rs +++ b/stackslib/src/clarity_vm/database/marf.rs @@ -26,7 +26,8 @@ use clarity::vm::database::sqlite::{ use clarity::vm::database::{ClarityBackingStore, SpecialCaseHandler, SqliteConnection}; use clarity::vm::errors::{IncomparableError, RuntimeError, VmExecutionError, VmInternalError}; use clarity::vm::types::QualifiedContractIdentifier; -use rusqlite::{self, Connection}; +use rusqlite; +use rusqlite::Connection; use stacks_common::codec::StacksMessageCodec; use stacks_common::types::chainstate::{BlockHeaderHash, StacksBlockId, TrieHash}; diff --git a/stackslib/src/net/api/getsortition.rs b/stackslib/src/net/api/getsortition.rs index d79faf711dc..365b6588cd9 100644 --- a/stackslib/src/net/api/getsortition.rs +++ b/stackslib/src/net/api/getsortition.rs @@ -15,14 +15,14 @@ use clarity::types::chainstate::VRFSeed; use regex::{Captures, Regex}; -use serde::{self, Serialize}; -use serde_json; +use serde::Serialize; use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, SortitionId, StacksBlockId, }; use stacks_common::types::net::PeerHost; use stacks_common::util::hash::Hash160; use stacks_common::util::serde_serializers::{prefix_hex, prefix_opt_hex}; +use {serde, serde_json}; use crate::chainstate::burn::db::sortdb::SortitionDB; use crate::chainstate::burn::BlockSnapshot; diff --git a/stackslib/src/net/api/poststackerdbchunk.rs b/stackslib/src/net/api/poststackerdbchunk.rs index 3d2ea15c300..dc3981268cf 100644 --- a/stackslib/src/net/api/poststackerdbchunk.rs +++ b/stackslib/src/net/api/poststackerdbchunk.rs @@ -18,7 +18,8 @@ use clarity::vm::representations::{CONTRACT_NAME_REGEX_STRING, STANDARD_PRINCIPA use clarity::vm::types::QualifiedContractIdentifier; use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; use regex::{Captures, Regex}; -use serde_json::{self, json}; +use serde_json; +use serde_json::json; use stacks_common::codec::MAX_MESSAGE_LEN; use stacks_common::types::net::PeerHost; use stacks_common::util::secp256k1::MessageSignature; diff --git a/stackslib/src/net/chat.rs b/stackslib/src/net/chat.rs index 5ad242fbfee..84bb94dbc8e 100644 --- a/stackslib/src/net/chat.rs +++ b/stackslib/src/net/chat.rs @@ -20,7 +20,8 @@ use std::net::SocketAddr; use std::{cmp, mem}; use clarity::vm::types::QualifiedContractIdentifier; -use rand::{self, thread_rng, Rng}; +use rand; +use rand::{thread_rng, Rng}; use stacks_common::types::net::PeerAddress; use stacks_common::types::StacksPublicKeyBuffer; use stacks_common::util::hash::to_hex; diff --git a/stackslib/src/net/codec.rs b/stackslib/src/net/codec.rs index 3e2b5eeba85..a94ddce6898 100644 --- a/stackslib/src/net/codec.rs +++ b/stackslib/src/net/codec.rs @@ -21,7 +21,8 @@ use std::io::Read; use clarity::vm::types::QualifiedContractIdentifier; use clarity::vm::ContractName; -use rand::{self, Rng}; +use rand; +use rand::Rng; use sha2::{Digest, Sha512_256}; use stacks_common::bitvec::BitVec; use stacks_common::codec::{ diff --git a/stackslib/src/net/connection.rs b/stackslib/src/net/connection.rs index b3382666522..60367f66608 100644 --- a/stackslib/src/net/connection.rs +++ b/stackslib/src/net/connection.rs @@ -1550,7 +1550,8 @@ mod test { use std::sync::{Arc, Mutex}; use std::{io, thread}; - use rand::{self, RngCore}; + use rand; + use rand::RngCore; use stacks_common::util::secp256k1::*; use stacks_common::util::*; diff --git a/stackslib/src/net/http/response.rs b/stackslib/src/net/http/response.rs index 7cf3ec8d58c..1428352e3e8 100644 --- a/stackslib/src/net/http/response.rs +++ b/stackslib/src/net/http/response.rs @@ -18,8 +18,6 @@ use std::collections::{BTreeMap, HashSet}; use std::fmt; use std::io::{Read, Write}; -use serde; -use serde_json; use stacks_common::codec::{Error as CodecError, StacksMessageCodec}; use stacks_common::deps_common::httparse; use stacks_common::util::chunked_encoding::{ @@ -27,6 +25,7 @@ use stacks_common::util::chunked_encoding::{ }; use stacks_common::util::hash::to_hex; use stacks_common::util::pipe::PipeWrite; +use {serde, serde_json}; use crate::net::http::common::{ HttpReservedHeader, HTTP_PREAMBLE_MAX_ENCODED_SIZE, HTTP_PREAMBLE_MAX_NUM_HEADERS, diff --git a/stackslib/src/net/inv/epoch2x.rs b/stackslib/src/net/inv/epoch2x.rs index 03f3c81b119..312b67a8c05 100644 --- a/stackslib/src/net/inv/epoch2x.rs +++ b/stackslib/src/net/inv/epoch2x.rs @@ -18,8 +18,9 @@ use std::cmp; use std::collections::{HashMap, HashSet}; use p2p::DropSource; +use rand; use rand::seq::SliceRandom; -use rand::{self, thread_rng}; +use rand::thread_rng; use stacks_common::types::chainstate::{BlockHeaderHash, PoxId}; use stacks_common::util::get_epoch_time_secs; diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index 86d9baf4b0e..44e18e0c4e9 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -23,7 +23,6 @@ use clarity::vm::errors::{ClarityTypeError, VmExecutionError}; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; use libstackerdb::{Error as libstackerdb_error, StackerDBChunkData}; use p2p::{DropReason, DropSource}; -use rusqlite; use serde::{Deserialize, Serialize}; use stacks_common::bitvec::BitVec; use stacks_common::codec::{Error as codec_error, StacksMessageCodec}; @@ -35,7 +34,7 @@ use stacks_common::types::StacksPublicKeyBuffer; use stacks_common::util::get_epoch_time_secs; use stacks_common::util::hash::{Hash160, Sha256Sum}; use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey}; -use url; +use {rusqlite, url}; use self::dns::*; use crate::burnchains::{Error as burnchain_error, Txid}; diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs index 8d9344987ea..a4d7d99ce34 100644 --- a/stackslib/src/net/p2p.rs +++ b/stackslib/src/net/p2p.rs @@ -21,7 +21,7 @@ use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TryRecvError, TrySendE use std::thread::JoinHandle; use clarity::vm::types::QualifiedContractIdentifier; -use mio::{self, net as mio_net}; +use mio::net as mio_net; use rand::prelude::*; use rand::thread_rng; use stacks_common::consts::{FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH}; @@ -31,7 +31,7 @@ use stacks_common::types::StacksEpochId; use stacks_common::util::hash::to_hex; use stacks_common::util::secp256k1::Secp256k1PublicKey; use stacks_common::util::{get_epoch_time_ms, get_epoch_time_secs}; -use url; +use {mio, url}; use crate::burnchains::db::{BurnchainDB, BurnchainHeaderReader}; use crate::burnchains::{Burnchain, BurnchainView}; @@ -5568,7 +5568,8 @@ mod test { use std::{thread, time}; use clarity::util::sleep_ms; - use rand::{self, RngCore}; + use rand; + use rand::RngCore; use stacks_common::types::chainstate::BurnchainHeaderHash; use super::*; diff --git a/stackslib/src/net/poll.rs b/stackslib/src/net/poll.rs index ff4abfd7b3e..2589fed0801 100644 --- a/stackslib/src/net/poll.rs +++ b/stackslib/src/net/poll.rs @@ -20,9 +20,10 @@ use std::net::{Shutdown, SocketAddr}; use std::time::Duration; use std::{io, time}; -use mio::{self, net as mio_net, PollOpt, Ready, Token}; -use rand::{self, RngCore}; +use mio::{net as mio_net, PollOpt, Ready, Token}; +use rand::RngCore; use stacks_common::util::sleep_ms; +use {mio, rand}; use crate::net::Error as net_error; From 5969e8137ba5de64811b7c55671286e0054a75b0 Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:19:13 +0200 Subject: [PATCH 06/12] copilot pr comments and docs improvements --- clarity-types/src/resident_bytes.rs | 47 +++++++++++++++-------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/clarity-types/src/resident_bytes.rs b/clarity-types/src/resident_bytes.rs index 5b1bcc88bf5..1707b53e3b6 100644 --- a/clarity-types/src/resident_bytes.rs +++ b/clarity-types/src/resident_bytes.rs @@ -93,49 +93,50 @@ mod btree { } } -/// Layout constants for `std::collections::HashMap` / `HashSet`. +/// Layout constants for [`HashMap`] / [`HashSet`]. /// -/// std's HashMap has been backed by hashbrown since Rust 1.36. These constants reflect hashbrown -/// internals that are not exposed through any std API. +/// `std`'s HashMap has been backed by `hashbrown` since Rust 1.36. These constants reflect +/// internals that are not exposed through any `std` API. /// /// * `hashbrown` targets a 7/8 max load factor: it allocates more buckets than `capacity()` /// reports. `capacity()` returns the number of insertions before reallocation, not the bucket /// count. Actual buckets ~= `ceil(capacity * LOAD_FACTOR_INV_NUM / LOAD_FACTOR_INV_DEN)`. -/// * Each bucket has a 1-byte control tag. The control array is padded by `Group::WIDTH` bytes -/// (4, 8, or 16 depending on target SIMD support) for probing at the end of the table. -/// We use 16 as a conservative upper bound. +/// * Each bucket has a 1-byte control tag. The control array is padded by `Group::WIDTH` bytes (4, +/// 8, or 16 depending on target SIMD support) for probing at the end of the table. We use 16 as a +/// conservative upper bound. /// * `hashbrown` also aligns `buckets * entry_size` up to `ctrl_align` (max of entry alignment and /// Group alignment) before placing control bytes. We don't model this padding — for the types /// used in Clarity, bucket counts are powers of 2 and entry alignments are <=8, so the gap is /// typically zero. mod hashmap { - /// Inverse of hashbrown's max load factor (7/8), as a fraction: `buckets ~= (capacity * 8/7)`. + /// Inverse of `hashbrown`'s max load factor (7/8), as a fraction: `buckets ~= (capacity * 8/7)`. pub const LOAD_FACTOR_INV_NUM: usize = 8; pub const LOAD_FACTOR_INV_DEN: usize = 7; /// Conservative upper bound for SIMD group width padding appended to the control byte array. - /// hashbrown's actual `Group::WIDTH` varies by target (4, 8, or 16 bytes); 16 is the max - /// (SSE2 path on x86_64) and overestimates by at most 12 bytes on other platforms. + /// `hashbrown`'s actual `Group::WIDTH` varies by target (4, 8, or 16 bytes); 16 is the max + /// (`SSE2` path on `x86_64`) and overestimates by at most 12 bytes on other platforms. pub const CONTROL_GROUP_PADDING: usize = 16; - - // NOTE: } /// Reports the approximate in-memory footprint of an instance, in bytes. /// -/// See module-level documentation for the two-method design. -pub trait ResidentBytes { - /// Total in-memory footprint: inline [`size_of()`](size_of) + heap allocations. +/// The trait is split into two methods to avoid double-counting when composing nested types. A +/// container (e.g. `Vec`) already accounts for the inline `size_of::()` of each element in +/// its heap allocation, so it must call [`heap_bytes()`](Self::heap_bytes) — not `resident_bytes()` +/// — on each child. Only the outermost caller uses [`resident_bytes()`](Self::resident_bytes), +/// which adds `size_of::()` exactly once. +pub trait ResidentBytes: Sized { + /// Total approximate memory footprint of this instance. /// - /// This is the method callers should use. It has a provided default implementation; - /// implementors only need to implement [`heap_bytes()`](Self::heap_bytes). + /// Default implementation: [`size_of::()`](size_of) (inline size) + + /// [`heap_bytes()`](Self::heap_bytes) (additional heap allocations). fn resident_bytes(&self) -> usize { - std::mem::size_of_val(self) + self.heap_bytes() + // Note: if we ever need to support unsized types, we should switch to size_of_val(self) + // here instead of size_of::() and remove the Sized trait bound. + std::mem::size_of::() + self.heap_bytes() } - /// Heap allocations only, beyond the inline [`size_of()`](size_of). - /// - /// Container types call this on their children to avoid double-counting inline sizes that are - /// already part of the container's backing allocation. + /// Heap allocations only, beyond the inline size reported by [`size_of::()`](size_of). fn heap_bytes(&self) -> usize; } @@ -168,7 +169,7 @@ impl ResidentBytes for Box { impl ResidentBytes for Option { fn heap_bytes(&self) -> usize { match self { - // For Some, the T is inline in the Option — only count T's heap + // For Some, the T is inline in the Option; only count T's heap Some(v) => v.heap_bytes(), None => 0, } @@ -178,7 +179,7 @@ impl ResidentBytes for Option { impl ResidentBytes for Arc { fn heap_bytes(&self) -> usize { // Arc heap-allocates: header (strong + weak counts, ~16 bytes) + T inline + T's heap - 16 + size_of::() + (**self).heap_bytes() + ARC_OVERHEAD + size_of::() + (**self).heap_bytes() } } From a66a77f7657f833acb722e1cebff3db22df6873e Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:22:11 +0200 Subject: [PATCH 07/12] add changelog.d entry --- changelog.d/7049-clarity-type-size-approximation.added | 1 + changelog.d/README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/7049-clarity-type-size-approximation.added diff --git a/changelog.d/7049-clarity-type-size-approximation.added b/changelog.d/7049-clarity-type-size-approximation.added new file mode 100644 index 00000000000..b79a3077027 --- /dev/null +++ b/changelog.d/7049-clarity-type-size-approximation.added @@ -0,0 +1 @@ +New `ResidentBytes` trait for types which can approximate their resident memory size (stack+heap) \ No newline at end of file diff --git a/changelog.d/README.md b/changelog.d/README.md index 8312dfdf556..5b90cb08e3f 100644 --- a/changelog.d/README.md +++ b/changelog.d/README.md @@ -17,7 +17,7 @@ process clearer. 2. Write the changelog entry text in the file (one or more lines of markdown): - ``` + ```text Added `marf_compress` as a node configuration parameter to enable MARF compression feature ([#6811](https://github.com/stacks-network/stacks-core/pull/6811)) ``` From 39105fb3b44f4f5d3dbe637b3774f8fdfdf97e97 Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:19:45 +0200 Subject: [PATCH 08/12] clarify arc comment and make contractcontext test more robust --- clarity-types/src/resident_bytes.rs | 3 +- clarity/src/vm/contexts.rs | 40 ++++++++++++++++------ clarity/src/vm/contracts.rs | 53 +++++++++-------------------- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/clarity-types/src/resident_bytes.rs b/clarity-types/src/resident_bytes.rs index 1707b53e3b6..bf0e1a05164 100644 --- a/clarity-types/src/resident_bytes.rs +++ b/clarity-types/src/resident_bytes.rs @@ -178,7 +178,8 @@ impl ResidentBytes for Option { impl ResidentBytes for Arc { fn heap_bytes(&self) -> usize { - // Arc heap-allocates: header (strong + weak counts, ~16 bytes) + T inline + T's heap + // Counts the Arc allocation (header + pointee). Shared backing may be overcounted if + // multiple Arc handles to the same allocation are reachable in one measured graph. ARC_OVERHEAD + size_of::() + (**self).heap_bytes() } } diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index 4596a2466a4..61849e5c763 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -333,17 +333,35 @@ pub struct ContractContext { impl ResidentBytes for ContractContext { fn heap_bytes(&self) -> usize { - self.contract_identifier.heap_bytes() - + self.variables.heap_bytes() - + self.functions.heap_bytes() - + self.defined_traits.heap_bytes() - + self.implemented_traits.heap_bytes() - + self.persisted_names.heap_bytes() - + self.meta_data_map.heap_bytes() - + self.meta_data_var.heap_bytes() - + self.meta_nft.heap_bytes() - + self.meta_ft.heap_bytes() - // data_size: u64, clarity_version: enum — inline, covered by size_of::() + // Destructure to get a compile error when a field is added without accounting for it. + let ContractContext { + // Heap-allocated fields: accounted for by heap_bytes() calls below + contract_identifier, + variables, + functions, + defined_traits, + implemented_traits, + persisted_names, + meta_data_map, + meta_data_var, + meta_nft, + meta_ft, + // Inline-only fields: covered by size_of::() + data_size: _, + clarity_version: _, + is_deploying: _, + } = self; + + contract_identifier.heap_bytes() + + variables.heap_bytes() + + functions.heap_bytes() + + defined_traits.heap_bytes() + + implemented_traits.heap_bytes() + + persisted_names.heap_bytes() + + meta_data_map.heap_bytes() + + meta_data_var.heap_bytes() + + meta_nft.heap_bytes() + + meta_ft.heap_bytes() } } diff --git a/clarity/src/vm/contracts.rs b/clarity/src/vm/contracts.rs index c3a97bd048c..ae753877382 100644 --- a/clarity/src/vm/contracts.rs +++ b/clarity/src/vm/contracts.rs @@ -35,8 +35,6 @@ impl ResidentBytes for Contract { } } -// AARON: this is an increasingly useless wrapper around a ContractContext struct. -// will probably be removed soon. impl Contract { pub fn initialize_from_ast( contract_identifier: QualifiedContractIdentifier, @@ -81,33 +79,16 @@ mod tests { use crate::vm::types::QualifiedContractIdentifier; use crate::vm::version::ClarityVersion; - fn expected_contract_context_heap_bytes(contract: &Contract) -> usize { - let contract_context = &contract.contract_context; - - // This is a bit rigid and will break if we change ContractContext's fields, but will catch - // if we forget to include a field in the resident_bytes calculation. - contract_context.contract_identifier.heap_bytes() - + contract_context.variables.heap_bytes() - + contract_context.functions.heap_bytes() - + contract_context.defined_traits.heap_bytes() - + contract_context.implemented_traits.heap_bytes() - + contract_context.persisted_names.heap_bytes() - + contract_context.meta_data_map.heap_bytes() - + contract_context.meta_data_var.heap_bytes() - + contract_context.meta_nft.heap_bytes() - + contract_context.meta_ft.heap_bytes() - } - + /// Verify that `Contract::heap_bytes` delegates cleanly to `ContractContext::heap_bytes`, and + /// that `resident_bytes` adds exactly `size_of::()`. + /// + /// Field-coverage (compile-time guard against forgotten fields) is enforced by the + /// destructuring inside `ContractContext::heap_bytes()` itself. #[track_caller] - fn assert_contract_bytes_match_context_fields(contract: &Contract) { - let expected_heap = expected_contract_context_heap_bytes(contract); - - assert_eq!(contract.contract_context.heap_bytes(), expected_heap); - assert_eq!(contract.heap_bytes(), expected_heap); - assert_eq!( - contract.resident_bytes(), - size_of::() + expected_heap - ); + fn assert_contract_bytes_consistent(contract: &Contract) { + let ctx_heap = contract.contract_context.heap_bytes(); + assert_eq!(contract.heap_bytes(), ctx_heap); + assert_eq!(contract.resident_bytes(), size_of::() + ctx_heap); } #[track_caller] @@ -160,7 +141,7 @@ mod tests { assert!(contract.contract_context.meta_nft.is_empty()); assert!(contract.contract_context.meta_ft.is_empty()); - assert_contract_bytes_match_context_fields(&contract); + assert_contract_bytes_consistent(&contract); assert_eq!( contract.heap_bytes(), contract.contract_context.contract_identifier.heap_bytes() @@ -199,10 +180,10 @@ mod tests { assert_eq!(contract.contract_context.meta_data_var.len(), 1); assert!(contract.contract_context.persisted_names.len() >= 2); - assert_contract_bytes_match_context_fields(&contract); + assert_contract_bytes_consistent(&contract); - // Magnitude check: a contract with 3 functions, a map, a var, and a constant - // must have substantial heap allocation beyond the bare struct size. + // Magnitude check: a contract with 3 functions, a map, a var, and a constant must have + // substantial heap allocation beyond the bare struct size. assert!( contract.resident_bytes() > size_of::() + 1000, "rich contract resident_bytes ({}) should exceed struct size + 1000", @@ -229,7 +210,7 @@ mod tests { assert_eq!(contract.contract_context.meta_nft.len(), 2); assert_eq!(contract.contract_context.defined_traits.len(), 1); - assert_contract_bytes_match_context_fields(&contract); + assert_contract_bytes_consistent(&contract); // meta_nft contains a tuple key type (badge) — verify it contributes heap bytes let nft_heap: usize = contract @@ -283,7 +264,7 @@ mod tests { assert_eq!(impl_contract.contract_context.implemented_traits.len(), 1); - assert_contract_bytes_match_context_fields(&impl_contract); + assert_contract_bytes_consistent(&impl_contract); // implemented_traits contains a TraitIdentifier; verify non-zero heap let impl_heap = impl_contract @@ -321,8 +302,8 @@ mod tests { "resident-bytes-many-fns", ); - assert_contract_bytes_match_context_fields(&single_function); - assert_contract_bytes_match_context_fields(&many_functions); + assert_contract_bytes_consistent(&single_function); + assert_contract_bytes_consistent(&many_functions); assert!( many_functions.contract_context.functions.len() > single_function.contract_context.functions.len() From 7da2a82997f3bd75d321373eb20afb68d1df224d Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:59:07 +0200 Subject: [PATCH 09/12] cleanup comments --- clarity-types/src/resident_bytes.rs | 44 ++++++++++------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/clarity-types/src/resident_bytes.rs b/clarity-types/src/resident_bytes.rs index bf0e1a05164..a5945c1189b 100644 --- a/clarity-types/src/resident_bytes.rs +++ b/clarity-types/src/resident_bytes.rs @@ -41,13 +41,9 @@ const ARC_OVERHEAD: usize = 16; /// Layout constants for `std::collections::BTreeMap` / `BTreeSet`. /// -/// Rust's BTreeMap uses `B=6` (hardcoded). Each node holds up to `CAPACITY = 2*B-1 = 11` entries: -/// * **LeafNode** layout: parent ptr (8) + parent_idx (2) + len (2) + padding (~4) + keys: -/// `[MaybeUninit; 11]` + vals: `[MaybeUninit; 11]`. -/// * **InternalNode** layout: LeafNode fields + edges: `[MaybeUninit>; 12]`. -/// * Allocator header adds ~16 bytes per node. -/// * Total per-node overhead (metadata + allocator): ~32 bytes. Average fill factor ~2/3 → ~7 -/// entries per node. Internal nodes at average fill have ~8 children. +/// BTreeMap uses `B=6`, so nodes hold up to `2*B-1 = 11` entries. Leaf nodes store keys+values; +/// internal nodes add 12 edge pointers. ~32 bytes overhead per node (metadata + allocator header), +/// ~2/3 average fill (~7 entries/node, ~8 children for internal nodes). mod btree { use std::mem::size_of; @@ -93,38 +89,28 @@ mod btree { } } -/// Layout constants for [`HashMap`] / [`HashSet`]. +/// Layout constants for [`HashMap`] / [`HashSet`] (hashbrown-backed since Rust 1.36). /// -/// `std`'s HashMap has been backed by `hashbrown` since Rust 1.36. These constants reflect -/// internals that are not exposed through any `std` API. +/// `hashbrown` uses a 7/8 max load factor and 1-byte control tags per bucket. /// -/// * `hashbrown` targets a 7/8 max load factor: it allocates more buckets than `capacity()` -/// reports. `capacity()` returns the number of insertions before reallocation, not the bucket -/// count. Actual buckets ~= `ceil(capacity * LOAD_FACTOR_INV_NUM / LOAD_FACTOR_INV_DEN)`. -/// * Each bucket has a 1-byte control tag. The control array is padded by `Group::WIDTH` bytes (4, -/// 8, or 16 depending on target SIMD support) for probing at the end of the table. We use 16 as a -/// conservative upper bound. -/// * `hashbrown` also aligns `buckets * entry_size` up to `ctrl_align` (max of entry alignment and -/// Group alignment) before placing control bytes. We don't model this padding — for the types -/// used in Clarity, bucket counts are powers of 2 and entry alignments are <=8, so the gap is -/// typically zero. +/// The control array is padded by `Group::WIDTH` (4/8/16 depending on SIMD support); we use 16 as +/// an upper bound. mod hashmap { /// Inverse of `hashbrown`'s max load factor (7/8), as a fraction: `buckets ~= (capacity * 8/7)`. pub const LOAD_FACTOR_INV_NUM: usize = 8; pub const LOAD_FACTOR_INV_DEN: usize = 7; - /// Conservative upper bound for SIMD group width padding appended to the control byte array. - /// `hashbrown`'s actual `Group::WIDTH` varies by target (4, 8, or 16 bytes); 16 is the max - /// (`SSE2` path on `x86_64`) and overestimates by at most 12 bytes on other platforms. + /// Upper bound for SIMD group-width padding. In hashbrown 0.15, Group::WIDTH varies by target + /// and implementation (4/8/16 bytes), so we use 16 as a conservative upper bound for + /// control-byte padding overhead. pub const CONTROL_GROUP_PADDING: usize = 16; } -/// Reports the approximate in-memory footprint of an instance, in bytes. +/// Approximate in-memory footprint, in bytes. /// -/// The trait is split into two methods to avoid double-counting when composing nested types. A -/// container (e.g. `Vec`) already accounts for the inline `size_of::()` of each element in -/// its heap allocation, so it must call [`heap_bytes()`](Self::heap_bytes) — not `resident_bytes()` -/// — on each child. Only the outermost caller uses [`resident_bytes()`](Self::resident_bytes), -/// which adds `size_of::()` exactly once. +/// Split into [`heap_bytes()`](Self::heap_bytes) (children only) and +/// [`resident_bytes()`](Self::resident_bytes) (inline + heap) to avoid double-counting in nested +/// types — containers call `heap_bytes()` on children, only the outermost caller should use +/// `resident_bytes()`. pub trait ResidentBytes: Sized { /// Total approximate memory footprint of this instance. /// From cc5c1757e79fd5845978324d7aef01264fc5b1c2 Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:27:26 -0700 Subject: [PATCH 10/12] slightly rework hashmap calc code and add tests to prove pow2 sizing --- clarity-types/src/resident_bytes.rs | 83 ++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/clarity-types/src/resident_bytes.rs b/clarity-types/src/resident_bytes.rs index a5945c1189b..3f8f7a7a8e9 100644 --- a/clarity-types/src/resident_bytes.rs +++ b/clarity-types/src/resident_bytes.rs @@ -103,6 +103,12 @@ mod hashmap { /// and implementation (4/8/16 bytes), so we use 16 as a conservative upper bound for /// control-byte padding overhead. pub const CONTROL_GROUP_PADDING: usize = 16; + + /// Calculate the number of buckets for a given `HashMap` capacity, based on hashbrown's growth + /// strategy and load factor. + pub fn buckets_for_capacity(cap: usize) -> usize { + (cap * LOAD_FACTOR_INV_NUM).div_ceil(LOAD_FACTOR_INV_DEN) + } } /// Approximate in-memory footprint, in bytes. @@ -178,7 +184,7 @@ impl ResidentBytes for HashMap { return 0; } - let buckets = (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); + let buckets = hashmap::buckets_for_capacity(cap); let backing = buckets * size_of::<(K, V)>() + buckets + hashmap::CONTROL_GROUP_PADDING; // Children's heap allocations (only for occupied entries) @@ -239,7 +245,7 @@ impl ResidentBytes for HashSet { return 0; } - let buckets = (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); + let buckets = hashmap::buckets_for_capacity(cap); let backing = buckets * size_of::() + buckets + hashmap::CONTROL_GROUP_PADDING; let children: usize = self.iter().map(|v| v.heap_bytes()).sum(); backing + children @@ -563,6 +569,18 @@ mod tests { mod std_containers { use super::*; + const HASHMAP_CAPACITY_TRANSITIONS: &[(usize, usize, usize)] = &[ + (0, 0, 0), + (1, 3, 4), + (4, 7, 8), + (8, 14, 16), + (15, 28, 32), + (29, 56, 64), + (57, 112, 128), + (113, 224, 256), + (225, 448, 512), + ]; + #[test] fn string() { let s = String::from("hello world"); @@ -616,8 +634,7 @@ mod tests { m.insert("key2".into(), 2); let cap = m.capacity(); - let buckets = - (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); + let buckets = hashmap::buckets_for_capacity(cap); // Structural lower bound: buckets * entry_size + control bytes let min_structural = buckets * size_of::<(String, u64)>() + buckets; // Child heap: each key String has at least 4 bytes of heap @@ -636,6 +653,61 @@ mod tests { assert_eq!(m.heap_bytes(), 0); } + #[test] + fn hashmap_with_capacity_progression_matches_expected_boundaries() { + let mut observed = Vec::new(); + let mut previous = None; + + for requested in 0usize..=256 { + let map = HashMap::::with_capacity(requested); + let cap = map.capacity(); + let buckets = if cap == 0 { + 0 + } else { + hashmap::buckets_for_capacity(cap) + }; + + if previous != Some((cap, buckets)) { + observed.push((requested, cap, buckets)); + previous = Some((cap, buckets)); + } + } + + assert_eq!(observed.as_slice(), HASHMAP_CAPACITY_TRANSITIONS); + } + + #[test] + fn hashmap_capacity_boundaries_match_bucket_accounting() { + for (_, expected_cap, _) in HASHMAP_CAPACITY_TRANSITIONS.iter().copied().skip(1) { + let mut map = HashMap::with_capacity(expected_cap); + + assert_eq!( + map.capacity(), + expected_cap, + "HashMap::with_capacity({expected_cap}) returned capacity {cap}", + cap = map.capacity(), + ); + + for entry in 0..expected_cap { + map.insert(entry as u64, entry as u64); + assert_eq!( + map.capacity(), + expected_cap, + "HashMap grew before reaching capacity {expected_cap}; capacity is {cap} after {inserts} inserts", + cap = map.capacity(), + inserts = entry + 1, + ); + } + + map.insert(expected_cap as u64, expected_cap as u64); + assert!( + map.capacity() > expected_cap, + "HashMap did not grow after exceeding capacity {expected_cap}; capacity remained {cap}", + cap = map.capacity(), + ); + } + } + #[test] fn hashset() { let mut s: HashSet = HashSet::new(); @@ -644,8 +716,7 @@ mod tests { } let cap = s.capacity(); - let buckets = - (cap * hashmap::LOAD_FACTOR_INV_NUM).div_ceil(hashmap::LOAD_FACTOR_INV_DEN); + let buckets = hashmap::buckets_for_capacity(cap); let min_structural = buckets * size_of::() + buckets; assert!( s.heap_bytes() >= min_structural, From 68cb07099231cfd42b6bb306534927c36d4466c9 Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:37:56 -0700 Subject: [PATCH 11/12] simplify ResidentBytes impl for UTF8Data --- clarity-types/src/resident_bytes.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/clarity-types/src/resident_bytes.rs b/clarity-types/src/resident_bytes.rs index 3f8f7a7a8e9..c0cb16452c5 100644 --- a/clarity-types/src/resident_bytes.rs +++ b/clarity-types/src/resident_bytes.rs @@ -344,10 +344,7 @@ impl ResidentBytes for ASCIIData { impl ResidentBytes for UTF8Data { fn heap_bytes(&self) -> usize { - // Vec>: outer vec backing + each inner vec's backing - let outer = self.data.capacity() * size_of::>(); - let inner: usize = self.data.iter().map(|v| v.capacity()).sum(); - outer + inner + self.data.heap_bytes() } } From dc9ee62b2f550f79d40da55145156ea05cda0a02 Mon Sep 17 00:00:00 2001 From: Cyle Witruk <236413682+cylewitruk-stacks@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:37:20 -0700 Subject: [PATCH 12/12] improve test coverage --- clarity-types/src/resident_bytes.rs | 25 ++++++++++++ clarity/src/vm/callables.rs | 28 +++++++++++++ clarity/src/vm/database/structures.rs | 57 +++++++++++++++++++++++++++ clarity/src/vm/types/signatures.rs | 22 +++++++++++ 4 files changed, 132 insertions(+) diff --git a/clarity-types/src/resident_bytes.rs b/clarity-types/src/resident_bytes.rs index c0cb16452c5..8935bf5459d 100644 --- a/clarity-types/src/resident_bytes.rs +++ b/clarity-types/src/resident_bytes.rs @@ -801,6 +801,14 @@ mod tests { assert!(buf.heap_bytes() >= 100); } + #[test] + fn sequence_data_buffer_variant() { + let seq = SequenceData::Buffer(BuffData { + data: vec![0u8; 64], + }); + assert!(seq.heap_bytes() >= 64); + } + #[test] fn sequence_ascii() { let v = Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { @@ -826,6 +834,15 @@ mod tests { assert!(list.heap_bytes() > 0); } + #[test] + fn sequence_data_list_variant() { + let seq = SequenceData::List(ListData { + data: vec![Value::Int(1), Value::Int(2)], + type_signature: ListTypeData::new_list(TypeSignature::IntType, 10).unwrap(), + }); + assert!(seq.heap_bytes() > 0); + } + #[test] fn principal_standard() { let v = Value::Principal(PrincipalData::Standard(StandardPrincipalData::transient())); @@ -864,6 +881,14 @@ mod tests { assert!(opt.heap_bytes() > 0); } + #[test] + fn value_optional_variant() { + let value = Value::Optional(OptionalData { + data: Some(Box::new(Value::Int(42))), + }); + assert!(value.heap_bytes() >= size_of::()); + } + #[test] fn response() { let ok = Value::Response(ResponseData { diff --git a/clarity/src/vm/callables.rs b/clarity/src/vm/callables.rs index 9419276d192..3cb61c72a8f 100644 --- a/clarity/src/vm/callables.rs +++ b/clarity/src/vm/callables.rs @@ -430,6 +430,34 @@ impl DefinedFunction { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resident_bytes_defined_function_counts_all_heap_fields() { + let function = DefinedFunction { + identifier: FunctionIdentifier::new_native_function("map"), + name: ClarityName::try_from("resident-bytes-fn".to_string()).unwrap(), + arg_types: vec![TypeSignature::OptionalType(Box::new( + TypeSignature::UIntType, + ))], + define_type: DefineType::Private, + arguments: vec![ClarityName::try_from("arg".to_string()).unwrap()], + body: SymbolicExpression::atom_value(Value::Bool(true)), + }; + + let expected = function.identifier.heap_bytes() + + function.name.heap_bytes() + + function.arg_types.heap_bytes() + + function.arguments.heap_bytes() + + function.body.heap_bytes(); + + assert_eq!(function.heap_bytes(), expected); + assert!(function.heap_bytes() > 0); + } +} + impl CallableType { pub fn get_identifier(&self) -> FunctionIdentifier { match self { diff --git a/clarity/src/vm/database/structures.rs b/clarity/src/vm/database/structures.rs index 622556f6cfa..6d87f502b17 100644 --- a/clarity/src/vm/database/structures.rs +++ b/clarity/src/vm/database/structures.rs @@ -130,6 +130,63 @@ impl ResidentBytes for DataVariableMetadata { } } +#[cfg(test)] +mod resident_bytes_tests { + use super::*; + + fn heap_allocating_type() -> TypeSignature { + TypeSignature::OptionalType(Box::new(TypeSignature::UIntType)) + } + + #[test] + fn resident_bytes_fungible_token_metadata_has_no_heap() { + let none = FungibleTokenMetadata { total_supply: None }; + let some = FungibleTokenMetadata { + total_supply: Some(1), + }; + + assert_eq!(none.heap_bytes(), 0); + assert_eq!(some.heap_bytes(), 0); + } + + #[test] + fn resident_bytes_non_fungible_token_metadata_counts_key_type() { + let metadata = NonFungibleTokenMetadata { + key_type: heap_allocating_type(), + }; + + assert_eq!(metadata.heap_bytes(), metadata.key_type.heap_bytes()); + assert!(metadata.heap_bytes() > 0); + } + + #[test] + fn resident_bytes_data_map_metadata_counts_key_and_value_types() { + let metadata = DataMapMetadata { + key_type: heap_allocating_type(), + value_type: TypeSignature::ResponseType(Box::new(( + TypeSignature::BoolType, + TypeSignature::UIntType, + ))), + }; + + assert_eq!( + metadata.heap_bytes(), + metadata.key_type.heap_bytes() + metadata.value_type.heap_bytes() + ); + assert!(metadata.heap_bytes() > 0); + } + + #[test] + fn resident_bytes_data_variable_metadata_counts_value_type() { + let metadata = DataVariableMetadata { + value_type: heap_allocating_type(), + }; + + assert_eq!(metadata.heap_bytes(), metadata.value_type.heap_bytes()); + assert!(metadata.heap_bytes() > 0); + } +} + #[derive(Serialize, Deserialize)] pub struct ContractMetadata { pub contract: Contract, diff --git a/clarity/src/vm/types/signatures.rs b/clarity/src/vm/types/signatures.rs index 485853101c6..1e88131af9e 100644 --- a/clarity/src/vm/types/signatures.rs +++ b/clarity/src/vm/types/signatures.rs @@ -100,6 +100,28 @@ impl FunctionArgSignature { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resident_bytes_function_signature_counts_args_and_return() { + let signature = FunctionSignature { + args: vec![ + TypeSignature::PrincipalType, + TypeSignature::OptionalType(Box::new(TypeSignature::UIntType)), + ], + returns: TypeSignature::OptionalType(Box::new(TypeSignature::BoolType)), + }; + + assert_eq!( + signature.heap_bytes(), + signature.args.heap_bytes() + signature.returns.heap_bytes() + ); + assert!(signature.heap_bytes() > 0); + } +} + impl FunctionReturnsSignature { pub fn canonicalize(&self, epoch: &StacksEpochId) -> FunctionReturnsSignature { match self {