Skip to content

Commit e8d0393

Browse files
authored
feat: Optimize CommitDiff using efficient merge_diff_copy that avoids heap allocation (#113)
1 parent 97174ec commit e8d0393

File tree

5 files changed

+87
-20
lines changed

5 files changed

+87
-20
lines changed

src/diff/algorithm.rs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ use std::cmp::{min, Ordering};
33
use pinocchio::program_error::ProgramError;
44
use rkyv::util::AlignedVec;
55

6+
use crate::error::DlpError;
7+
68
use super::{
7-
DiffSet, SizeChanged, SIZE_OF_CHANGED_LEN, SIZE_OF_NUM_OFFSET_PAIRS, SIZE_OF_SINGLE_OFFSET_PAIR,
9+
DiffSet, OffsetInData, SizeChanged, SIZE_OF_CHANGED_LEN, SIZE_OF_NUM_OFFSET_PAIRS,
10+
SIZE_OF_SINGLE_OFFSET_PAIR,
811
};
912

1013
///
@@ -229,6 +232,35 @@ pub fn apply_diff_copy(original: &[u8], diffset: &DiffSet<'_>) -> Result<Vec<u8>
229232
})
230233
}
231234

235+
/// This function constructs destination by merging original with diff such that destination
236+
/// becomes the changed version of the original.
237+
///
238+
/// Precondition:
239+
/// - destination.len() == original.len()
240+
pub fn merge_diff_copy(
241+
destination: &mut [u8],
242+
original: &[u8],
243+
diffset: &DiffSet<'_>,
244+
) -> Result<(), ProgramError> {
245+
if destination.len() != original.len() {
246+
return Err(DlpError::MergeDiffError.into());
247+
}
248+
let mut write_index = 0;
249+
for item in diffset.iter() {
250+
let (diff_segment, OffsetInData { start, end }) = item?;
251+
if write_index < start {
252+
// copy the unchanged bytes
253+
destination[write_index..start].copy_from_slice(&original[write_index..start]);
254+
}
255+
destination[start..end].copy_from_slice(diff_segment);
256+
write_index = end;
257+
}
258+
if write_index < original.len() {
259+
destination[write_index..].copy_from_slice(&original[write_index..]);
260+
}
261+
Ok(())
262+
}
263+
232264
// private function that does the actual work.
233265
fn apply_diff_impl(original: &mut [u8], diffset: &DiffSet<'_>) -> Result<(), ProgramError> {
234266
for item in diffset.iter() {
@@ -247,7 +279,7 @@ mod tests {
247279
Rng, RngCore, SeedableRng,
248280
};
249281

250-
use crate::{apply_diff_copy, apply_diff_in_place, compute_diff, DiffSet};
282+
use crate::{apply_diff_copy, apply_diff_in_place, compute_diff, merge_diff_copy, DiffSet};
251283

252284
#[test]
253285
fn test_no_change() {
@@ -311,6 +343,14 @@ mod tests {
311343
let expected_changed = apply_diff_copy(&original, &actual_diffset).unwrap();
312344

313345
assert_eq!(changed.as_slice(), expected_changed.as_slice());
346+
347+
let expected_changed = {
348+
let mut destination = vec![255; original.len()];
349+
merge_diff_copy(&mut destination, &original, &actual_diffset).unwrap();
350+
destination
351+
};
352+
353+
assert_eq!(changed.as_slice(), expected_changed.as_slice());
314354
}
315355

316356
#[test]
@@ -394,11 +434,19 @@ mod tests {
394434

395435
// apply diff back to verify correctness
396436
let expected_changed = {
397-
let mut copy = original;
437+
let mut copy = original.clone();
398438
apply_diff_in_place(&mut copy, &actual_diffset).unwrap();
399439
copy
400440
};
401441

402442
assert_eq!(changed, expected_changed);
443+
444+
let expected_changed = {
445+
let mut destination = vec![255; original.len()];
446+
merge_diff_copy(&mut destination, &original, &actual_diffset).unwrap();
447+
destination
448+
};
449+
450+
assert_eq!(changed, expected_changed);
403451
}
404452
}

src/error.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ pub enum DlpError {
3535
Overflow = 13,
3636
#[error("Too many seeds")]
3737
TooManySeeds = 14,
38-
#[error("Invalid diff passed to DiffSet::try_new")]
38+
#[error("Invalid length of diff passed to DiffSet::try_new")]
3939
InvalidDiff = 15,
4040
#[error("Diff is not properly aligned")]
4141
InvalidDiffAlignment = 16,
42+
#[error("MergeDiff precondition did not meet")]
43+
MergeDiffError = 17,
4244
}
4345

4446
impl From<DlpError> for ProgramError {

src/processor/fast/commit_diff.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use pinocchio_log::log;
66

77
use crate::args::{CommitDiffArgsWithoutDiff, SIZE_COMMIT_DIFF_ARGS_WITHOUT_DIFF};
88
use crate::processor::fast::{process_commit_state_internal, CommitStateInternalArgs};
9-
use crate::{apply_diff_copy, DiffSet};
9+
use crate::DiffSet;
10+
11+
use super::NewState;
1012

1113
/// Commit diff to a delegated PDA
1214
///
@@ -70,14 +72,8 @@ pub fn process_commit_diff(
7072
let commit_record_nonce = args.nonce;
7173
let allow_undelegation = args.allow_undelegation;
7274

73-
// TODO (snawaz): the following approach to apply diff works, but it's not efficient.
74-
// It is also problematic for larger account as it allocates memory on the heap.
75-
// It will be fixed in a separate PR.
76-
let original = unsafe { delegated_account.borrow_data_unchecked() };
77-
let changed = apply_diff_copy(original, &diffset)?;
78-
7975
let commit_args = CommitStateInternalArgs {
80-
commit_state_bytes: &changed,
76+
commit_state_bytes: NewState::Diff(diffset),
8177
commit_record_lamports,
8278
commit_record_nonce,
8379
allow_undelegation,

src/processor/fast/commit_state.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use pinocchio_system::instructions as system;
1010

1111
use crate::args::CommitStateArgs;
1212
use crate::error::DlpError;
13-
use crate::pda;
1413
use crate::processor::fast::utils::{
1514
pda::create_pda,
1615
requires::{
@@ -20,6 +19,7 @@ use crate::processor::fast::utils::{
2019
},
2120
};
2221
use crate::state::{CommitRecord, DelegationMetadata, DelegationRecord, ProgramConfig};
22+
use crate::{merge_diff_copy, pda, DiffSet};
2323

2424
use super::to_pinocchio_program_error;
2525

@@ -59,7 +59,6 @@ pub fn process_commit_state(
5959
) -> ProgramResult {
6060
let args = CommitStateArgs::try_from_slice(data).map_err(|_| ProgramError::BorshIoError)?;
6161

62-
let commit_state_bytes: &[u8] = args.data.as_ref();
6362
let commit_record_lamports = args.lamports;
6463
let commit_record_nonce = args.nonce;
6564
let allow_undelegation = args.allow_undelegation;
@@ -71,7 +70,7 @@ pub fn process_commit_state(
7170
};
7271

7372
let commit_args = CommitStateInternalArgs {
74-
commit_state_bytes,
73+
commit_state_bytes: NewState::FullBytes(&args.data),
7574
commit_record_lamports,
7675
commit_record_nonce,
7776
allow_undelegation,
@@ -88,9 +87,23 @@ pub fn process_commit_state(
8887
process_commit_state_internal(commit_args)
8988
}
9089

90+
pub(crate) enum NewState<'a> {
91+
FullBytes(&'a [u8]),
92+
Diff(DiffSet<'a>),
93+
}
94+
95+
impl NewState<'_> {
96+
pub fn data_len(&self) -> usize {
97+
match self {
98+
NewState::FullBytes(bytes) => bytes.len(),
99+
NewState::Diff(diff) => diff.changed_len(),
100+
}
101+
}
102+
}
103+
91104
/// Arguments for the commit state internal function
92105
pub(crate) struct CommitStateInternalArgs<'a> {
93-
pub(crate) commit_state_bytes: &'a [u8],
106+
pub(crate) commit_state_bytes: NewState<'a>,
94107
pub(crate) commit_record_lamports: u64,
95108
pub(crate) commit_record_nonce: u64,
96109
pub(crate) allow_undelegation: bool,
@@ -238,7 +251,7 @@ pub(crate) fn process_commit_state_internal(
238251
create_pda(
239252
args.commit_state_account,
240253
&crate::fast::ID,
241-
args.commit_state_bytes.len(),
254+
args.commit_state_bytes.data_len(),
242255
&[Signer::from(&seeds!(
243256
pda::COMMIT_STATE_TAG,
244257
args.delegated_account.key(),
@@ -274,7 +287,14 @@ pub(crate) fn process_commit_state_internal(
274287

275288
// Copy the new state to the initialized PDA
276289
let mut commit_state_data = args.commit_state_account.try_borrow_mut_data()?;
277-
(*commit_state_data).copy_from_slice(args.commit_state_bytes);
290+
291+
match args.commit_state_bytes {
292+
NewState::FullBytes(bytes) => (*commit_state_data).copy_from_slice(bytes),
293+
NewState::Diff(diff) => {
294+
let original_data = args.delegated_account.try_borrow_data()?;
295+
merge_diff_copy(&mut commit_state_data, &original_data, &diff)?;
296+
}
297+
}
278298

279299
// TODO - Add additional validation for the commitment, e.g. sufficient validator stake
280300

src/processor/fast/commit_state_from_buffer.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use pinocchio::program_error::ProgramError;
77
use pinocchio::pubkey::Pubkey;
88
use pinocchio::ProgramResult;
99

10+
use super::NewState;
11+
1012
pub fn process_commit_state_from_buffer(
1113
_program_id: &Pubkey,
1214
accounts: &[AccountInfo],
@@ -26,10 +28,9 @@ pub fn process_commit_state_from_buffer(
2628
let allow_undelegation = args.allow_undelegation;
2729

2830
let state = state_buffer_account.try_borrow_data()?;
29-
let commit_state_bytes: &[u8] = &state;
3031

3132
let commit_args = CommitStateInternalArgs {
32-
commit_state_bytes,
33+
commit_state_bytes: NewState::FullBytes(&state),
3334
commit_record_lamports,
3435
commit_record_nonce,
3536
allow_undelegation,

0 commit comments

Comments
 (0)