Skip to main content

mlua/luau/
heap_dump.rs

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/// Represents a heap dump of a Luau memory state.
11#[cfg(any(feature = "luau", doc))]
12#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
13pub struct HeapDump {
14    data: Json<'static>, // refers to the contents of `buf`
15    buf: Box<str>,
16}
17
18impl HeapDump {
19    /// Dumps the current Lua heap state.
20    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    /// Returns the raw JSON representation of the heap dump.
54    ///
55    /// The JSON structure is an internal detail and may change in future versions.
56    #[doc(hidden)]
57    pub fn to_json(&self) -> &str {
58        &self.buf
59    }
60
61    /// Returns the total size of the Lua heap in bytes.
62    pub fn size(&self) -> u64 {
63        self.data["stats"]["size"].as_u64().unwrap_or_default()
64    }
65
66    /// Returns a mapping from object type to (count, total size in bytes).
67    ///
68    /// If `category` is provided, only objects in that category are considered. An unknown category
69    /// yields an empty map.
70    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            // If we cannot find the category, return empty result
77            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    /// Returns a mapping from category name to total size in bytes.
95    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    /// Returns a mapping from userdata type to (count, total size in bytes).
108    ///
109    /// If `category` is provided, only objects in that category are considered. An unknown category
110    /// yields an empty map.
111    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            // If we cannot find the category, return empty result
121            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            // Determine userdata type from metatable
138            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    /// Finds the category ID for a given category name.
150    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
161/// Updates the size mapping for a given key.
162fn 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
168/// Retrieves the value associated with a given `key` from a Lua table `tbl`.
169fn 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}