Skip to content

Commit 26fdac2

Browse files
committed
optimize guard performance
1 parent 112ffc6 commit 26fdac2

File tree

1 file changed

+161
-26
lines changed
  • crates/emmylua_code_analysis/src/semantic

1 file changed

+161
-26
lines changed

crates/emmylua_code_analysis/src/semantic/guard.rs

Lines changed: 161 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,54 +4,63 @@ use crate::{InferFailReason, LuaTypeDeclId};
44

55
pub type InferGuardRef = Rc<InferGuard>;
66

7-
/// Guard to prevent infinite recursion
8-
/// Some type may reference itself, so we need to check if we have already inferred this type
7+
/// Guard to prevent infinite recursion with optimized lazy allocation
98
///
10-
/// This guard supports inheritance through Rc parent chain, allowing child guards to see
11-
/// parent's visited types while maintaining their own independent tracking for branch protection.
9+
/// This guard uses a lazy allocation strategy:
10+
/// - Fork is zero-cost (no HashSet allocation)
11+
/// - `current` HashSet is only created when needed (write-on-create)
12+
/// - Most child guards never allocate memory if they only read from parents
13+
///
14+
/// # Memory Layout
15+
/// ```text
16+
/// Root: current=[A, B] parent=None
17+
/// |
18+
/// +-- Child1: current=None parent=Root (no allocation!)
19+
/// | |
20+
/// | +-- GrandChild: current=[C] parent=Child1 (allocated on first write)
21+
/// |
22+
/// +-- Child2: current=None parent=Root (no allocation!)
23+
/// ```
1224
#[derive(Debug, Clone)]
1325
pub struct InferGuard {
14-
/// Current level's visited types
15-
current: RefCell<HashSet<LuaTypeDeclId>>,
26+
/// Current level's visited types (lazily allocated)
27+
/// Only created when we need to add a new type not in parent chain
28+
current: RefCell<Option<HashSet<LuaTypeDeclId>>>,
1629
/// Parent guard (shared reference)
1730
parent: Option<Rc<InferGuard>>,
1831
}
1932

2033
impl InferGuard {
2134
pub fn new() -> Rc<Self> {
2235
Rc::new(Self {
23-
current: RefCell::new(HashSet::default()),
36+
current: RefCell::new(None),
2437
parent: None,
2538
})
2639
}
2740

2841
/// Create a child guard that inherits from parent
29-
/// This allows branching while preventing infinite recursion across the entire call stack
42+
///
43+
/// Zero-cost operation: no HashSet allocation until first write
3044
pub fn fork(self: &Rc<Self>) -> Rc<Self> {
3145
Rc::new(Self {
32-
current: RefCell::new(HashSet::default()),
46+
current: RefCell::new(None), // Lazy allocation
3347
parent: Some(Rc::clone(self)),
3448
})
3549
}
3650

37-
/// Create a child guard from a non-Rc guard
38-
/// This is a convenience method for when you have a stack-allocated guard
39-
pub fn fork_owned(&self) -> Self {
40-
Self {
41-
current: RefCell::new(HashSet::default()),
42-
parent: self.parent.clone(),
43-
}
44-
}
45-
4651
/// Check if a type has been visited in current branch or any parent
4752
pub fn check(&self, type_id: &LuaTypeDeclId) -> Result<(), InferFailReason> {
4853
// Check in all parent levels first
4954
if self.contains_in_parents(type_id) {
5055
return Err(InferFailReason::RecursiveInfer);
5156
}
5257

53-
// Check in current level
54-
let mut current = self.current.borrow_mut();
58+
// Check in current level (if exists)
59+
let mut current_opt = self.current.borrow_mut();
60+
61+
// Lazy allocation: create HashSet only when needed
62+
let current = current_opt.get_or_insert_with(HashSet::default);
63+
5564
if current.contains(type_id) {
5665
return Err(InferFailReason::RecursiveInfer);
5766
}
@@ -65,8 +74,10 @@ impl InferGuard {
6574
fn contains_in_parents(&self, type_id: &LuaTypeDeclId) -> bool {
6675
let mut current_parent = self.parent.as_ref();
6776
while let Some(parent) = current_parent {
68-
if parent.current.borrow().contains(type_id) {
69-
return true;
77+
if let Some(ref set) = *parent.current.borrow() {
78+
if set.contains(type_id) {
79+
return true;
80+
}
7081
}
7182
current_parent = parent.parent.as_ref();
7283
}
@@ -75,20 +86,27 @@ impl InferGuard {
7586

7687
/// Check if a type has been visited (without modifying the guard)
7788
pub fn contains(&self, type_id: &LuaTypeDeclId) -> bool {
78-
self.current.borrow().contains(type_id) || self.contains_in_parents(type_id)
89+
// Check current level
90+
if let Some(ref set) = *self.current.borrow() {
91+
if set.contains(type_id) {
92+
return true;
93+
}
94+
}
95+
// Check parents
96+
self.contains_in_parents(type_id)
7997
}
8098

8199
/// Get the depth of current level
82100
pub fn current_depth(&self) -> usize {
83-
self.current.borrow().len()
101+
self.current.borrow().as_ref().map_or(0, |set| set.len())
84102
}
85103

86104
/// Get the total depth of the entire guard chain
87105
pub fn total_depth(&self) -> usize {
88-
let mut depth = self.current.borrow().len();
106+
let mut depth = self.current_depth();
89107
let mut current_parent = self.parent.as_ref();
90108
while let Some(parent) = current_parent {
91-
depth += parent.current.borrow().len();
109+
depth += parent.current_depth();
92110
current_parent = parent.parent.as_ref();
93111
}
94112
depth
@@ -104,4 +122,121 @@ impl InferGuard {
104122
}
105123
level
106124
}
125+
126+
/// Check if current level has allocated memory
127+
/// Useful for debugging and performance analysis
128+
#[cfg(test)]
129+
pub fn has_allocated(&self) -> bool {
130+
self.current.borrow().is_some()
131+
}
132+
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use super::*;
137+
138+
#[test]
139+
fn test_lazy_allocation() {
140+
let root = InferGuard::new();
141+
assert!(!root.has_allocated(), "New guard should not allocate");
142+
143+
// Fork should NOT allocate
144+
let child = root.fork();
145+
assert!(!child.has_allocated(), "Fork should not allocate memory");
146+
147+
// Check on child should allocate
148+
let type_b = LuaTypeDeclId::new("TestTypeB");
149+
child.check(&type_b).unwrap();
150+
assert!(
151+
child.has_allocated(),
152+
"Check should trigger lazy allocation"
153+
);
154+
assert!(!root.has_allocated(), "Root should not be affected");
155+
}
156+
157+
#[test]
158+
fn test_fork_without_write() {
159+
let root = InferGuard::new();
160+
let type_a = LuaTypeDeclId::new("TestTypeA");
161+
root.check(&type_a).unwrap();
162+
163+
// Create multiple forks
164+
let child1 = root.fork();
165+
let child2 = root.fork();
166+
let grandchild = child1.fork();
167+
168+
// None of them should allocate if they don't write
169+
assert!(!child1.has_allocated());
170+
assert!(!child2.has_allocated());
171+
assert!(!grandchild.has_allocated());
172+
173+
// They should still see parent's types
174+
assert!(child1.contains(&type_a));
175+
assert!(child2.contains(&type_a));
176+
assert!(grandchild.contains(&type_a));
177+
}
178+
179+
#[test]
180+
fn test_recursive_detection() {
181+
let root = InferGuard::new();
182+
let type_a = LuaTypeDeclId::new("TestTypeA");
183+
184+
// First check should succeed
185+
assert!(root.check(&type_a).is_ok());
186+
187+
// Second check should fail (recursive)
188+
assert!(root.check(&type_a).is_err());
189+
}
190+
191+
#[test]
192+
fn test_parent_chain_detection() {
193+
let root = InferGuard::new();
194+
let type_a = LuaTypeDeclId::new("TestTypeA");
195+
let type_b = LuaTypeDeclId::new("TestTypeB");
196+
197+
root.check(&type_a).unwrap();
198+
199+
let child = root.fork();
200+
201+
// Child should detect type_a from parent
202+
assert!(child.check(&type_a).is_err());
203+
204+
// But can add type_b
205+
assert!(child.check(&type_b).is_ok());
206+
207+
let grandchild = child.fork();
208+
209+
// Grandchild should detect both
210+
assert!(grandchild.check(&type_a).is_err());
211+
assert!(grandchild.check(&type_b).is_err());
212+
}
213+
214+
#[test]
215+
fn test_memory_efficiency() {
216+
let root = InferGuard::new();
217+
let type_a = LuaTypeDeclId::new("TestTypeA");
218+
root.check(&type_a).unwrap();
219+
220+
// Create a deep fork chain
221+
let mut guards = vec![root];
222+
for _ in 0..10 {
223+
let child = guards.last().unwrap().fork();
224+
guards.push(child);
225+
}
226+
227+
// Only root and last guard (if it wrote) should have allocation
228+
// All intermediate forks should have NO allocation
229+
for (i, guard) in guards.iter().enumerate() {
230+
if i == 0 {
231+
assert!(guard.has_allocated(), "Root should be allocated");
232+
} else if i < guards.len() - 1 {
233+
// Intermediate nodes that didn't write
234+
assert!(
235+
!guard.has_allocated(),
236+
"Intermediate fork {} should not allocate",
237+
i
238+
);
239+
}
240+
}
241+
}
107242
}

0 commit comments

Comments
 (0)