Skip to content

Commit cceeb69

Browse files
committed
use min heap for run loop timers
1 parent be1b999 commit cceeb69

4 files changed

Lines changed: 75 additions & 74 deletions

File tree

Headers/GNUstepBase/GSMinHeap.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ GS_EXPORT_CLASS
101101
*/
102102
- (id) pop;
103103

104+
/** Removes the first object from the heap and returns it. Returns nil
105+
* if the heap was already empty. The returned object is not autoreleased.
106+
*/
107+
- (id) popRetained NS_RETURNS_RETAINED;
108+
104109
/** Adds obj to the heap and returns YES on success. May return NO on failure
105110
* (if obj was nil or if there is insufficient memory for the heap to grow).
106111
* The heap takes ownership of (retains) obj on success, but not on failure.

Source/Additions/GSMinHeap.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,11 @@ - (id) pop
360360
return AUTORELEASE(heap_pop(internal));
361361
}
362362

363+
- (id) popRetained
364+
{
365+
return heap_pop(internal);
366+
}
367+
363368
- (BOOL) push: (id)obj
364369
{
365370
if (obj != nil)

Source/GSRunLoopCtxt.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ typedef struct{
5757
NSString *mode; /** The mode for this context. */
5858
GSIArray performers; /** The actions to perform regularly. */
5959
unsigned maxPerformers;
60-
GSIArray timers;
61-
unsigned maxTimers;
6260
GSIArray watchers; /** The inputs set for the runloop mode */
6361
unsigned maxWatchers;
62+
GSMinHeap *timers;
63+
unsigned maxTimers;
6464
@protected
6565
GSIArray _trigger; // Watchers to trigger unconditionally.
6666
int fairStart; // For trying to ensure fair handling.

Source/NSRunLoop.m

Lines changed: 63 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -887,7 +887,7 @@ - (void) addTimer: (NSTimer*)timer
887887
{
888888
const void *loop = (const void*)self;
889889
GSRunLoopCtxt *context;
890-
GSIArray timers;
890+
GSMinHeap *timers;
891891
unsigned i;
892892

893893
if ([timer isKindOfClass: [NSTimer class]] == NO
@@ -903,6 +903,12 @@ - (void) addTimer: (NSTimer*)timer
903903
format: @"[%@-%@] timer already scheduled in another runloop",
904904
NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
905905
}
906+
if (255 == timer->_scheduled)
907+
{
908+
[NSException raise: NSInvalidArgumentException
909+
format: @"[%@-%@] timer already scheduled in too many modes",
910+
NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
911+
}
906912
if ([mode isKindOfClass: [NSString class]] == NO)
907913
{
908914
[NSException raise: NSInvalidArgumentException
@@ -927,40 +933,22 @@ - (void) addTimer: (NSTimer*)timer
927933
{
928934
/* Timer was already scheduled: check whether it is in this mode.
929935
*/
930-
i = GSIArrayCount(timers);
931-
while (i-- > 0)
936+
if ([timers containsObjectIdenticalTo: timer])
932937
{
933-
if (timer == GSIArrayItemAtIndex(timers, i).obj)
934-
{
935-
return; /* Timer already present */
936-
}
938+
return; /* Timer already present in this mode */
937939
}
938940
}
939941

940-
/* Count scheduling of timer (skip counts too large to fit field).
942+
/* Timers can be scheduled in more than one mode, and if a timer fires
943+
* and repeats it will have updated its fire date. That will leave it
944+
* incorrectly positioned in the min heap in other modes. To handle
945+
* that we must track whether it is scheduled in more than one mode to
946+
* know if we need to check other modes for repositionng.
941947
*/
942-
if (timer->_scheduled < 255)
943-
{
944-
timer->_scheduled++;
945-
}
948+
timer->_scheduled++;
949+
[timers push: timer];
946950

947-
/*
948-
* NB. A previous version of the timer code maintained an ordered
949-
* array on the theory that we could improve performance by only
950-
* checking the first few timers (up to the first one whose fire
951-
* date is in the future) each time -limitDateForMode: is called.
952-
* The problem with this was that it's possible for one timer to
953-
* be added in multiple modes (or to different run loops) and for
954-
* a repeated timer this could mean that the firing of the timer
955-
* in one mode/loop adjusts its date ... without changing the
956-
* ordering of the timers in the other modes/loops which contain
957-
* the timer. When the ordering of timers in an array was broken
958-
* we could get delays in processing timeouts, so we reverted to
959-
* simply having timers in an unordered array and checking them
960-
* all each time -limitDateForMode: is called.
961-
*/
962-
GSIArrayAddItem(timers, (GSIArrayItem)((id)timer));
963-
i = GSIArrayCount(timers);
951+
i = [timers count];
964952
if (i % 1000 == 0 && i > context->maxTimers)
965953
{
966954
context->maxTimers = i;
@@ -1029,20 +1017,14 @@ - (NSDate*) _limitDateForContext: (GSRunLoopCtxt *)context
10291017
{
10301018
NSDate *when = nil;
10311019
NSAutoreleasePool *arp = [NSAutoreleasePool new];
1032-
GSIArray timers = context->timers;
1020+
GSMinHeap *timers = context->timers;
10331021
NSTimeInterval now;
10341022
NSDate *earliest;
10351023
NSDate *d;
10361024
NSTimer *t;
10371025
NSTimeInterval ti;
1038-
NSTimeInterval ei;
1039-
unsigned c;
1040-
unsigned i;
10411026

1042-
ei = 0.0; // Only needed to avoid compiler warning
1043-
1044-
/*
1045-
* Save current time so we don't keep redoing system call to
1027+
/* Save current time so we don't keep redoing system call to
10461028
* get it and so that we check timer fire dates against a known
10471029
* value at the point when the method was called.
10481030
* If we refetched the date after firing each timer, the time
@@ -1054,67 +1036,76 @@ - (NSDate*) _limitDateForContext: (GSRunLoopCtxt *)context
10541036

10551037
/* Fire the oldest/first valid timer whose fire date has passed
10561038
* and fire it.
1057-
* We fire timers in the order in which they were added to the
1058-
* run loop rather than in date order. This prevents code
1059-
* from blocking other timers by adding timers whose fire date
1060-
* is some time in the past... we guarantee fair handling.
10611039
*/
1062-
c = GSIArrayCount(timers);
1063-
for (i = 0; i < c; i++)
1040+
t = [timers peek];
1041+
while (t != nil)
10641042
{
1065-
t = GSIArrayItemAtIndex(timers, i).obj;
1066-
if (timerInvalidated(t) == NO)
1043+
if (timerInvalidated(t))
1044+
{
1045+
t = [timers next];
1046+
}
1047+
else
10671048
{
10681049
d = timerDate(t);
10691050
ti = [d timeIntervalSinceReferenceDate];
10701051
if (ti < now)
10711052
{
1072-
GSIArrayRemoveItemAtIndexNoRelease(timers, i);
1053+
t = [timers popRetained];
10731054
[t fire];
10741055
GSPrivateNotifyASAP(_currentMode);
10751056
IF_NO_ARC([arp emptyPool];)
10761057
if (updateTimer(t, d, now) == YES)
10771058
{
1078-
/* Updated ... replace in array.
1079-
*/
1080-
GSIArrayAddItemNoRetain(timers,
1081-
(GSIArrayItem)((id)t));
1082-
}
1083-
else
1084-
{
1085-
/* The timer was invalidated, so we can
1086-
* release it as we aren't putting it back
1087-
* in the array.
1059+
/* Updated ... replace in heap.
10881060
*/
1089-
RELEASE(t);
1061+
[timers push: t];
1062+
if (t->_scheduled > 1)
1063+
{
1064+
uint8_t n = t->_scheduled - 1;
1065+
1066+
/* The timer was scheduled in more than one mode,
1067+
* so we must reposition it (for the new fire date)
1068+
* in any other mode it is in.
1069+
*/
1070+
GS_FOR_IN(NSString*, m, _contextMap)
1071+
{
1072+
GSRunLoopCtxt *c = NSMapGet(_contextMap, m);
1073+
1074+
if (c != context)
1075+
{
1076+
if ([context->timers repositionObject: t] != nil)
1077+
{
1078+
if (--n == 0)
1079+
{
1080+
break;
1081+
}
1082+
}
1083+
}
1084+
}
1085+
GS_END_FOR(_contextMap)
1086+
}
10901087
}
1091-
break;
1088+
RELEASE(t);
10921089
}
1090+
break;
10931091
}
10941092
}
10951093

10961094
/* Now, find the earliest remaining timer date while removing
1097-
* any invalidated timers. We iterate from the end of the
1098-
* array to minimise the amount of array alteration needed.
1095+
* any invalidated timers.
10991096
*/
11001097
earliest = nil;
1101-
i = GSIArrayCount(timers);
1102-
while (i-- > 0)
1098+
t = [timers peek];
1099+
while (t != nil)
11031100
{
1104-
t = GSIArrayItemAtIndex(timers, i).obj;
1105-
if (timerInvalidated(t) == YES)
1101+
if (timerInvalidated(t))
11061102
{
1107-
GSIArrayRemoveItemAtIndex(timers, i);
1103+
t = [timers next];
11081104
}
11091105
else
11101106
{
1111-
d = timerDate(t);
1112-
ti = [d timeIntervalSinceReferenceDate];
1113-
if (earliest == nil || ti < ei)
1114-
{
1115-
earliest = d;
1116-
ei = ti;
1117-
}
1107+
earliest = timerDate(t);
1108+
break;
11181109
}
11191110
}
11201111
[arp drain];

0 commit comments

Comments
 (0)