Skip to content

Commit 91f4c11

Browse files
Merge pull request #7426 from francesco-stacks/perf/clarity-deser-streaming
perf: drop per-element copies in Clarity value deserialization
2 parents b17d3a1 + 2247d86 commit 91f4c11

5 files changed

Lines changed: 299 additions & 67 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Reduced Clarity `Value` deserialization cost: ~57% lower peak memory for untyped reads of large lists and 15-55% less CPU across deserialization paths.

clarity-types/src/tests/types/serialization.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,3 +442,36 @@ fn test_serialize_to_hex_returns_serialization_failure() {
442442
err
443443
);
444444
}
445+
446+
/// Truncating after the first entry's value makes the unwind loop hit EOF
447+
/// while reading the next key of a partially-filled tuple frame.
448+
#[test]
449+
fn test_tuple_truncated_at_next_key_errors() {
450+
let tuple = Value::from(
451+
TupleData::from_data(vec![
452+
(
453+
ClarityName::try_from("a".to_string()).unwrap(),
454+
Value::Bool(true),
455+
),
456+
(
457+
ClarityName::try_from("b".to_string()).unwrap(),
458+
Value::Bool(false),
459+
),
460+
])
461+
.unwrap(),
462+
);
463+
let mut bytes = vec![];
464+
tuple.serialize_write(&mut bytes).unwrap();
465+
466+
// tuple prefix (1) + entry count (4) + key "a" (1-byte len + 1) +
467+
// BoolTrue prefix (1) = 8 bytes; cut right before key "b".
468+
bytes.truncate(8);
469+
470+
match Value::deserialize_read(&mut bytes.as_slice(), None, false) {
471+
Ok(value) => panic!("accidentally parsed truncated tuple: {value}"),
472+
Err(SerializationError::IOError(e)) => {
473+
assert_eq!(e.err.kind(), std::io::ErrorKind::UnexpectedEof);
474+
}
475+
Err(other) => panic!("expected IOError(UnexpectedEof), got {other:?}"),
476+
}
477+
}

clarity-types/src/tests/types/signatures.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1515
use std::collections::BTreeSet;
1616

17+
use rstest::rstest;
1718
use stacks_common::types::StacksEpochId;
1819

1920
use crate::errors::ClarityTypeError;
@@ -1059,3 +1060,165 @@ fn test_least_supertype() {
10591060
);
10601061
}
10611062
}
1063+
1064+
fn trait_id(contract: &str, name: &str) -> TraitIdentifier {
1065+
TraitIdentifier {
1066+
name: ClarityName::try_from(name.to_string()).unwrap(),
1067+
contract_identifier: QualifiedContractIdentifier::local(contract).unwrap(),
1068+
}
1069+
}
1070+
1071+
fn callable_principal_subtype(contract: &str) -> CallableSubtype {
1072+
CallableSubtype::Principal(QualifiedContractIdentifier::local(contract).unwrap())
1073+
}
1074+
1075+
fn callable_trait_subtype(contract: &str, name: &str) -> CallableSubtype {
1076+
CallableSubtype::Trait(trait_id(contract, name))
1077+
}
1078+
1079+
fn list_union_type(members: Vec<CallableSubtype>) -> TypeSignature {
1080+
TypeSignature::ListUnionType(members.into_iter().collect())
1081+
}
1082+
1083+
fn buff_type(len: u32) -> TypeSignature {
1084+
TypeSignature::SequenceType(SequenceSubtype::BufferType(
1085+
BufferLength::try_from(len).unwrap(),
1086+
))
1087+
}
1088+
1089+
fn ascii_type(len: u32) -> TypeSignature {
1090+
TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII(
1091+
BufferLength::try_from(len).unwrap(),
1092+
)))
1093+
}
1094+
1095+
fn utf8_type(len: u32) -> TypeSignature {
1096+
TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::UTF8(
1097+
StringUTF8Length::try_from(len).unwrap(),
1098+
)))
1099+
}
1100+
1101+
/// The equal-type fast path in `parent_list_type_from_iter` is only sound if
1102+
/// `least_supertype_v2_1(T, T) == Ok(T)` for every `T`. Each case seeds one
1103+
/// leaf type; its nested compounds (~64 shapes) are generated in the body.
1104+
#[rstest]
1105+
#[case::no_type(TypeSignature::NoType)]
1106+
#[case::int(TypeSignature::IntType)]
1107+
#[case::uint(TypeSignature::UIntType)]
1108+
#[case::bool(TypeSignature::BoolType)]
1109+
#[case::principal(TypeSignature::PrincipalType)]
1110+
#[case::buffer_zero_len(buff_type(0))]
1111+
#[case::buffer(buff_type(17))]
1112+
#[case::string_ascii(ascii_type(5))]
1113+
#[case::string_utf8(utf8_type(3))]
1114+
#[case::callable_principal(TypeSignature::CallableType(callable_principal_subtype("contract-a")))]
1115+
#[case::callable_trait(TypeSignature::CallableType(callable_trait_subtype(
1116+
"contract-a",
1117+
"trait-a"
1118+
)))]
1119+
#[case::trait_reference(TypeSignature::TraitReferenceType(trait_id("contract-a", "trait-a")))]
1120+
#[case::list_union_single(list_union_type(vec![callable_principal_subtype("contract-a")]))]
1121+
#[case::list_union_mixed(list_union_type(vec![
1122+
callable_principal_subtype("contract-a"),
1123+
callable_principal_subtype("contract-b"),
1124+
callable_trait_subtype("contract-a", "trait-a"),
1125+
callable_trait_subtype("contract-b", "trait-b"),
1126+
]))]
1127+
#[case::empty_list(TypeSignature::from(TypeSignature::empty_list()))]
1128+
fn test_least_supertype_v2_1_idempotent(#[case] leaf: TypeSignature) {
1129+
use crate::types::TypeSignature::*;
1130+
1131+
let mut corpus = vec![leaf];
1132+
// two wrapping rounds cover nested compounds
1133+
for _ in 0..2 {
1134+
let snapshot = corpus.clone();
1135+
for t in snapshot {
1136+
corpus.push(OptionalType(Box::new(t.clone())));
1137+
corpus.push(ResponseType(Box::new((t.clone(), NoType))));
1138+
corpus.push(ResponseType(Box::new((NoType, t.clone()))));
1139+
corpus.push(ResponseType(Box::new((t.clone(), t.clone()))));
1140+
corpus.push(TypeSignature::list_of(t.clone(), 0).unwrap());
1141+
corpus.push(TypeSignature::list_of(t.clone(), 4).unwrap());
1142+
corpus.push(
1143+
TupleTypeSignature::try_from(vec![(
1144+
ClarityName::try_from("field-a".to_string()).unwrap(),
1145+
t,
1146+
)])
1147+
.unwrap()
1148+
.into(),
1149+
);
1150+
}
1151+
}
1152+
1153+
for t in &corpus {
1154+
assert_eq!(
1155+
TypeSignature::least_supertype_v2_1(t, t).as_ref(),
1156+
Ok(t),
1157+
"least_supertype_v2_1 must be idempotent for {t:?}"
1158+
);
1159+
}
1160+
}
1161+
1162+
fn buff_value(len: usize) -> Value {
1163+
Value::buff_from(vec![0; len]).unwrap()
1164+
}
1165+
1166+
fn ascii_value(s: &str) -> Value {
1167+
Value::string_ascii_from_bytes(s.as_bytes().to_vec()).unwrap()
1168+
}
1169+
1170+
fn callable_value(contract: &str) -> Value {
1171+
Value::CallableContract(CallableData {
1172+
contract_identifier: QualifiedContractIdentifier::local(contract).unwrap(),
1173+
trait_identifier: None,
1174+
})
1175+
}
1176+
1177+
fn tuple_value(v: Value) -> Value {
1178+
Value::from(
1179+
TupleData::from_data(vec![(
1180+
ClarityName::try_from("field-a".to_string()).unwrap(),
1181+
v,
1182+
)])
1183+
.unwrap(),
1184+
)
1185+
}
1186+
1187+
/// `construct_parent_list_type` and `parent_list_type` must stay equivalent
1188+
/// for values satisfying constructor invariants, including identical errors.
1189+
#[rstest]
1190+
#[case::empty(vec![])]
1191+
#[case::homogeneous_bools(vec![Value::Bool(true), Value::Bool(false), Value::Bool(true)])]
1192+
#[case::homogeneous_ints(vec![Value::Int(1), Value::Int(2)])]
1193+
#[case::int_uint_mismatch(vec![Value::Int(1), Value::UInt(2)])]
1194+
#[case::bool_int_mismatch(vec![Value::Bool(true), Value::Int(1)])]
1195+
#[case::buffers_different_lengths(vec![buff_value(3), buff_value(5)])]
1196+
#[case::ascii_different_lengths(vec![ascii_value("abc"), ascii_value("defgh")])]
1197+
#[case::identical_callables(vec![callable_value("contract-a"), callable_value("contract-a")])]
1198+
#[case::distinct_callables(vec![callable_value("contract-a"), callable_value("contract-b")])]
1199+
#[case::callable_with_standard_principal(vec![
1200+
callable_value("contract-a"),
1201+
Value::from(StandardPrincipalData::transient()),
1202+
])]
1203+
#[case::optional_none_and_some(vec![Value::none(), Value::some(Value::Int(5)).unwrap()])]
1204+
#[case::response_ok_and_err(vec![
1205+
Value::okay(Value::Int(1)).unwrap(),
1206+
Value::error(Value::Bool(false)).unwrap(),
1207+
])]
1208+
#[case::empty_and_nonempty_lists(vec![
1209+
Value::cons_list_unsanitized(vec![]).unwrap(),
1210+
Value::cons_list_unsanitized(vec![Value::Int(3)]).unwrap(),
1211+
])]
1212+
#[case::tuple_field_supertype(vec![tuple_value(buff_value(3)), tuple_value(buff_value(5))])]
1213+
fn test_construct_parent_list_type_matches_parent_list_type(#[case] values: Vec<Value>) {
1214+
let streamed = TypeSignature::construct_parent_list_type(&values);
1215+
let children: Vec<_> = values
1216+
.iter()
1217+
.map(|v| TypeSignature::type_of(v).unwrap())
1218+
.collect();
1219+
let collected = TypeSignature::parent_list_type(&children);
1220+
assert_eq!(
1221+
streamed, collected,
1222+
"construct_parent_list_type diverged from parent_list_type for {values:?}"
1223+
);
1224+
}

0 commit comments

Comments
 (0)