Skip to content

Commit

Permalink
Closure stabilization
Browse files Browse the repository at this point in the history
  • Loading branch information
luc-blaeser committed Nov 25, 2024
1 parent fd40011 commit f968a6f
Show file tree
Hide file tree
Showing 13 changed files with 432 additions and 168 deletions.
225 changes: 72 additions & 153 deletions rts/motoko-rts/src/persistence/stable_functions.rs

Large diffs are not rendered by default.

121 changes: 121 additions & 0 deletions rts/motoko-rts/src/persistence/stable_functions/gc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use motoko_rts_macros::ic_mem_fn;

use crate::{gc::remembered_set::RememberedSet, memory::Memory, persistence::stable_functions::is_flexible_function_id, types::{Value, NULL_POINTER, TAG_CLOSURE, TAG_OBJECT, TAG_SOME}, visitor::enhanced::visit_pointer_fields};

use super::{mark_stack::{MarkStack, StackEntry}, resolve_stable_function_id, FunctionId, PersistentVirtualTable};

// Currently fields in closure (captures) are not yet discovered in a type-directed way.
// This sentinel denotes that there is no static type known and the generic visitor is to be invoked.
// TODO: Optimization: Use expected closure types to select a compiler-generated specialized visitor.
const UNKNOWN_TYPE_ID: u64 = u64::MAX;

extern "C" {
fn moc_visit_stable_functions(object: Value, type_id: u64);
}

pub struct FunctionGC {
mark_set: RememberedSet,
mark_stack: MarkStack,
virtual_table: *mut PersistentVirtualTable,
}

impl FunctionGC {
unsafe fn new<M: Memory>(
mem: &mut M,
virtual_table: *mut PersistentVirtualTable,
) -> FunctionGC {
let mark_set = RememberedSet::new(mem);
let mark_stack = MarkStack::new(mem);
FunctionGC {
mark_set,
mark_stack,
virtual_table,
}
}

unsafe fn run<M: Memory>(&mut self, mem: &mut M) {
self.clear_mark_bits();
loop {
match self.mark_stack.pop() {
None => return,
Some(StackEntry { object, type_id }) => {
debug_assert_ne!(object, NULL_POINTER);
if object.tag() == TAG_SOME {
// skip null boxes, not visited
} else if object.tag() == TAG_CLOSURE {
self.visit_stable_closure(mem, object);
} else if type_id == UNKNOWN_TYPE_ID {
self.generic_visit(mem, object);
} else {
// Specialized field visitor, as optimization.
moc_visit_stable_functions(object, type_id);
}
}
}
}
}

unsafe fn generic_visit<M: Memory>(&mut self, mem: &mut M, object: Value) {
visit_pointer_fields(
mem,
object.as_obj(),
object.tag(),
|mem, field| {
collect_stable_functions(mem, *field, UNKNOWN_TYPE_ID);
},
|_, slice_start, arr| {
assert!(slice_start == 0);
arr.len()
},
);
}

unsafe fn visit_stable_closure<M: Memory>(&mut self, mem: &mut M, object: Value) {
let closure = object.as_closure();
let function_id = (*closure).funid;
assert!(!is_flexible_function_id(function_id));
self.mark_function(function_id);
self.generic_visit(mem, object);
}

unsafe fn mark_function(&mut self, function_id: FunctionId) {
let entry = self.virtual_table.get(resolve_stable_function_id(function_id));
(*entry).marked = true;
}

unsafe fn clear_mark_bits(&mut self) {
for index in 0..self.virtual_table.length() {
let entry = self.virtual_table.get(index);
(*entry).marked = false;
}
}
}

static mut COLLECTOR_STATE: Option<FunctionGC> = None;

// Garbage collect the stable functions in the old version on an upgrade.
pub unsafe fn garbage_collect_functions<M: Memory>(
mem: &mut M,
virtual_table: *mut PersistentVirtualTable,
old_actor: Option<Value>,
) {
if old_actor.is_none() {
return;
}
let old_actor = old_actor.unwrap();
assert_eq!(old_actor.tag(), TAG_OBJECT);
COLLECTOR_STATE = Some(FunctionGC::new(mem, virtual_table));
const ACTOR_TYPE_ID: u64 = 0;
collect_stable_functions(mem, old_actor, ACTOR_TYPE_ID);
COLLECTOR_STATE.as_mut().unwrap().run(mem);
COLLECTOR_STATE = None;
}

#[ic_mem_fn]
unsafe fn collect_stable_functions<M: Memory>(mem: &mut M, object: Value, type_id: u64) {
let state = COLLECTOR_STATE.as_mut().unwrap();
if object != NULL_POINTER && !state.mark_set.contains(object) {
state.mark_set.insert(mem, object);
state.mark_stack.push(mem, StackEntry { object, type_id });
}
}
23 changes: 19 additions & 4 deletions rts/motoko-rts/src/stabilization/ic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ use crate::{
gc::incremental::{is_gc_stopped, resume_gc, stop_gc},
memory::Memory,
persistence::{
compatibility::{MemoryCompatibilityTest, TypeDescriptor},
set_upgrade_instructions,
compatibility::{MemoryCompatibilityTest, TypeDescriptor}, set_upgrade_instructions, stable_function_state, stable_functions::{gc::garbage_collect_functions, upgrade_stable_functions, PersistentVirtualTable, StableFunctionMap}
},
rts_trap_with,
stabilization::ic::metadata::StabilizationMetadata,
Expand All @@ -24,6 +23,7 @@ use super::{deserialization::Deserialization, serialization::Serialization};
struct StabilizationState {
old_candid_data: Value,
old_type_offsets: Value,
old_virtual_table: Value,
completed: bool,
serialization: Serialization,
instruction_meter: InstructionMeter,
Expand All @@ -34,10 +34,12 @@ impl StabilizationState {
serialization: Serialization,
old_candid_data: Value,
old_type_offsets: Value,
old_virtual_table: Value,
) -> StabilizationState {
StabilizationState {
old_candid_data,
old_type_offsets,
old_virtual_table,
completed: false,
serialization,
instruction_meter: InstructionMeter::new(),
Expand Down Expand Up @@ -67,17 +69,23 @@ pub unsafe fn start_graph_stabilization<M: Memory>(
stable_actor: Value,
old_candid_data: Value,
old_type_offsets: Value,
stable_functions_map: Value,
_old_function_map: Value,
) {
assert!(STABILIZATION_STATE.is_none());
assert!(is_gc_stopped());
let function_state = stable_function_state();
garbage_collect_functions(mem, function_state.get_virtual_table(), Some(stable_actor));
let stable_memory_pages = stable_mem::size(); // Backup the virtual size.
let serialized_data_start = stable_memory_pages * PAGE_SIZE;
let serialization = Serialization::start(mem, stable_actor, serialized_data_start);
// Mark the alive stable functions before stabilization such that destabilization can later check
// their existence in the new program version.
let old_virtual_table = function_state.virtual_table();
STABILIZATION_STATE = Some(StabilizationState::new(
serialization,
old_candid_data,
old_type_offsets,
old_virtual_table,
));
}

Expand Down Expand Up @@ -121,10 +129,12 @@ unsafe fn write_metadata() {
let serialized_data_length = state.serialization.serialized_data_length();

let type_descriptor = TypeDescriptor::new(state.old_candid_data, state.old_type_offsets);
let persistent_virtual_table = state.old_virtual_table;
let metadata = StabilizationMetadata {
serialized_data_start,
serialized_data_length,
type_descriptor,
persistent_virtual_table,
};
state.instruction_meter.stop();
metadata.store(&mut state.instruction_meter);
Expand Down Expand Up @@ -156,7 +166,7 @@ pub unsafe fn start_graph_destabilization<M: Memory>(
mem: &mut M,
new_candid_data: Value,
new_type_offsets: Value,
stable_functions_map: Value,
new_function_map: Value,
) {
assert!(DESTABILIZATION_STATE.is_none());

Expand All @@ -170,6 +180,11 @@ pub unsafe fn start_graph_destabilization<M: Memory>(
if !type_test.compatible_stable_actor() {
rts_trap_with("Memory-incompatible program upgrade");
}
// Upgrade the stable functions and check their compatibility.
// The alive stable functions have been marked by the GC in `start_graph_stabilization`.
let virtual_table = PersistentVirtualTable::from_blob(metadata.persistent_virtual_table);
let stable_functions = StableFunctionMap::from_blob(new_function_map);
upgrade_stable_functions(mem, virtual_table, stable_functions, Some(&type_test));
// Restore the virtual size.
moc_stable_mem_set_size(metadata.serialized_data_start / PAGE_SIZE);

Expand Down
25 changes: 25 additions & 0 deletions rts/motoko-rts/src/stabilization/ic/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
//! Type offset table
//! Byte length (u64)
//! Data
//! Persistent virtual table (only available with stable functions)
//! Byte length (u64)
//! Data
//! (possible zero padding)
//! -- Last physical page (metadata):
//! (zero padding to align at page end)
Expand Down Expand Up @@ -64,6 +67,7 @@ pub struct StabilizationMetadata {
pub serialized_data_start: u64,
pub serialized_data_length: u64,
pub type_descriptor: TypeDescriptor,
pub persistent_virtual_table: Value, // refers to `PersistentVirtualTable`
}

impl StabilizationMetadata {
Expand Down Expand Up @@ -104,6 +108,10 @@ impl StabilizationMetadata {
Self::write_blob(offset, descriptor.type_offsets());
}

fn save_stable_functions(offset: &mut u64, virtual_table: Value) {
Self::write_blob(offset, virtual_table);
}

fn read_length(offset: &mut u64) -> u64 {
let length = read_u64(*offset);
// Note: Do not use `types::size_of()` as it rounds to 64-bit words.
Expand Down Expand Up @@ -134,6 +142,20 @@ impl StabilizationMetadata {
TypeDescriptor::new(candid_data, type_offsets)
}

fn load_peristent_virtual_table<M: Memory>(mem: &mut M, offset: &mut u64) -> Value {
assert!(*offset <= Self::metadata_location());
// Backwards compatibility: The persistent virtual table may be missing,
// in which case the metadata directly follows the offset, or there is zero padding.
if *offset < Self::metadata_location() {
// There is either an existing virtual table, or if it is missing, there is zero padding
// which is decoded as an empty blob.
Self::read_blob(mem, TAG_BLOB_B, offset)
} else {
// No space for persistent virtual table.
unsafe { alloc_blob(mem, TAG_BLOB_B, Bytes(0)) }
}
}

fn metadata_location() -> u64 {
let physical_pages = unsafe { ic0_stable64_size() };
assert!(physical_pages > 0);
Expand Down Expand Up @@ -172,6 +194,7 @@ impl StabilizationMetadata {
Self::align_page_start(&mut offset);
let type_descriptor_address = offset;
Self::save_type_descriptor(&mut offset, &self.type_descriptor);
Self::save_stable_functions(&mut offset, self.persistent_virtual_table);
Self::align_page_start(&mut offset);
let first_word_backup = read_u32(0);
// Clear very first word that is backed up in the last page.
Expand Down Expand Up @@ -202,10 +225,12 @@ impl StabilizationMetadata {
write_u32(0, last_page_record.first_word_backup);
let mut offset = last_page_record.type_descriptor_address;
let type_descriptor = Self::load_type_descriptor(mem, &mut offset);
let persistent_virtual_table = Self::load_peristent_virtual_table(mem, &mut offset);
let metadata = StabilizationMetadata {
serialized_data_start: last_page_record.serialized_data_address,
serialized_data_length: last_page_record.serialized_data_length,
type_descriptor,
persistent_virtual_table,
};
(metadata, last_page_record.statistics)
}
Expand Down
17 changes: 15 additions & 2 deletions rts/motoko-rts/src/stabilization/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
//! offsets can be scaled down by a factor `8` during the destabilization
//! such that they fit into 32-bit values during Cheney's graph-copy.

use stable_closure::StableClosure;

use crate::{
barriers::allocation_barrier,
constants::WORD_SIZE,
Expand All @@ -31,8 +33,8 @@ use crate::{
types::{
base_array_tag, size_of, Tag, Value, TAG_ARRAY_I, TAG_ARRAY_M, TAG_ARRAY_S,
TAG_ARRAY_SLICE_MIN, TAG_ARRAY_T, TAG_BIGINT, TAG_BITS64_F, TAG_BITS64_S, TAG_BITS64_U,
TAG_BLOB_A, TAG_BLOB_B, TAG_BLOB_P, TAG_BLOB_T, TAG_CONCAT, TAG_MUTBOX, TAG_OBJECT,
TAG_REGION, TAG_SOME, TAG_VARIANT, TRUE_VALUE,
TAG_BLOB_A, TAG_BLOB_B, TAG_BLOB_P, TAG_BLOB_T, TAG_CLOSURE, TAG_CONCAT, TAG_MUTBOX,
TAG_OBJECT, TAG_REGION, TAG_SOME, TAG_VARIANT, TRUE_VALUE,
},
};

Expand All @@ -55,6 +57,7 @@ mod stable_array;
mod stable_bigint;
mod stable_bits64;
mod stable_blob;
mod stable_closure;
mod stable_concat;
mod stable_mutbox;
mod stable_object;
Expand Down Expand Up @@ -84,6 +87,8 @@ pub enum StableObjectKind {
Concat = 16,
BigInt = 17,
Some = 18,
// Extension:
Closure = 19,
}

#[repr(C)]
Expand Down Expand Up @@ -115,6 +120,7 @@ impl StableTag {
const STABLE_TAG_CONCAT: u64 = StableObjectKind::Concat as u64;
const STABLE_TAG_BIGINT: u64 = StableObjectKind::BigInt as u64;
const STABLE_TAG_SOME: u64 = StableObjectKind::Some as u64;
const STABLE_TAG_CLOSURE: u64 = StableObjectKind::Closure as u64;
match self.0 {
STABLE_TAG_ARRAY_IMMUTABLE => StableObjectKind::ArrayImmutable,
STABLE_TAG_ARRAY_MUTABLE => StableObjectKind::ArrayMutable,
Expand All @@ -134,6 +140,7 @@ impl StableTag {
STABLE_TAG_CONCAT => StableObjectKind::Concat,
STABLE_TAG_BIGINT => StableObjectKind::BigInt,
STABLE_TAG_SOME => StableObjectKind::Some,
STABLE_TAG_CLOSURE => StableObjectKind::Closure,
_ => unsafe { rts_trap_with("Invalid tag") },
}
}
Expand Down Expand Up @@ -167,6 +174,7 @@ impl StableObjectKind {
TAG_CONCAT => StableObjectKind::Concat,
TAG_BIGINT => StableObjectKind::BigInt,
TAG_SOME => StableObjectKind::Some,
TAG_CLOSURE => StableObjectKind::Closure,
_ => unreachable!("invalid tag"),
}
}
Expand Down Expand Up @@ -369,6 +377,7 @@ pub fn scan_serialized<
StableObjectKind::Concat => StableConcat::scan_serialized(context, translate),
StableObjectKind::BigInt => StableBigInt::scan_serialized(context, translate),
StableObjectKind::Some => StableSome::scan_serialized(context, translate),
StableObjectKind::Closure => StableClosure::scan_serialized(context, translate),
}
}

Expand All @@ -394,6 +403,7 @@ pub unsafe fn serialize(stable_memory: &mut StableMemoryStream, main_object: Val
StableObjectKind::Concat => StableConcat::serialize(stable_memory, main_object),
StableObjectKind::BigInt => StableBigInt::serialize(stable_memory, main_object),
StableObjectKind::Some => StableSome::serialize(stable_memory, main_object),
StableObjectKind::Closure => StableClosure::serialize(stable_memory, main_object),
}
}

Expand Down Expand Up @@ -443,5 +453,8 @@ pub unsafe fn deserialize<M: Memory>(
StableObjectKind::Some => {
StableSome::deserialize(main_memory, stable_memory, stable_object, object_kind)
}
StableObjectKind::Closure => {
StableClosure::deserialize(main_memory, stable_memory, stable_object, object_kind)
}
}
}
Loading

0 comments on commit f968a6f

Please sign in to comment.