use crate::{
constants::WASM_PAGE_SIZE_BYTES,
declaration::AllocationDeclaration,
ledger::{AllocationLedger, AllocationRecord, GenerationRecord},
physical::CommitStoreDiagnostic,
slot::{AllocationSlotDescriptor, MemoryManagerAuthorityRecord, MemoryManagerRangeAuthority},
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DiagnosticExport {
pub current_generation: u64,
pub ledger_anchor: AllocationSlotDescriptor,
pub records: Vec<DiagnosticRecord>,
pub generations: Vec<DiagnosticGeneration>,
pub commit_recovery: Option<CommitStoreDiagnostic>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DefaultMemoryManagerDoctorReport {
pub bootstrapped: bool,
pub ledger_anchor: AllocationSlotDescriptor,
pub stable_cell: DiagnosticStableCell,
pub commit_recovery: Option<CommitStoreDiagnostic>,
pub ledger: Option<DiagnosticExport>,
pub registered_declarations: Vec<DiagnosticDeclaration>,
pub range_authority: DiagnosticRangeAuthority,
pub validation: DiagnosticCheck,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticDeclaration {
pub declaring_crate: String,
pub declaration: AllocationDeclaration,
}
impl DiagnosticDeclaration {
#[must_use]
pub fn new(declaring_crate: impl Into<String>, declaration: AllocationDeclaration) -> Self {
Self {
declaring_crate: declaring_crate.into(),
declaration,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticRangeAuthority {
pub registered_records: Vec<MemoryManagerAuthorityRecord>,
pub effective_authority: Option<MemoryManagerRangeAuthority>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl DiagnosticRangeAuthority {
#[must_use]
pub const fn new(
registered_records: Vec<MemoryManagerAuthorityRecord>,
effective_authority: Option<MemoryManagerRangeAuthority>,
error: Option<String>,
) -> Self {
Self {
registered_records,
effective_authority,
error,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticStableCell {
pub status: DiagnosticStableCellStatus,
pub memory_size: DiagnosticMemorySize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl DiagnosticStableCell {
#[must_use]
pub const fn new(
status: DiagnosticStableCellStatus,
memory_size: DiagnosticMemorySize,
error: Option<String>,
) -> Self {
Self {
status,
memory_size,
error,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum DiagnosticStableCellStatus {
Empty,
Readable,
Corrupt,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticCheck {
pub status: DiagnosticCheckStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl DiagnosticCheck {
#[must_use]
pub const fn passed() -> Self {
Self {
status: DiagnosticCheckStatus::Passed,
message: None,
}
}
#[must_use]
pub fn failed(message: impl Into<String>) -> Self {
Self {
status: DiagnosticCheckStatus::Failed,
message: Some(message.into()),
}
}
#[must_use]
pub fn not_run(message: impl Into<String>) -> Self {
Self {
status: DiagnosticCheckStatus::NotRun,
message: Some(message.into()),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum DiagnosticCheckStatus {
NotRun,
Passed,
Failed,
}
impl DiagnosticExport {
#[must_use]
pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
}
#[must_use]
pub fn from_ledger_with_commit_recovery(
ledger: &AllocationLedger,
ledger_anchor: AllocationSlotDescriptor,
commit_recovery: Option<CommitStoreDiagnostic>,
) -> Self {
Self::from_ledger_with_commit_recovery_and_memory_sizes(
ledger,
ledger_anchor,
commit_recovery,
std::iter::empty(),
)
}
#[must_use]
pub fn from_ledger_with_memory_sizes(
ledger: &AllocationLedger,
ledger_anchor: AllocationSlotDescriptor,
memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
) -> Self {
Self::from_ledger_with_commit_recovery_and_memory_sizes(
ledger,
ledger_anchor,
None,
memory_sizes,
)
}
#[must_use]
pub fn from_ledger_with_commit_recovery_and_memory_sizes(
ledger: &AllocationLedger,
ledger_anchor: AllocationSlotDescriptor,
commit_recovery: Option<CommitStoreDiagnostic>,
memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
) -> Self {
let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
Self {
current_generation: ledger.current_generation,
ledger_anchor,
records: ledger
.allocation_history()
.records()
.iter()
.cloned()
.map(|allocation| {
let memory_size = memory_sizes.get(allocation.slot()).copied();
DiagnosticRecord {
allocation,
memory_size,
}
})
.collect(),
generations: ledger
.allocation_history()
.generations()
.iter()
.cloned()
.map(|generation| DiagnosticGeneration { generation })
.collect(),
commit_recovery,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DiagnosticRecord {
pub allocation: AllocationRecord,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory_size: Option<DiagnosticMemorySize>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct DiagnosticMemorySize {
pub wasm_pages: u64,
pub bytes: u64,
}
impl DiagnosticMemorySize {
#[must_use]
pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
Self {
wasm_pages,
bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DiagnosticGeneration {
pub generation: GenerationRecord,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
declaration::AllocationDeclaration,
ledger::{AllocationHistory, AllocationRecord, AllocationState},
physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
schema::SchemaMetadata,
};
#[test]
fn diagnostic_export_copies_ledger_records() {
let declaration = AllocationDeclaration::new(
"app.users.v1",
AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
None,
SchemaMetadata::default(),
)
.expect("declaration");
let ledger = AllocationLedger {
current_generation: 3,
allocation_history: AllocationHistory::from_parts(
vec![AllocationRecord::from_declaration(
3,
declaration,
AllocationState::Active,
)],
vec![GenerationRecord {
generation: 3,
parent_generation: 2,
runtime_fingerprint: Some("wasm:abc123".to_string()),
declaration_count: 1,
committed_at: None,
}],
),
};
let export = DiagnosticExport::from_ledger(
&ledger,
AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
);
assert_eq!(export.current_generation, 3);
assert_eq!(export.records.len(), 1);
assert_eq!(export.records[0].memory_size, None);
assert_eq!(export.generations.len(), 1);
assert_eq!(
export.ledger_anchor,
AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
);
assert_eq!(export.commit_recovery, None);
}
#[test]
fn diagnostic_export_can_include_commit_recovery_state() {
let ledger = AllocationLedger {
current_generation: 3,
allocation_history: AllocationHistory::default(),
};
let commit_recovery = CommitStoreDiagnostic {
slot0: CommitSlotDiagnostic {
present: true,
generation: Some(3),
valid: true,
},
slot1: CommitSlotDiagnostic {
present: false,
generation: None,
valid: false,
},
authoritative_generation: Some(3),
recovery_error: None,
};
let export = DiagnosticExport::from_ledger_with_commit_recovery(
&ledger,
AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
Some(commit_recovery),
);
assert_eq!(export.commit_recovery, Some(commit_recovery));
}
#[test]
fn diagnostic_export_can_include_memory_sizes() {
let declaration = AllocationDeclaration::new(
"app.users.v1",
AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
None,
SchemaMetadata::default(),
)
.expect("declaration");
let ledger = AllocationLedger {
current_generation: 3,
allocation_history: AllocationHistory::from_parts(
vec![AllocationRecord::from_declaration(
3,
declaration,
AllocationState::Active,
)],
Vec::new(),
),
};
let export = DiagnosticExport::from_ledger_with_memory_sizes(
&ledger,
AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
[(
AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
DiagnosticMemorySize::from_wasm_pages(2),
)],
);
assert_eq!(
export.records[0].memory_size,
Some(DiagnosticMemorySize {
wasm_pages: 2,
bytes: 131_072,
})
);
}
#[test]
fn diagnostic_export_can_report_recovery_failure() {
let ledger = AllocationLedger {
current_generation: 0,
allocation_history: AllocationHistory::default(),
};
let commit_recovery = CommitStoreDiagnostic {
slot0: CommitSlotDiagnostic {
present: false,
generation: None,
valid: false,
},
slot1: CommitSlotDiagnostic {
present: false,
generation: None,
valid: false,
},
authoritative_generation: None,
recovery_error: Some(CommitRecoveryError::NoValidGeneration),
};
let export = DiagnosticExport::from_ledger_with_commit_recovery(
&ledger,
AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
Some(commit_recovery),
);
assert_eq!(
export
.commit_recovery
.expect("commit recovery")
.recovery_error,
Some(CommitRecoveryError::NoValidGeneration)
);
}
}