use crate::runtime::ObjTag;
use crate::runtime::function::CallFrame;
use crate::vm::Vm;
#[derive(Debug, Clone)]
pub struct HeapSnapshot {
pub total_objects: usize,
pub total_bytes: usize,
pub buckets: Vec<HeapBucket>,
}
#[derive(Debug, Clone)]
pub struct HeapBucket {
pub type_name: &'static str,
pub count: usize,
pub bytes_approx: usize,
}
pub fn heap_walk(vm: &Vm) -> HeapSnapshot {
let mut counts = [0usize; 8];
let mut byte_estimate = [0usize; 8];
vm.heap.walk_objects(|tag| {
let idx = tag as usize;
counts[idx] += 1;
byte_estimate[idx] += tag_payload_size(tag);
});
let mut buckets: Vec<HeapBucket> = ALL_TAGS
.iter()
.copied()
.filter_map(|tag| {
let idx = tag as usize;
if counts[idx] == 0 {
return None;
}
Some(HeapBucket {
type_name: tag_name(tag),
count: counts[idx],
bytes_approx: byte_estimate[idx],
})
})
.collect();
buckets.sort_by(|a, b| {
b.count
.cmp(&a.count)
.then_with(|| a.type_name.cmp(b.type_name))
});
HeapSnapshot {
total_objects: vm.heap.live_objects(),
total_bytes: vm.heap.bytes(),
buckets,
}
}
const ALL_TAGS: [ObjTag; 8] = [
ObjTag::Str,
ObjTag::Table,
ObjTag::Proto,
ObjTag::Closure,
ObjTag::Upvalue,
ObjTag::Native,
ObjTag::Coro,
ObjTag::Userdata,
];
const _OBJTAG_COVERS_EVERY_VARIANT: () = {
let _ = |t: ObjTag| match t {
ObjTag::Str
| ObjTag::Table
| ObjTag::Proto
| ObjTag::Closure
| ObjTag::Upvalue
| ObjTag::Native
| ObjTag::Coro
| ObjTag::Userdata => (),
};
};
fn tag_name(tag: ObjTag) -> &'static str {
match tag {
ObjTag::Str => "str",
ObjTag::Table => "table",
ObjTag::Proto => "proto",
ObjTag::Closure => "closure",
ObjTag::Upvalue => "upvalue",
ObjTag::Native => "native",
ObjTag::Coro => "coro",
ObjTag::Userdata => "userdata",
}
}
fn tag_payload_size(tag: ObjTag) -> usize {
use crate::runtime::function::{LuaClosure, NativeClosure, Proto, Upvalue};
use crate::runtime::table::Table;
use crate::runtime::userdata::Userdata;
use crate::runtime::{Coro, LuaStr};
match tag {
ObjTag::Str => core::mem::size_of::<LuaStr>(),
ObjTag::Table => core::mem::size_of::<Table>(),
ObjTag::Proto => core::mem::size_of::<Proto>(),
ObjTag::Closure => core::mem::size_of::<LuaClosure>(),
ObjTag::Upvalue => core::mem::size_of::<Upvalue>(),
ObjTag::Native => core::mem::size_of::<NativeClosure>(),
ObjTag::Coro => core::mem::size_of::<Coro>(),
ObjTag::Userdata => core::mem::size_of::<Userdata>(),
}
}
#[derive(Debug, Clone)]
pub struct JitStateSnapshot {
pub enabled: bool,
pub trace_enabled: bool,
pub active_trace_head_pc: Option<u32>,
pub active_trace_len: Option<usize>,
pub trace_closed_count: u64,
pub trace_aborted_count: u64,
pub trace_dispatched_count: u64,
pub trace_compiled_count: u64,
pub trace_deopt_count: u64,
}
pub fn jit_state_snapshot(vm: &Vm) -> JitStateSnapshot {
let js = &vm.jit;
JitStateSnapshot {
enabled: js.enabled,
trace_enabled: js.trace_enabled,
active_trace_head_pc: js.active_trace.as_ref().map(|t| t.head_pc),
active_trace_len: js.active_trace.as_ref().map(|t| t.ops.len()),
trace_closed_count: js.counters.closed,
trace_aborted_count: js.counters.aborted,
trace_dispatched_count: js.counters.dispatched,
trace_compiled_count: js.counters.compiled,
trace_deopt_count: js.counters.deopt,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FrameInfo {
pub source: String,
pub line: u32,
pub line_defined: u32,
}
pub fn frames_for_profile(vm: &Vm) -> Vec<FrameInfo> {
let frames = vm.inspect_frames();
let mut out = Vec::with_capacity(frames.len());
for cf in frames {
let CallFrame::Lua(f) = cf else { continue };
let closure = &*f.closure;
let proto = &*closure.proto;
let pc_idx = (f.pc as usize).saturating_sub(1);
let line = proto.lines.get(pc_idx).copied().unwrap_or(0);
let src_bytes = proto.source.as_bytes();
out.push(FrameInfo {
source: String::from_utf8_lossy(src_bytes).into_owned(),
line,
line_defined: proto.line_defined,
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn heap_walk_fresh_vm_has_some_protos_and_strings() {
let vm = Vm::new(crate::version::LuaVersion::Lua55);
let snap = heap_walk(&vm);
assert_eq!(
snap.total_objects,
snap.buckets.iter().map(|b| b.count).sum::<usize>(),
"per-bucket counts must sum to live_objects"
);
assert!(
snap.buckets.iter().all(|b| b.count > 0),
"no zero-count rows allowed in the report"
);
}
#[test]
fn jit_state_snapshot_default_inert() {
let vm = Vm::new(crate::version::LuaVersion::Lua55);
let snap = jit_state_snapshot(&vm);
assert!(snap.enabled);
assert!(snap.active_trace_head_pc.is_none());
assert_eq!(snap.trace_closed_count, 0);
assert_eq!(snap.trace_aborted_count, 0);
}
#[test]
fn frames_for_profile_empty_when_no_call_in_flight() {
let vm = Vm::new(crate::version::LuaVersion::Lua55);
let frames = frames_for_profile(&vm);
assert!(
frames.is_empty(),
"no Lua call in flight, expected empty frame list, got {frames:?}"
);
}
}