1use std::collections::HashMap;
2use std::hash::Hash;
3use std::mem;
4use std::os::raw::c_char;
5
6use crate::state::ExtraData;
7
8use super::json::{self, Json};
9
10#[cfg(any(feature = "luau", doc))]
12#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
13pub struct HeapDump {
14 data: Json<'static>, buf: Box<str>,
16}
17
18impl HeapDump {
19 pub(crate) unsafe fn new(state: *mut ffi::lua_State) -> Option<Self> {
21 unsafe extern "C" fn category_name(state: *mut ffi::lua_State, cat: u8) -> *const c_char {
22 (&*ExtraData::get(state))
23 .mem_categories
24 .get(cat as usize)
25 .map(|s| s.as_ptr())
26 .unwrap_or(cstr!("unknown"))
27 }
28
29 let mut buf = Vec::new();
30 unsafe {
31 let file = libc::tmpfile();
32 if file.is_null() {
33 return None;
34 }
35 ffi::lua_gcdump(state, file as *mut _, Some(category_name));
36 libc::fseek(file, 0, libc::SEEK_END);
37 let len = libc::ftell(file);
38 libc::rewind(file);
39 if len > 0 {
40 let len = len as usize;
41 buf.reserve(len);
42 let n = libc::fread(buf.as_mut_ptr() as *mut _, 1, len, file);
43 buf.set_len(n);
44 }
45 libc::fclose(file);
46 }
47
48 let buf = String::from_utf8(buf).ok()?.into_boxed_str();
49 let data = json::parse(unsafe { mem::transmute::<&str, &'static str>(&buf) }).ok()?;
50 Some(HeapDump { data, buf })
51 }
52
53 #[doc(hidden)]
57 pub fn to_json(&self) -> &str {
58 &self.buf
59 }
60
61 pub fn size(&self) -> u64 {
63 self.data["stats"]["size"].as_u64().unwrap_or_default()
64 }
65
66 pub fn size_by_type<'a>(&'a self, category: Option<&str>) -> HashMap<&'a str, (usize, u64)> {
71 self.size_by_type_inner(category).unwrap_or_default()
72 }
73
74 fn size_by_type_inner<'a>(&'a self, category: Option<&str>) -> Option<HashMap<&'a str, (usize, u64)>> {
75 let category_id = match category {
76 Some(cat) => Some(self.find_category_id(cat)?),
78 None => None,
79 };
80
81 let mut size_by_type = HashMap::new();
82 let objects = self.data["objects"].as_object()?;
83 for obj in objects.values() {
84 if let Some(cat_id) = category_id
85 && obj["cat"].as_i64()? != cat_id
86 {
87 continue;
88 }
89 update_size(&mut size_by_type, obj["type"].as_str()?, obj["size"].as_u64()?);
90 }
91 Some(size_by_type)
92 }
93
94 pub fn size_by_category(&self) -> HashMap<&str, u64> {
96 let mut size_by_category = HashMap::new();
97 if let Some(categories) = self.data["stats"]["categories"].as_object() {
98 for cat in categories.values() {
99 if let Some(cat_name) = cat["name"].as_str() {
100 size_by_category.insert(cat_name, cat["size"].as_u64().unwrap_or_default());
101 }
102 }
103 }
104 size_by_category
105 }
106
107 pub fn size_by_userdata<'a>(&'a self, category: Option<&str>) -> HashMap<&'a str, (usize, u64)> {
112 self.size_by_userdata_inner(category).unwrap_or_default()
113 }
114
115 fn size_by_userdata_inner<'a>(
116 &'a self,
117 category: Option<&str>,
118 ) -> Option<HashMap<&'a str, (usize, u64)>> {
119 let category_id = match category {
120 Some(cat) => Some(self.find_category_id(cat)?),
122 None => None,
123 };
124
125 let mut size_by_userdata = HashMap::new();
126 let objects = self.data["objects"].as_object()?;
127 for obj in objects.values() {
128 if obj["type"] != "userdata" {
129 continue;
130 }
131 if let Some(cat_id) = category_id
132 && obj["cat"].as_i64()? != cat_id
133 {
134 continue;
135 }
136
137 let mut ud_type = "unknown";
139 if let Some(metatable_addr) = obj["metatable"].as_str()
140 && let Some(t) = get_key(objects, &objects[metatable_addr], "__type")
141 {
142 ud_type = t;
143 }
144 update_size(&mut size_by_userdata, ud_type, obj["size"].as_u64()?);
145 }
146 Some(size_by_userdata)
147 }
148
149 fn find_category_id(&self, category: &str) -> Option<i64> {
151 let categories = self.data["stats"]["categories"].as_object()?;
152 for (cat_id, cat) in categories {
153 if cat["name"].as_str() == Some(category) {
154 return cat_id.parse().ok();
155 }
156 }
157 None
158 }
159}
160
161fn update_size<K: Eq + Hash>(size_type: &mut HashMap<K, (usize, u64)>, key: K, size: u64) {
163 let (count, total_size) = size_type.entry(key).or_insert((0, 0));
164 *count += 1;
165 *total_size += size;
166}
167
168fn get_key<'a>(objects: &'a HashMap<&'a str, Json>, tbl: &Json, key: &str) -> Option<&'a str> {
170 let pairs = tbl["pairs"].as_array()?;
171 for kv in pairs.chunks_exact(2) {
172 #[rustfmt::skip]
173 let (Some(key_addr), Some(val_addr)) = (kv[0].as_str(), kv[1].as_str()) else { continue; };
174 if objects[key_addr]["type"] == "string" && objects[key_addr]["data"].as_str() == Some(key) {
175 if objects[val_addr]["type"] == "string" {
176 return objects[val_addr]["data"].as_str();
177 } else {
178 break;
179 }
180 }
181 }
182 None
183}