Skip to content

Commit

Permalink
perf(vm): Improve snapshot management in batch executor (#2513)
Browse files Browse the repository at this point in the history
## What ❔

- Does not embed snapshots into each other, and instead use a linear
workflow.
- Discards an old snapshot, if any, when processing a new transaction
(currently, old snapshots are seemingly never discarded unless the
transaction is rolled back).

## Why ❔

- Linear snapshow workflow is easier to grasp and it could allow
optimizations in the future.
- Accumulating snapshots introduces an overhead, esp. in the new VM.
  • Loading branch information
slowli authored Jul 31, 2024
1 parent e4ddb30 commit 69be78c
Show file tree
Hide file tree
Showing 17 changed files with 51 additions and 63 deletions.
14 changes: 13 additions & 1 deletion core/lib/multivm/src/interface/traits/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,25 @@ pub trait VmFactory<S>: VmInterface {
}

/// Methods of VM requiring history manipulations.
///
/// # Snapshot workflow
///
/// External callers must follow the following snapshot workflow:
///
/// - Each new snapshot created using `make_snapshot()` must be either popped or rolled back before creating the following snapshot.
/// OTOH, it's not required to call either of these methods by the end of VM execution.
/// - `pop_snapshot_no_rollback()` may be called spuriously, when no snapshot was created. It is a no-op in this case.
///
/// These rules guarantee that at each given moment, a VM instance has at most one snapshot (unless the VM makes snapshots internally),
/// which may allow additional VM optimizations.
pub trait VmInterfaceHistoryEnabled: VmInterface {
/// Create a snapshot of the current VM state and push it into memory.
fn make_snapshot(&mut self);

/// Roll back VM state to the latest snapshot and destroy the snapshot.
fn rollback_to_the_latest_snapshot(&mut self);

/// Pop the latest snapshot from memory and destroy it.
/// Pop the latest snapshot from memory and destroy it. If there are no snapshots, this should be a no-op
/// (i.e., the VM must not panic in this case).
fn pop_snapshot_no_rollback(&mut self);
}
2 changes: 1 addition & 1 deletion core/lib/multivm/src/versions/vm_1_3_2/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,6 @@ impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, crate::vm_latest::Hist
}

fn pop_snapshot_no_rollback(&mut self) {
self.vm.pop_snapshot_no_rollback()
self.vm.pop_snapshot_no_rollback();
}
}
2 changes: 1 addition & 1 deletion core/lib/multivm/src/versions/vm_1_3_2/vm_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ impl<H: HistoryMode, S: WriteStorage> VmInstance<S, H> {
/// Removes the latest snapshot without rolling it back.
/// This function expects that there is at least one snapshot present.
pub fn pop_snapshot_no_rollback(&mut self) {
self.snapshots.pop().unwrap();
self.snapshots.pop();
}

/// Returns the amount of gas remaining to the VM.
Expand Down
6 changes: 1 addition & 5 deletions core/lib/multivm/src/versions/vm_1_4_1/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, crate::vm_latest::Hist
self.make_snapshot_inner()
}

/// Rollback vm state to the latest snapshot and destroy the snapshot
fn rollback_to_the_latest_snapshot(&mut self) {
let snapshot = self
.snapshots
Expand All @@ -189,10 +188,7 @@ impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, crate::vm_latest::Hist
self.rollback_to_snapshot(snapshot);
}

/// Pop the latest snapshot from the memory and destroy it
fn pop_snapshot_no_rollback(&mut self) {
self.snapshots
.pop()
.expect("Snapshot should be created before rolling it back");
self.snapshots.pop();
}
}
10 changes: 2 additions & 8 deletions core/lib/multivm/src/versions/vm_1_4_2/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,11 @@ impl<S: WriteStorage, H: HistoryMode> VmFactory<S> for Vm<S, H> {
}
}

/// Methods of vm, which required some history manipulations
impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, crate::vm_latest::HistoryEnabled> {
/// Create snapshot of current vm state and push it into the memory
fn make_snapshot(&mut self) {
self.make_snapshot_inner()
self.make_snapshot_inner();
}

/// Rollback vm state to the latest snapshot and destroy the snapshot
fn rollback_to_the_latest_snapshot(&mut self) {
let snapshot = self
.snapshots
Expand All @@ -194,10 +191,7 @@ impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, crate::vm_latest::Hist
self.rollback_to_snapshot(snapshot);
}

/// Pop the latest snapshot from the memory and destroy it
fn pop_snapshot_no_rollback(&mut self) {
self.snapshots
.pop()
.expect("Snapshot should be created before rolling it back");
self.snapshots.pop();
}
}
10 changes: 2 additions & 8 deletions core/lib/multivm/src/versions/vm_boojum_integration/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,11 @@ impl<S: WriteStorage, H: HistoryMode> VmFactory<S> for Vm<S, H> {
}
}

/// Methods of vm, which required some history manipulations
impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, crate::vm_latest::HistoryEnabled> {
/// Create snapshot of current vm state and push it into the memory
fn make_snapshot(&mut self) {
self.make_snapshot_inner()
self.make_snapshot_inner();
}

/// Rollback vm state to the latest snapshot and destroy the snapshot
fn rollback_to_the_latest_snapshot(&mut self) {
let snapshot = self
.snapshots
Expand All @@ -189,10 +186,7 @@ impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, crate::vm_latest::Hist
self.rollback_to_snapshot(snapshot);
}

/// Pop the latest snapshot from the memory and destroy it
fn pop_snapshot_no_rollback(&mut self) {
self.snapshots
.pop()
.expect("Snapshot should be created before rolling it back");
self.snapshots.pop();
}
}
2 changes: 2 additions & 0 deletions core/lib/multivm/src/versions/vm_fast/tests/rollbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{
},
};

#[ignore] // FIXME(PLA-1002): VM state assertion in `execute_tx_and_verify` fails
#[test]
fn test_vm_rollbacks() {
let mut vm = VmTesterBuilder::new()
Expand Down Expand Up @@ -59,6 +60,7 @@ fn test_vm_rollbacks() {
assert_eq!(result_without_rollbacks, result_with_rollbacks);
}

#[ignore] // FIXME(PLA-1002): VM state assertion in `execute_tx_and_verify` fails
#[test]
fn test_vm_loadnext_rollbacks() {
let mut vm = VmTesterBuilder::new()
Expand Down
1 change: 1 addition & 0 deletions core/lib/multivm/src/versions/vm_fast/tests/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ fn test_storage(first_tx_calldata: Vec<u8>, second_tx_calldata: Vec<u8>) -> u32
vm.vm.push_transaction(tx1);
let result = vm.vm.execute(VmExecutionMode::OneTx);
assert!(!result.result.is_failed(), "First tx failed");
vm.vm.pop_snapshot_no_rollback();

// We rollback once because transient storage and rollbacks are a tricky combination.
vm.vm.make_snapshot();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ impl VmTester {
inner_state_before, inner_state_after,
"Inner state before and after rollback should be equal"
);
} else {
self.vm.pop_snapshot_no_rollback();
}
result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
};

#[test]
#[ignore] // FIXME(PLA-1002): VM state assertion in `execute_tx_and_verify` fails
fn test_tracing_of_execution_errors() {
let contract_address = H160::random();
let mut vm = VmTesterBuilder::new()
Expand Down
21 changes: 11 additions & 10 deletions core/lib/multivm/src/versions/vm_fast/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,10 @@ pub struct Vm<S> {
pub(crate) inner: VirtualMachine,
suspended_at: u16,
gas_for_account_validation: u32,

pub(crate) bootloader_state: BootloaderState,

pub(crate) batch_env: L1BatchEnv,
pub(crate) system_env: SystemEnv,

snapshots: Vec<VmSnapshot>,
snapshot: Option<VmSnapshot>,
}

impl<S: ReadStorage> Vm<S> {
Expand Down Expand Up @@ -476,7 +473,7 @@ impl<S: ReadStorage> Vm<S> {
),
system_env,
batch_env,
snapshots: vec![],
snapshot: None,
};

me.write_to_bootloader_heap(bootloader_memory);
Expand Down Expand Up @@ -684,7 +681,11 @@ struct VmSnapshot {

impl<S: ReadStorage> VmInterfaceHistoryEnabled for Vm<S> {
fn make_snapshot(&mut self) {
self.snapshots.push(VmSnapshot {
assert!(
self.snapshot.is_none(),
"cannot create a VM snapshot until a previous snapshot is rolled back to or popped"
);
self.snapshot = Some(VmSnapshot {
vm_snapshot: self.inner.snapshot(),
bootloader_snapshot: self.bootloader_state.get_snapshot(),
suspended_at: self.suspended_at,
Expand All @@ -698,7 +699,7 @@ impl<S: ReadStorage> VmInterfaceHistoryEnabled for Vm<S> {
bootloader_snapshot,
suspended_at,
gas_for_account_validation,
} = self.snapshots.pop().expect("no snapshots to rollback to");
} = self.snapshot.take().expect("no snapshots to rollback to");

self.inner.rollback(vm_snapshot);
self.bootloader_state.apply_snapshot(bootloader_snapshot);
Expand All @@ -709,14 +710,14 @@ impl<S: ReadStorage> VmInterfaceHistoryEnabled for Vm<S> {
}

fn pop_snapshot_no_rollback(&mut self) {
self.snapshots.pop();
self.snapshot = None;
self.delete_history_if_appropriate();
}
}

impl<S: ReadStorage> Vm<S> {
fn delete_history_if_appropriate(&mut self) {
if self.snapshots.is_empty() && self.inner.state.previous_frames.is_empty() {
if self.snapshot.is_none() && self.inner.state.previous_frames.is_empty() {
self.inner.delete_history();
}
}
Expand All @@ -735,7 +736,7 @@ impl<S: fmt::Debug> fmt::Debug for Vm<S> {
.field("program_cache", &self.world.program_cache)
.field("batch_env", &self.batch_env)
.field("system_env", &self.system_env)
.field("snapshots", &self.snapshots.len())
.field("snapshot", &self.snapshot.as_ref().map(|_| ()))
.finish()
}
}
Expand Down
8 changes: 1 addition & 7 deletions core/lib/multivm/src/versions/vm_latest/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,14 +229,11 @@ impl<S: WriteStorage, H: HistoryMode> Vm<S, H> {
}
}

/// Methods of vm, which required some history manipulations
impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, HistoryEnabled> {
/// Create snapshot of current vm state and push it into the memory
fn make_snapshot(&mut self) {
self.make_snapshot_inner()
}

/// Rollback vm state to the latest snapshot and destroy the snapshot
fn rollback_to_the_latest_snapshot(&mut self) {
let snapshot = self
.snapshots
Expand All @@ -245,10 +242,7 @@ impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, HistoryEnabled> {
self.rollback_to_snapshot(snapshot);
}

/// Pop the latest snapshot from the memory and destroy it
fn pop_snapshot_no_rollback(&mut self) {
self.snapshots
.pop()
.expect("Snapshot should be created before rolling it back");
self.snapshots.pop();
}
}
2 changes: 1 addition & 1 deletion core/lib/multivm/src/versions/vm_m6/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,6 @@ impl<S: Storage> VmInterfaceHistoryEnabled for Vm<S, crate::vm_latest::HistoryEn
}

fn pop_snapshot_no_rollback(&mut self) {
self.vm.pop_snapshot_no_rollback()
self.vm.pop_snapshot_no_rollback();
}
}
3 changes: 1 addition & 2 deletions core/lib/multivm/src/versions/vm_m6/vm_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,9 +396,8 @@ impl<H: HistoryMode, S: Storage> VmInstance<S, H> {
}

/// Removes the latest snapshot without rolling back to it.
/// This function expects that there is at least one snapshot present.
pub fn pop_snapshot_no_rollback(&mut self) {
self.snapshots.pop().unwrap();
self.snapshots.pop();
}

/// Returns the amount of gas remaining to the VM.
Expand Down
9 changes: 2 additions & 7 deletions core/lib/multivm/src/versions/vm_refunds_enhancement/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,12 @@ impl<S: WriteStorage, H: HistoryMode> VmFactory<S> for Vm<S, H> {
}
}

/// Methods of vm, which required some history manipulations
impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, HistoryEnabled> {
/// Create snapshot of current vm state and push it into the memory
fn make_snapshot(&mut self) {
self.make_snapshot_inner()
self.make_snapshot_inner();
}

/// Rollback vm state to the latest snapshot and destroy the snapshot
fn rollback_to_the_latest_snapshot(&mut self) {
let snapshot = self
.snapshots
Expand All @@ -162,10 +160,7 @@ impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, HistoryEnabled> {
self.rollback_to_snapshot(snapshot);
}

/// Pop the latest snapshot from the memory and destroy it
fn pop_snapshot_no_rollback(&mut self) {
self.snapshots
.pop()
.expect("Snapshot should be created before rolling it back");
self.snapshots.pop();
}
}
8 changes: 1 addition & 7 deletions core/lib/multivm/src/versions/vm_virtual_blocks/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,11 @@ impl<S: WriteStorage, H: HistoryMode> VmFactory<S> for Vm<S, H> {
}
}

/// Methods of vm, which required some history manipulations
impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, HistoryEnabled> {
/// Create snapshot of current vm state and push it into the memory
fn make_snapshot(&mut self) {
self.make_snapshot_inner()
}

/// Rollback vm state to the latest snapshot and destroy the snapshot
fn rollback_to_the_latest_snapshot(&mut self) {
let snapshot = self
.snapshots
Expand All @@ -162,10 +159,7 @@ impl<S: WriteStorage> VmInterfaceHistoryEnabled for Vm<S, HistoryEnabled> {
self.rollback_to_snapshot(snapshot);
}

/// Pop the latest snapshot from the memory and destroy it
fn pop_snapshot_no_rollback(&mut self) {
self.snapshots
.pop()
.expect("Snapshot should be created before rolling it back");
self.snapshots.pop();
}
}
13 changes: 8 additions & 5 deletions core/node/state_keeper/src/batch_executor/main_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ impl CommandReceiver {
tx: &Transaction,
vm: &mut VmInstance<S, HistoryEnabled>,
) -> TxExecutionResult {
// Save pre-`execute_next_tx` VM snapshot.
// Executing a next transaction means that a previous transaction was either rolled back (in which case its snapshot
// was already removed), or that we build on top of it (in which case, it can be removed now).
vm.pop_snapshot_no_rollback();
// Save pre-execution VM snapshot.
vm.make_snapshot();

// Execute the transaction.
Expand Down Expand Up @@ -253,9 +256,6 @@ impl CommandReceiver {
// it means that there is no sense in polluting the space of compressed bytecodes,
// and so we re-execute the transaction, but without compression.

// Saving the snapshot before executing
vm.make_snapshot();

let call_tracer_result = Arc::new(OnceCell::default());
let tracer = if self.save_call_traces {
vec![CallTracer::new(call_tracer_result.clone()).into_tracer_pointer()]
Expand All @@ -267,15 +267,18 @@ impl CommandReceiver {
vm.inspect_transaction_with_bytecode_compression(tracer.into(), tx.clone(), true)
{
let compressed_bytecodes = vm.get_last_tx_compressed_bytecodes();
vm.pop_snapshot_no_rollback();

let trace = Arc::try_unwrap(call_tracer_result)
.unwrap()
.take()
.unwrap_or_default();
return (result, compressed_bytecodes, trace);
}

// Roll back to the snapshot just before the transaction execution taken in `Self::execute_tx()`
// and create a snapshot at the same VM state again.
vm.rollback_to_the_latest_snapshot();
vm.make_snapshot();

let call_tracer_result = Arc::new(OnceCell::default());
let tracer = if self.save_call_traces {
Expand Down

0 comments on commit 69be78c

Please sign in to comment.