Skip to content

Commit 3fb36ab

Browse files
fix: fragment large allocations (#534)
this allows to efficiently reuse large free allocations, resulting from realloc/remove of large accounts, without moving accountsdb storage pointer to the end of the file <!-- greptile_comment --> ## Greptile Summary Updated On: 2025-09-09 14:49:25 UTC This PR implements allocation splitting functionality for the accounts database to improve memory efficiency. The changes allow large freed allocations (from removed or reallocated accounts) to be split into smaller chunks that can be reused by subsequent accounts, rather than requiring the entire large allocation to be consumed by a single new account. The core implementation is in the `try_recycle_allocation` method in `magicblock-accounts-db/src/index.rs`, which now checks if a recycled allocation is larger than needed. When this occurs, it splits the allocation - using exactly the requested amount and returning the remainder to the deallocations index for future reuse. This is achieved by calculating the remainder blocks, updating the offset for the unused portion, and reinserting it into the recycleable allocations pool. The changes include comprehensive test coverage with two new test functions: `test_recycle_allocation_split` in the index tests and `test_reallocation_split` in the main tests. These tests verify that large allocations can be properly split and that memory pointers are correctly managed during the splitting process. Helper methods have been refactored to support creating accounts with custom sizes for testing purposes. This optimization addresses a significant inefficiency in the accounts database where large freed allocations could only be reused by accounts of equal or larger size, leading to memory fragmentation and unnecessary growth of the storage file. The splitting functionality integrates seamlessly with the existing allocation recycling system while maintaining proper offset calculations and endianness handling. ## Confidence score: 4/5 - This PR appears safe to merge with careful attention to the memory management logic - Score reflects solid implementation with good test coverage, but complexity in allocation splitting warrants review - Pay close attention to the allocation splitting logic in `magicblock-accounts-db/src/index.rs` lines 372-383 <!-- /greptile_comment --> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent f1600db commit 3fb36ab

File tree

4 files changed

+101
-31
lines changed

4 files changed

+101
-31
lines changed

magicblock-accounts-db/src/index.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,8 @@ impl AccountsDbIndex {
250250
Ok(())
251251
}
252252

253-
/// Ensures that current owner of account matches the one recorded in index
254-
/// if not, the index cleanup will be performed and new entries inserted to
253+
/// Ensures that current owner of account matches the one recorded in index,
254+
/// if not, the index cleanup will be performed and new entry inserted to
255255
/// match the current state
256256
pub(crate) fn ensure_correct_owner(
257257
&self,
@@ -355,7 +355,7 @@ impl AccountsDbIndex {
355355

356356
/// Check whether allocation of given size (in blocks) exists.
357357
/// These are the allocations which are leftovers from
358-
/// accounts' reallocations due to their resizing
358+
/// accounts' reallocations due to their resizing/removal
359359
pub(crate) fn try_recycle_allocation(
360360
&self,
361361
space: u32,
@@ -369,9 +369,18 @@ impl AccountsDbIndex {
369369
let (_, val) =
370370
cursor.get(Some(&space.to_le_bytes()), None, MDB_SET_RANGE_OP)?;
371371

372-
let (offset, blocks) = bytes!(#unpack, val, u32, u32);
372+
let (offset, mut blocks) = bytes!(#unpack, val, u32, u32);
373373
// delete the allocation record from recycleable list
374374
cursor.del(WEMPTY)?;
375+
// check whether the found allocation contains more space than necessary
376+
let remainder = blocks.saturating_sub(space);
377+
if remainder > 0 {
378+
// split the allocation, to maximize the efficiency of block reuse
379+
blocks = space;
380+
let new_offset = offset.saturating_add(blocks);
381+
let index_value = bytes!(#pack, new_offset, u32, remainder, u32);
382+
cursor.put(&remainder.to_le_bytes(), &index_value, WEMPTY)?;
383+
}
375384

376385
drop(cursor);
377386
txn.commit()?;

magicblock-accounts-db/src/index/tests.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ fn test_recycle_allocation_after_realloc() {
256256
assert_eq!(
257257
tenv.get_delloactions_count(),
258258
0,
259-
"the number of deallocations should have decresed after recycling"
259+
"the number of deallocations should have decreased after recycling"
260260
);
261261
let result = tenv.try_recycle_allocation(new_allocation.blocks);
262262
assert!(
@@ -278,7 +278,48 @@ fn test_recycle_allocation_after_realloc() {
278278
assert_eq!(
279279
tenv.get_delloactions_count(),
280280
0,
281-
"the number of deallocations should have decresed after recycling"
281+
"the number of deallocations should have decreased after recycling"
282+
);
283+
}
284+
285+
#[test]
286+
fn test_recycle_allocation_split() {
287+
let tenv = setup();
288+
let IndexAccount {
289+
pubkey,
290+
owner,
291+
allocation,
292+
} = tenv.account();
293+
294+
tenv.insert_account(&pubkey, &owner, allocation)
295+
.expect("failed to insert account");
296+
tenv.remove_account(&pubkey).unwrap();
297+
assert_eq!(
298+
tenv.get_delloactions_count(),
299+
1,
300+
"the number of deallocations should have increased after account removal"
301+
);
302+
303+
let result = tenv
304+
.try_recycle_allocation(allocation.blocks / 2)
305+
.expect("failed to recycle allocation");
306+
assert_eq!(result.blocks, allocation.blocks / 2);
307+
assert_eq!(result.offset, allocation.offset);
308+
let result = tenv
309+
.try_recycle_allocation(allocation.blocks / 2)
310+
.expect("failed to recycle allocation");
311+
assert_eq!(result.blocks, allocation.blocks / 2);
312+
assert!(result.offset > allocation.offset);
313+
314+
assert_eq!(
315+
tenv.get_delloactions_count(),
316+
0,
317+
"the number of deallocations should have decreased after recycling"
318+
);
319+
let result = tenv.try_recycle_allocation(allocation.blocks);
320+
assert!(
321+
matches!(result, Err(AccountsDbError::NotFound)),
322+
"deallocations index should have run out of existing allocations"
282323
);
283324
}
284325

magicblock-accounts-db/src/tests.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,23 @@ fn test_many_insertions_to_accountsdb() {
541541
});
542542
}
543543

544+
#[test]
545+
fn test_reallocation_split() {
546+
let tenv = init_test_env();
547+
const SIZE: usize = 1024;
548+
tenv.account();
549+
let account1 = tenv.account_with_size(SIZE * 2);
550+
let data_ptr1 = account1.account.data().as_ptr();
551+
let account2 = tenv.account_with_size(SIZE);
552+
let data_ptr2 = account2.account.data().as_ptr();
553+
tenv.remove_account(&account1.pubkey);
554+
let account3 = tenv.account_with_size(SIZE / 4);
555+
let account4 = tenv.account_with_size(SIZE / 4);
556+
assert_eq!(account3.account.data().as_ptr(), data_ptr1);
557+
assert!(account4.account.data().as_ptr() < data_ptr2);
558+
assert!(account4.account.data().as_ptr() > data_ptr1);
559+
}
560+
544561
// ==============================================================
545562
// ==============================================================
546563
// UTILITY CODE BELOW
@@ -580,8 +597,11 @@ fn init_test_env() -> AdbTestEnv {
580597

581598
impl AdbTestEnv {
582599
fn account(&self) -> AccountWithPubkey {
600+
Self::account_with_size(self, SPACE)
601+
}
602+
fn account_with_size(&self, size: usize) -> AccountWithPubkey {
583603
let pubkey = Pubkey::new_unique();
584-
let mut account = AccountSharedData::new(LAMPORTS, SPACE, &OWNER);
604+
let mut account = AccountSharedData::new(LAMPORTS, size, &OWNER);
585605
account.data_as_mut_slice()[..INIT_DATA_LEN]
586606
.copy_from_slice(ACCOUNT_DATA);
587607
self.adb.insert_account(&pubkey, &account);

magicblock-api/src/fund_account.rs

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ use magicblock_magic_program_api::{
55
self, MAGIC_CONTEXT_PUBKEY, MAGIC_CONTEXT_SIZE,
66
};
77
use solana_sdk::{
8-
account::Account, clock::Epoch, pubkey::Pubkey, signature::Keypair,
9-
signer::Signer, system_program,
8+
account::{Account, WritableAccount},
9+
clock::Epoch,
10+
pubkey::Pubkey,
11+
signature::Keypair,
12+
signer::Signer,
13+
system_program,
1014
};
1115

1216
use crate::{
@@ -15,17 +19,7 @@ use crate::{
1519
};
1620

1721
pub(crate) fn fund_account(bank: &Bank, pubkey: &Pubkey, lamports: u64) {
18-
bank.store_account(
19-
*pubkey,
20-
Account {
21-
lamports,
22-
data: vec![],
23-
owner: system_program::id(),
24-
executable: false,
25-
rent_epoch: Epoch::MAX,
26-
}
27-
.into(),
28-
);
22+
fund_account_with_data(bank, pubkey, lamports, vec![]);
2923
}
3024

3125
pub(crate) fn fund_account_with_data(
@@ -34,17 +28,23 @@ pub(crate) fn fund_account_with_data(
3428
lamports: u64,
3529
data: Vec<u8>,
3630
) {
37-
bank.store_account(
38-
*pubkey,
39-
Account {
40-
lamports,
41-
data,
42-
owner: system_program::id(),
43-
executable: false,
44-
rent_epoch: Epoch::MAX,
45-
}
46-
.into(),
47-
);
31+
if let Some(mut acc) = bank.get_account(pubkey) {
32+
acc.set_lamports(lamports);
33+
acc.set_data(data);
34+
bank.store_account(*pubkey, acc);
35+
} else {
36+
bank.store_account(
37+
*pubkey,
38+
Account {
39+
lamports,
40+
data,
41+
owner: system_program::id(),
42+
executable: false,
43+
rent_epoch: Epoch::MAX,
44+
}
45+
.into(),
46+
);
47+
}
4848
}
4949

5050
pub(crate) fn fund_validator_identity(bank: &Bank, validator_id: &Pubkey) {

0 commit comments

Comments
 (0)