Skip to content

Commit 9b46360

Browse files
Implement garbage collector to speed up everything
Deallocating objc objects is costly, especially deallocating deeply nested MLXMLNodes and MLHandlers (with MLXMLNodes bound to it) can take several minutes in extreme situations, even if reading those objects from database only takes a few milliseconds. We don't want to block the receive queue that long, in fact we don't want to block any thread that long. This commit therefore moves the deallocation into a custom garbage collector. The garbage collector takes strong references to every object given to it by calling [MLDelayedDealloc delayFor:] and moves deallocation into a background QOS serial queue that periodically deallocates all objects only referenced by the garbage collector itself. We automatically add all MLXMLNodes and MLHandlers to the garbage collector and also add the list of stanzas and iq handlers when clearing the smacks queue or invalidating iq handlers (although this shouldn't be needed since we automatically add all MLXMLNodes and MLHandlers to th garbage collector inside their init methods). Other objects can be added to the garbage collector as needed or the NSObject init method swizzled to unconditionally add all objects to the garbage collector in the future.
1 parent bfd3851 commit 9b46360

File tree

6 files changed

+314
-50
lines changed

6 files changed

+314
-50
lines changed

Monal/Classes/MLDelayedDealloc.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// MLDelayedDealloc.h
3+
// monalxmpp
4+
//
5+
// Created by Thilo Molitor on 04.03.26.
6+
// Copyright © 2026 Monal.im. All rights reserved.
7+
//
8+
9+
NS_ASSUME_NONNULL_BEGIN
10+
11+
@interface MLDelayedDealloc : NSObject
12+
13+
+(void) delayFor:(id) obj;
14+
15+
@end
16+
17+
NS_ASSUME_NONNULL_END

Monal/Classes/MLDelayedDealloc.m

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
//
2+
// MLDelayedDealloc.m
3+
// monalxmpp
4+
//
5+
// Created by Thilo Molitor on 04.03.26.
6+
// Copyright © 2026 Monal.im. All rights reserved.
7+
//
8+
9+
//#define DEBUG_DEALLOC_DEBUGGING
10+
11+
#import <Foundation/Foundation.h>
12+
#import <objc/runtime.h>
13+
#import <monalxmpp/MLConstants.h>
14+
#import <monalxmpp/MLDelayedDealloc.h>
15+
16+
static NSMutableArray* _deallocList;
17+
static dispatch_source_t _timer;
18+
static dispatch_queue_t _queue;
19+
20+
#ifdef DEBUG
21+
static NSString* _oldDebugOutput = nil;
22+
static NSArray* ContainerChildren(id obj);
23+
static NSMutableSet* IvarChildren(id obj);
24+
static NSString* NodeName(id obj);
25+
static void DumpMemoryGraph(NSSet* objects);
26+
#endif
27+
28+
@implementation MLDelayedDealloc
29+
30+
+(void) initialize
31+
{
32+
_deallocList = [NSMutableArray new];
33+
34+
//configure timer
35+
_queue = dispatch_queue_create("im.monal.dealloc.timer", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_BACKGROUND, 0));
36+
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _queue);
37+
dispatch_source_set_timer(_timer,
38+
dispatch_time(DISPATCH_TIME_NOW, 0),
39+
500 * NSEC_PER_MSEC, //500ms period
40+
100 * NSEC_PER_MSEC); //100ms leeway
41+
42+
//periodically dealloc objects
43+
dispatch_source_set_event_handler(_timer, ^{
44+
NSMutableArray* deallocCopy = nil;
45+
//cleanup _deallocList, make this section short to not block other threads
46+
@synchronized(_deallocList) {
47+
deallocCopy = [_deallocList mutableCopy];
48+
[_deallocList removeAllObjects]; //this won't deallocate anything, because we still reference everything in deallocCopy
49+
}
50+
51+
NSUInteger count = [deallocCopy count];
52+
if(count > 0)
53+
{
54+
//requeue all entries that are still used by somebody else (retainCount == 1 being only us holding the object in deallocCopy)
55+
NSUInteger requeuedCount = 0;
56+
@autoreleasepool {
57+
NSMutableSet* retainedSet = [NSMutableSet new];
58+
for(id entry in deallocCopy)
59+
{
60+
NSUInteger retainCount = CFGetRetainCount((__bridge CFTypeRef)entry);
61+
if(retainCount > 1 && retainCount < 65536) //only requeue if still used but not due to some tagged pointer foo etc.
62+
{
63+
[self delayFor:entry];
64+
count--;
65+
requeuedCount++;
66+
#ifdef DEBUG
67+
[retainedSet addObject:entry];
68+
//DDLogDebug(@"Requeued deallocation with retain count %lu (%lu) of: %p %@", retainCount, CFGetRetainCount((__bridge CFTypeRef)entry), entry, entry);
69+
#endif
70+
}
71+
}
72+
#ifdef DEBUG
73+
DumpMemoryGraph(retainedSet);
74+
#endif
75+
retainedSet = nil;
76+
}
77+
78+
//check if, after removing still retained entries, count is still > 0 and start deallocation, if so
79+
if(count > 0)
80+
{
81+
//for(id entry in deallocCopy)
82+
// DDLogVerbose(@"Deallocating: %@", NodeName(entry));
83+
DDLogVerbose(@"Deallocating %lu objects, %lu still used objects requeued for later deallocation...", count, requeuedCount);
84+
NSDate* start = [NSDate date];
85+
deallocCopy = nil;
86+
NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:start];
87+
if(elapsed > 1.0)
88+
DDLogWarn(@"Done deallocating %lu objects in %.3fs (%lu still used objects requeued for later deallocation)...", count, elapsed, requeuedCount);
89+
else
90+
DDLogVerbose(@"Done deallocating %lu objects in %.3fs (%lu still used objects requeued for later deallocation)...", count, elapsed, requeuedCount);
91+
}
92+
}
93+
});
94+
dispatch_resume(_timer);
95+
}
96+
97+
+(void) delayFor:(id) obj
98+
{
99+
if(obj == nil)
100+
return;
101+
@synchronized(_deallocList) {
102+
[_deallocList addObject:obj];
103+
}
104+
}
105+
106+
@end
107+
108+
//****************************** FOR DEBUGGING ******************************
109+
#ifdef DEBUG
110+
static inline __attribute__((always_inline)) NSArray* ContainerChildren(id obj)
111+
{
112+
if([obj isKindOfClass:[NSArray class]])
113+
return obj;
114+
if([obj isKindOfClass:[NSSet class]])
115+
return [obj allObjects];
116+
if([obj isKindOfClass:[NSDictionary class]])
117+
return [[(NSDictionary*)obj allKeys] arrayByAddingObjectsFromArray:[(NSDictionary*)obj allValues]];
118+
return @[];
119+
}
120+
121+
static inline __attribute__((always_inline)) BOOL slotMatchesLayout(const uint8_t* layout, size_t slotIndex)
122+
{
123+
if(!layout)
124+
return NO;
125+
size_t cursor = 0;
126+
while(*layout)
127+
{
128+
uint8_t skip = *layout++;
129+
uint8_t count = *layout++;
130+
// #ifdef DEBUG_DEALLOC_DEBUGGING
131+
// DDLogVerbose(@"slotIndex=%d, skip=%d, count=%d", (int)slotIndex, (int)skip, (int)count);
132+
// #endif
133+
cursor += skip;
134+
if(slotIndex >= cursor && slotIndex < cursor + count)
135+
return YES;
136+
cursor += count;
137+
}
138+
return NO;
139+
}
140+
141+
static inline __attribute__((always_inline)) BOOL propertyIsWeak(objc_property_t prop)
142+
{
143+
if(!prop)
144+
return NO;
145+
const char* attrs = property_getAttributes(prop);
146+
if(!attrs)
147+
return NO;
148+
// #ifdef DEBUG_DEALLOC_DEBUGGING
149+
// DDLogVerbose(@"Property attributes: %s", attrs);
150+
// #endif
151+
BOOL retval = strstr(attrs, ",W") != NULL;
152+
return retval;
153+
}
154+
155+
static inline __attribute__((always_inline)) NSMutableSet* IvarChildren(id obj)
156+
{
157+
NSMutableSet* children = [NSMutableSet new];
158+
[children addObjectsFromArray:ContainerChildren(obj)]; //if we are a container
159+
if([children count] == 0) //if we aren't a container
160+
{
161+
Class cls = object_getClass(obj);
162+
while(cls)
163+
{
164+
unsigned int count = 0;
165+
Ivar* ivars = class_copyIvarList(cls, &count);
166+
//const uint8_t* strongLayout = class_getIvarLayout(cls);
167+
const uint8_t* weakLayout = class_getWeakIvarLayout(cls);
168+
for(unsigned int i = 0; i < count; i++)
169+
{
170+
const char* ivarName = ivar_getName(ivars[i]);
171+
const char* ivarType = ivar_getTypeEncoding(ivars[i]);
172+
if(ivarType[0] == '@')
173+
{
174+
//check ivar slot for weakness
175+
ptrdiff_t offset = ivar_getOffset(ivars[i]);
176+
size_t slotIndex = offset / sizeof(void*);
177+
//DDLogVerbose(@"Checking strong for %d: %s", (int)slotIndex, ivarName);
178+
//BOOL isStrong = slotMatchesLayout(strongLayout, slotIndex);
179+
BOOL isWeak = slotMatchesLayout(weakLayout, slotIndex);
180+
181+
//check property wrapping this ivar for weakness
182+
objc_property_t prop = class_getProperty(cls, ivarName);
183+
if(!prop)
184+
if(ivarName[0] == '_') //ivars usually start with '_' while properties don't
185+
prop = class_getProperty(cls, ivarName + 1);
186+
if(propertyIsWeak(prop))
187+
isWeak = YES;
188+
189+
id ivarValue = object_getIvar(obj, ivars[i]);
190+
#ifdef DEBUG_DEALLOC_DEBUGGING
191+
if(ivarValue && isWeak)
192+
DDLogError(@"WEAK IVAR[%u](%s::%s): %@", i, ivarType, ivarName, ivarValue);
193+
#endif
194+
if(ivarValue && !isWeak)
195+
{
196+
#ifdef DEBUG_DEALLOC_DEBUGGING
197+
DDLogError(@"STRONG IVAR[%u](%s::%s): %@", i, ivarType, ivarName, ivarValue);
198+
#endif
199+
[children addObjectsFromArray:@[ivarValue]];
200+
[children addObjectsFromArray:ContainerChildren(ivarValue)];
201+
}
202+
}
203+
}
204+
free(ivars);
205+
cls = class_getSuperclass(cls);
206+
}
207+
}
208+
return children;
209+
}
210+
211+
static inline NSString* NodeName(id obj)
212+
{
213+
return [NSString stringWithFormat:@"%s_%p_%lu --> %@", class_getName(object_getClass(obj)), obj, CFGetRetainCount((__bridge CFTypeRef)obj), obj];
214+
}
215+
216+
static inline __attribute__((always_inline)) __unused void DumpMemoryGraph(NSMutableSet* objects)
217+
{
218+
@autoreleasepool {
219+
NSDate* start = [NSDate date];
220+
NSMutableSet* referenced = [NSMutableSet set];
221+
for(id obj in objects)
222+
{
223+
NSMutableSet* targets = [NSMutableSet set];
224+
NSMutableSet* printableTargets = [NSMutableSet set];
225+
for(id child in IvarChildren(obj))
226+
if(child != obj && [objects containsObject:child])
227+
{
228+
[targets addObject:child];
229+
[printableTargets addObject:NodeName(child)];
230+
}
231+
#ifdef DEBUG_DEALLOC_DEBUGGING
232+
DDLogDebug(@"TARGETS OF %@: %@", NodeName(obj), printableTargets);
233+
#endif
234+
[referenced unionSet:targets];
235+
}
236+
[objects minusSet:referenced];
237+
238+
NSMutableString* output = [NSMutableString new];
239+
[output appendString:@"\n// Roots:\n"];
240+
for(id root in objects)
241+
[output appendFormat:@"// %@\n", NodeName(root)];
242+
243+
if(![output isEqualToString:_oldDebugOutput])
244+
DDLogDebug(@"Still used root nodes (calculated in %.3fs): %@", [[NSDate date] timeIntervalSinceDate:start], output);
245+
_oldDebugOutput = output;
246+
}
247+
}
248+
#endif

Monal/Classes/MLHandler.m

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#import "MLConstants.h"
1111
#import "MLHandler.h"
1212
#import "HelperTools.h"
13+
#import "MLDelayedDealloc.h"
1314

1415
#define HANDLER_VERSION 1
1516

@@ -30,6 +31,10 @@ @implementation MLHandler
3031
-(instancetype) init
3132
{
3233
self = [super init];
34+
35+
//always delay own deallocation to not block receive/send queues or any other queues
36+
[MLDelayedDealloc delayFor:self];
37+
3338
return self;
3439
}
3540

@@ -164,7 +169,7 @@ -(void) encodeWithCoder:(NSCoder*) coder
164169

165170
-(instancetype) initWithCoder:(NSCoder*) coder
166171
{
167-
self = [super init];
172+
self = [self init];
168173
_internalData = [coder decodeObjectForKey:@"internalData"];
169174
_invalidated = [coder decodeBoolForKey:@"invalidated"];
170175
return self;

Monal/Classes/MLXMLNode.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#import "XMPPMessage.h"
1616
#import "XMPPPresence.h"
1717
#import "XMPPDataForm.h"
18+
#import "MLDelayedDealloc.h"
1819

1920
@import UIKit.UIApplication;
2021

@@ -107,6 +108,9 @@ -(void) internalInit
107108
self.cache = [NSCache new];
108109
self.queryEntryCache = [NSCache new];
109110
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMemoryPressureNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
111+
112+
//always delay own deallocation to not block receive/send queues or any other queues
113+
[MLDelayedDealloc delayFor:self];
110114
}
111115

112116
-(id) init

0 commit comments

Comments
 (0)