Skip to content

Commit c93f6a7

Browse files
committed
chore: improve snapshot read/write entries performance
1 parent 8a60e01 commit c93f6a7

File tree

3 files changed

+190
-50
lines changed

3 files changed

+190
-50
lines changed

soroban-ledger-snapshot/src/lib.rs

Lines changed: 96 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use serde::Deserialize;
2-
use serde_with::{serde_as, DeserializeAs, SerializeAs};
1+
use serde_with::serde_as;
32
use std::{
3+
collections::{hash_map::Entry, HashMap},
44
fs::{create_dir_all, File},
55
io::{self, BufReader, Read, Write},
66
path::Path,
@@ -39,13 +39,11 @@ pub struct LedgerSnapshot {
3939
pub min_persistent_entry_ttl: u32,
4040
pub min_temp_entry_ttl: u32,
4141
pub max_entry_ttl: u32,
42-
#[serde_as(as = "LedgerEntryVec")]
43-
pub ledger_entries: Vec<(Box<LedgerKey>, (Box<LedgerEntry>, Option<u32>))>,
42+
ledger_entries: LedgerEntries,
4443
}
4544

4645
/// Extended ledger entry that includes the live util ledger sequence. Provides a more compact
47-
/// form of the tuple used in [`LedgerSnapshot::ledger_entries`], to reduce the size of the snapshot
48-
/// when serialized to JSON.
46+
/// form of the entry storage, to reduce the size of the snapshot when serialized to JSON.
4947
#[derive(Debug, Clone, serde::Deserialize)]
5048
struct LedgerEntryExt {
5149
entry: Box<LedgerEntry>,
@@ -60,19 +58,72 @@ struct LedgerEntryExtRef<'a> {
6058
live_until: Option<u32>,
6159
}
6260

63-
struct LedgerEntryVec;
61+
/// Storage for ledger entries. Uses a [`HashMap`] for O(1) keyed
62+
/// read/write access and a [`Vec`] of keys to preserve insertion order for serialization.
63+
/// Removed keys are left as tombstones in `keys` and filtered out during iteration.
64+
#[derive(Clone, Debug, Default, Eq)]
65+
pub struct LedgerEntries {
66+
map: HashMap<Box<LedgerKey>, (Box<LedgerEntry>, Option<u32>)>,
67+
keys: Vec<Box<LedgerKey>>,
68+
}
69+
70+
impl PartialEq for LedgerEntries {
71+
fn eq(&self, other: &Self) -> bool {
72+
self.map == other.map
73+
&& self
74+
.keys
75+
.iter()
76+
.filter(|k| self.map.contains_key(k.as_ref()))
77+
.eq(other
78+
.keys
79+
.iter()
80+
.filter(|k| other.map.contains_key(k.as_ref())))
81+
}
82+
}
83+
84+
impl LedgerEntries {
85+
/// Get the entry for a given key, if it exists.
86+
fn get(&self, key: &LedgerKey) -> Option<&(Box<LedgerEntry>, Option<u32>)> {
87+
self.map.get(key)
88+
}
89+
90+
/// Insert or replace the entry for a given key.
91+
fn insert(&mut self, key: Box<LedgerKey>, value: (Box<LedgerEntry>, Option<u32>)) {
92+
match self.map.entry(key) {
93+
Entry::Occupied(mut e) => {
94+
e.insert(value);
95+
}
96+
Entry::Vacant(e) => {
97+
self.keys.push(e.key().clone());
98+
e.insert(value);
99+
}
100+
}
101+
}
102+
103+
/// Remove the entry for a given key, if it exists.
104+
fn remove(&mut self, key: &LedgerKey) {
105+
self.map.remove(key);
106+
}
64107

65-
impl<'a> SerializeAs<Vec<(Box<LedgerKey>, (Box<LedgerEntry>, Option<u32>))>> for LedgerEntryVec {
66-
fn serialize_as<S>(
67-
source: &Vec<(Box<LedgerKey>, (Box<LedgerEntry>, Option<u32>))>,
68-
serializer: S,
69-
) -> Result<S::Ok, S::Error>
70-
where
71-
S: serde::Serializer,
72-
{
108+
/// Iterate over the entries in insertion order
109+
fn iter(&self) -> impl Iterator<Item = (&Box<LedgerKey>, &(Box<LedgerEntry>, Option<u32>))> {
110+
self.keys
111+
.iter()
112+
.filter_map(|k| self.map.get(k).map(|v| (k, v)))
113+
}
114+
115+
/// Clear all entries from the storage.
116+
fn clear(&mut self) {
117+
self.keys.clear();
118+
self.map.clear();
119+
}
120+
}
121+
122+
impl serde::Serialize for LedgerEntries {
123+
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
73124
use serde::ser::SerializeSeq;
74-
let mut seq = serializer.serialize_seq(Some(source.len()))?;
75-
for (_, (entry, live_until)) in source {
125+
let mut seq = serializer.serialize_seq(Some(self.map.len()))?;
126+
for (_, (entry, live_until)) in self.iter() {
76127
seq.serialize_element(&LedgerEntryExtRef {
77128
entry,
78129
live_until: *live_until,
@@ -82,32 +133,29 @@ impl<'a> SerializeAs<Vec<(Box<LedgerKey>, (Box<LedgerEntry>, Option<u32>))>> for
82133
}
83134
}
84135

85-
impl<'de> DeserializeAs<'de, Vec<(Box<LedgerKey>, (Box<LedgerEntry>, Option<u32>))>>
86-
for LedgerEntryVec
87-
{
88-
fn deserialize_as<D>(
89-
deserializer: D,
90-
) -> Result<Vec<(Box<LedgerKey>, (Box<LedgerEntry>, Option<u32>))>, D::Error>
91-
where
92-
D: serde::Deserializer<'de>,
93-
{
136+
impl<'de> serde::Deserialize<'de> for LedgerEntries {
137+
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
94138
#[derive(serde::Deserialize)]
95139
#[serde(untagged)]
96140
enum Format {
97141
V2(Vec<LedgerEntryExt>),
98142
V1(Vec<(Box<LedgerKey>, (Box<LedgerEntry>, Option<u32>))>),
99143
}
100-
144+
let mut entries = LedgerEntries::default();
101145
match Format::deserialize(deserializer)? {
102-
Format::V2(entries) => Ok(entries
103-
.into_iter()
104-
.map(|LedgerEntryExt { entry, live_until }| {
146+
Format::V2(raw) => {
147+
for LedgerEntryExt { entry, live_until } in raw {
105148
let key = Box::new(entry.to_key());
106-
(key, (entry, live_until))
107-
})
108-
.collect()),
109-
Format::V1(entries) => Ok(entries),
149+
entries.insert(key, (entry, live_until));
150+
}
151+
}
152+
Format::V1(raw) => {
153+
for (key, value) in raw {
154+
entries.insert(key, value);
155+
}
156+
}
110157
}
158+
Ok(entries)
111159
}
112160
}
113161

@@ -164,11 +212,16 @@ impl LedgerSnapshot {
164212
self.max_entry_ttl = info.max_entry_ttl;
165213
}
166214

167-
/// Get the entries in the snapshot.
215+
/// Count the number of entries in the snapshot.
216+
pub fn count_entries(&self) -> usize {
217+
self.ledger_entries.map.len()
218+
}
219+
220+
/// Iterate over all the entries in the snapshot, in insertion order.
168221
pub fn entries(
169222
&self,
170223
) -> impl IntoIterator<Item = (&Box<LedgerKey>, &(Box<LedgerEntry>, Option<u32>))> {
171-
self.ledger_entries.iter().map(|(k, v)| (k, v))
224+
self.ledger_entries.iter()
172225
}
173226

174227
/// Replace the entries in the snapshot with the entries in the iterator.
@@ -178,7 +231,7 @@ impl LedgerSnapshot {
178231
) {
179232
self.ledger_entries.clear();
180233
for (k, e) in entries {
181-
self.ledger_entries.push((k.clone(), (e.0.clone(), e.1)));
234+
self.ledger_entries.insert(k.clone(), (e.0.clone(), e.1));
182235
}
183236
}
184237

@@ -190,19 +243,13 @@ impl LedgerSnapshot {
190243
entries: impl IntoIterator<Item = &'a (Rc<LedgerKey>, Option<(Rc<LedgerEntry>, Option<u32>)>)>,
191244
) {
192245
for (k, e) in entries {
193-
let i = self.ledger_entries.iter().position(|(ik, _)| **ik == **k);
194246
if let Some((entry, live_until_ledger)) = e {
195-
let new = (
247+
self.ledger_entries.insert(
196248
Box::new((**k).clone()),
197249
(Box::new((**entry).clone()), *live_until_ledger),
198250
);
199-
if let Some(i) = i {
200-
self.ledger_entries[i] = new;
201-
} else {
202-
self.ledger_entries.push(new);
203-
}
204-
} else if let Some(i) = i {
205-
self.ledger_entries.swap_remove(i);
251+
} else {
252+
self.ledger_entries.remove(k);
206253
}
207254
}
208255
}
@@ -245,7 +292,7 @@ impl Default for LedgerSnapshot {
245292
timestamp: Default::default(),
246293
network_id: Default::default(),
247294
base_reserve: Default::default(),
248-
ledger_entries: Vec::default(),
295+
ledger_entries: LedgerEntries::default(),
249296
min_persistent_entry_ttl: Default::default(),
250297
min_temp_entry_ttl: Default::default(),
251298
max_entry_ttl: Default::default(),
@@ -258,8 +305,8 @@ impl SnapshotSource for &LedgerSnapshot {
258305
&self,
259306
key: &Rc<LedgerKey>,
260307
) -> Result<Option<(Rc<LedgerEntry>, Option<u32>)>, HostError> {
261-
match self.ledger_entries.iter().find(|(k, _)| **k == **key) {
262-
Some((_, v)) => Ok(Some((Rc::new(*v.0.clone()), v.1))),
308+
match self.ledger_entries.get(key) {
309+
Some(v) => Ok(Some((Rc::new(*v.0.clone()), v.1))),
263310
None => Ok(None),
264311
}
265312
}

soroban-ledger-snapshot/src/tests.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use soroban_env_host::{
22
storage::SnapshotSource,
33
xdr::{LedgerKey, Limits, ReadXdr},
4+
LedgerInfo,
45
};
56

67
use crate::LedgerSnapshot;
@@ -55,3 +56,95 @@ fn test_snapshot_roundtrip() {
5556
let written_normalized = written_str.replace("\r\n", "\n");
5657
assert_eq!(written_normalized, expected_normalized);
5758
}
59+
60+
#[test]
61+
fn test_ledger_info() {
62+
let mut snapshot = LedgerSnapshot::read_file("./test_data/snapshot_v2.json").unwrap();
63+
let info = LedgerInfo {
64+
protocol_version: 25,
65+
sequence_number: 1234567,
66+
timestamp: 1000000000,
67+
network_id: [1u8; 32],
68+
base_reserve: 123,
69+
min_persistent_entry_ttl: 42,
70+
min_temp_entry_ttl: 42,
71+
max_entry_ttl: 99999,
72+
};
73+
snapshot.set_ledger_info(info.clone());
74+
let got = snapshot.ledger_info();
75+
assert_eq!(got.protocol_version, info.protocol_version);
76+
assert_eq!(got.sequence_number, info.sequence_number);
77+
assert_eq!(got.timestamp, info.timestamp);
78+
assert_eq!(got.network_id, info.network_id);
79+
assert_eq!(got.base_reserve, info.base_reserve);
80+
assert_eq!(got.min_persistent_entry_ttl, info.min_persistent_entry_ttl);
81+
assert_eq!(got.min_temp_entry_ttl, info.min_temp_entry_ttl);
82+
assert_eq!(got.max_entry_ttl, info.max_entry_ttl);
83+
}
84+
85+
#[test]
86+
fn test_set_and_update_entries() {
87+
let base = LedgerSnapshot::read_file("./test_data/snapshot_v2.json").unwrap();
88+
let entries: Vec<_> = base.entries().into_iter().collect();
89+
90+
// Set entries to only the first two entries
91+
let mut snapshot = LedgerSnapshot::default();
92+
snapshot.set_entries(entries[..2].iter().map(|(k, v)| (*k, (&v.0, v.1))));
93+
assert_eq!(snapshot.count_entries(), 2);
94+
95+
// Upsert: update first entry and add third entry
96+
let updates: Vec<(
97+
Rc<LedgerKey>,
98+
Option<(Rc<soroban_env_host::xdr::LedgerEntry>, Option<u32>)>,
99+
)> = vec![
100+
// Update existing with new live_until
101+
(
102+
Rc::new((**entries[0].0).clone()),
103+
Some((Rc::new((*entries[0].1 .0).clone()), Some(99))),
104+
),
105+
// Add new entry
106+
(
107+
Rc::new((**entries[2].0).clone()),
108+
Some((Rc::new((*entries[2].1 .0).clone()), entries[2].1 .1)),
109+
),
110+
];
111+
snapshot.update_entries(&updates);
112+
assert_eq!(snapshot.count_entries(), 3);
113+
114+
let key0 = Rc::new((**entries[0].0).clone());
115+
let key2 = Rc::new((**entries[2].0).clone());
116+
assert_eq!(snapshot.get(&key0).unwrap().unwrap().1, Some(99));
117+
assert!(snapshot.get(&key2).unwrap().is_some());
118+
}
119+
120+
#[test]
121+
fn test_update_remove_then_serialize() {
122+
let mut snapshot = LedgerSnapshot::read_file("./test_data/snapshot_v2.json").unwrap();
123+
let ledger_key = LedgerKey::from_xdr_base64(TEST_SNAPSHOT_XDR[3].0, Limits::none()).unwrap();
124+
let ledger_key_rc = Rc::new(ledger_key.clone());
125+
126+
let entry = snapshot.get(&ledger_key_rc).unwrap();
127+
assert!(entry.is_some());
128+
assert_eq!(snapshot.count_entries(), 4);
129+
130+
// Remove via update_entries with None value
131+
let updates: Vec<(
132+
Rc<LedgerKey>,
133+
Option<(Rc<soroban_env_host::xdr::LedgerEntry>, Option<u32>)>,
134+
)> = vec![(ledger_key_rc.clone(), None)];
135+
snapshot.update_entries(&updates);
136+
137+
let entry = snapshot.get(&ledger_key_rc).unwrap();
138+
assert!(entry.is_none());
139+
assert_eq!(snapshot.count_entries(), 3);
140+
141+
// Write and read back — tombstone should not appear
142+
let mut buf = Vec::new();
143+
snapshot.write(&mut buf).unwrap();
144+
let restored = LedgerSnapshot::read(&buf[..]).unwrap();
145+
146+
let entry = restored.get(&ledger_key_rc).unwrap();
147+
assert!(entry.is_none());
148+
assert_eq!(restored.count_entries(), 3);
149+
assert_eq!(snapshot, restored);
150+
}

soroban-sdk/src/env.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1838,7 +1838,7 @@ impl Env {
18381838
let snapshot = self.to_snapshot();
18391839

18401840
// Don't write a snapshot that has no data in it.
1841-
if snapshot.ledger.entries().into_iter().count() == 0
1841+
if snapshot.ledger.count_entries() == 0
18421842
&& snapshot.events.0.is_empty()
18431843
&& snapshot.auth.0.is_empty()
18441844
{

0 commit comments

Comments
 (0)