use crate::{
constants::WASM_PAGE_SIZE_BYTES,
ledger::{AllocationLedger, AllocationRecord},
physical::CommitSnapshotDiagnostic,
policy::PolicyIdentity,
};
use serde::Serialize;
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DiagnosticExport {
pub current_generation: u64,
pub records: Vec<DiagnosticRecord>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DiagnosticRuntimeBinding {
pub policy_identity: PolicyIdentity,
pub allocation_pool: crate::MemoryAllocationPool,
}
impl DiagnosticRuntimeBinding {
#[must_use]
pub const fn new(
policy_identity: PolicyIdentity,
allocation_pool: crate::MemoryAllocationPool,
) -> Self {
Self {
policy_identity,
allocation_pool,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct MemoryRuntimeDoctorReport {
pub bootstrapped: bool,
pub tested_policy_identity: Result<PolicyIdentity, DiagnosticFailure>,
pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
pub bootstrap_binding: DiagnosticCheck,
pub stable_cell: DiagnosticStableCell,
pub commit_recovery: Option<CommitSnapshotDiagnostic>,
pub ledger: Option<DiagnosticExport>,
pub requests: Vec<crate::MemoryRequest>,
pub allocation_pool: crate::MemoryAllocationPool,
pub validation: DiagnosticCheck,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub enum DiagnosticCode {
#[serde(rename = "stable_cell")]
StableCell,
#[serde(rename = "unsupported_format")]
UnsupportedFormat,
#[serde(rename = "ledger_recovery")]
LedgerRecovery,
#[serde(rename = "allocation_validation")]
AllocationValidation,
#[serde(rename = "policy_identity")]
PolicyIdentity,
#[serde(rename = "runtime_binding")]
RuntimeBinding,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DiagnosticFailure {
pub code: DiagnosticCode,
pub message: String,
}
impl DiagnosticFailure {
#[must_use]
pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DiagnosticStableCell {
pub status: DiagnosticStableCellStatus,
pub memory_size: DiagnosticMemorySize,
}
impl DiagnosticStableCell {
#[must_use]
pub const fn new(
status: DiagnosticStableCellStatus,
memory_size: DiagnosticMemorySize,
) -> Self {
Self {
status,
memory_size,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum DiagnosticStableCellStatus {
Empty,
Readable,
Corrupt {
failure: DiagnosticFailure,
},
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum DiagnosticCheck {
NotRun {
code: DiagnosticCode,
message: String,
},
Passed,
Failed {
code: DiagnosticCode,
message: String,
},
}
impl DiagnosticCheck {
#[must_use]
pub const fn passed() -> Self {
Self::Passed
}
#[must_use]
pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
Self::Failed {
code,
message: message.into(),
}
}
#[must_use]
pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
Self::NotRun {
code,
message: message.into(),
}
}
}
impl DiagnosticExport {
#[must_use]
pub fn from_ledger(ledger: &AllocationLedger) -> Self {
Self::from_records(ledger.current_generation, ledger.records.iter().cloned())
}
pub(crate) fn from_owned_ledger(ledger: AllocationLedger) -> Self {
Self::from_records(ledger.current_generation, ledger.records.into_iter())
}
fn from_records(
current_generation: u64,
records: impl Iterator<Item = AllocationRecord>,
) -> Self {
Self {
current_generation,
records: records
.map(|allocation| DiagnosticRecord {
allocation,
memory_size: None,
})
.collect(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct DiagnosticRecord {
pub allocation: AllocationRecord,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_size: Option<DiagnosticMemorySize>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub struct DiagnosticMemorySize {
pub wasm_pages: u64,
}
impl DiagnosticMemorySize {
#[must_use]
pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
Self { wasm_pages }
}
#[must_use]
pub const fn bytes(self) -> u64 {
self.wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AllocationDeclaration, MemoryManagerSlot, ledger::AllocationRecord};
#[test]
fn diagnostic_export_copies_ledger_records() {
let declaration = AllocationDeclaration::new(
"app.users.v1",
MemoryManagerSlot::new(100).expect("usable slot"),
)
.expect("declaration");
let ledger = AllocationLedger {
current_generation: 3,
records: vec![AllocationRecord::active(&declaration)],
};
let export = DiagnosticExport::from_ledger(&ledger);
assert_eq!(export.current_generation, 3);
assert_eq!(export.records.len(), 1);
assert_eq!(export.records[0].memory_size, None);
assert_eq!(export, DiagnosticExport::from_owned_ledger(ledger));
let wire = serde_json::to_value(&export).expect("diagnostic JSON");
assert_eq!(
wire,
serde_json::json!({
"current_generation": 3,
"records": [{"allocation": {"stable_key": "app.users.v1", "slot": 100, "state": "Active"}}]
})
);
}
#[test]
fn diagnostic_outcome_states_serialize() {
let stable_cell = DiagnosticStableCell::new(
DiagnosticStableCellStatus::Corrupt {
failure: DiagnosticFailure::new(
DiagnosticCode::StableCell,
"bad stable-cell record",
),
},
DiagnosticMemorySize::from_wasm_pages(1),
);
let allocation_pool = crate::MemoryAllocationPool::new(Vec::new(), Vec::new()).unwrap();
let check = DiagnosticCheck::failed(
DiagnosticCode::AllocationValidation,
"duplicate declaration",
);
assert_eq!(
serde_json::to_value(DiagnosticCheck::passed()).unwrap(),
"Passed"
);
assert_eq!(
serde_json::to_value(check).unwrap(),
serde_json::json!({
"Failed": {"code": "allocation_validation", "message": "duplicate declaration"}
})
);
assert_eq!(
serde_json::to_value(stable_cell).unwrap(),
serde_json::json!({
"status": {"Corrupt": {"failure": {"code": "stable_cell", "message": "bad stable-cell record"}}},
"memory_size": {"wasm_pages": 1}
})
);
assert_eq!(
serde_json::to_value(allocation_pool).unwrap(),
serde_json::json!({
"authorities": [], "excluded_ids": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
})
);
}
#[test]
fn diagnostic_codes_have_stable_wire_names() {
let cases = [
(DiagnosticCode::StableCell, "stable_cell"),
(DiagnosticCode::UnsupportedFormat, "unsupported_format"),
(DiagnosticCode::LedgerRecovery, "ledger_recovery"),
(
DiagnosticCode::AllocationValidation,
"allocation_validation",
),
(DiagnosticCode::PolicyIdentity, "policy_identity"),
(DiagnosticCode::RuntimeBinding, "runtime_binding"),
];
for (code, expected) in cases {
assert_eq!(
crate::test_cbor::to_value(code).expect("diagnostic code value"),
crate::test_cbor::Value::Text(expected.to_string())
);
}
}
#[test]
fn diagnostic_export_can_include_memory_sizes() {
let declaration = AllocationDeclaration::new(
"app.users.v1",
MemoryManagerSlot::new(100).expect("usable slot"),
)
.expect("declaration");
let ledger = AllocationLedger {
current_generation: 3,
records: vec![AllocationRecord::active(&declaration)],
};
let mut export = DiagnosticExport::from_ledger(&ledger);
export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
assert_eq!(
export.records[0].memory_size,
Some(DiagnosticMemorySize { wasm_pages: 2 })
);
let wire = serde_json::to_value(&export).expect("diagnostic JSON");
assert_eq!(
wire["records"][0]["memory_size"],
serde_json::json!({"wasm_pages": 2})
);
}
}