| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- /*
- * Copyright 2017 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- #import <Foundation/Foundation.h>
- #include <atomic>
- #import "Firestore/Source/Util/FSTAssert.h"
- #import "Firestore/Source/Util/FSTDispatchQueue.h"
- NS_ASSUME_NONNULL_BEGIN
- /**
- * removeDelayedCallback is used by FSTDelayedCallback and so we pre-declare it before the rest of
- * the FSTDispatchQueue private interface.
- */
- @interface FSTDispatchQueue ()
- - (void)removeDelayedCallback:(FSTDelayedCallback *)callback;
- @end
- #pragma mark - FSTDelayedCallback
- /**
- * Represents a callback scheduled to be run in the future on an FSTDispatchQueue.
- *
- * It is created via [FSTDelayedCallback createAndScheduleWithQueue].
- *
- * Supports cancellation (via cancel) and early execution (via skipDelay).
- */
- @interface FSTDelayedCallback ()
- @property(nonatomic, strong, readonly) FSTDispatchQueue *queue;
- @property(nonatomic, assign, readonly) FSTTimerID timerID;
- @property(nonatomic, assign, readonly) NSTimeInterval targetTime;
- @property(nonatomic, copy) void (^callback)();
- /** YES if the callback has been run or canceled. */
- @property(nonatomic, getter=isDone) BOOL done;
- /**
- * Creates and returns an FSTDelayedCallback that has been scheduled on the provided queue with the
- * provided delay.
- *
- * @param queue The FSTDispatchQueue to run the callback on.
- * @param timerID A FSTTimerID identifying the type of the delayed callback.
- * @param delay The delay before the callback should be scheduled.
- * @param callback The callback block to run.
- * @return The created FSTDelayedCallback instance.
- */
- + (instancetype)createAndScheduleWithQueue:(FSTDispatchQueue *)queue
- timerID:(FSTTimerID)timerID
- delay:(NSTimeInterval)delay
- callback:(void (^)(void))callback;
- /**
- * Queues the callback to run immediately (if it hasn't already been run or canceled).
- */
- - (void)skipDelay;
- @end
- @implementation FSTDelayedCallback
- - (instancetype)initWithQueue:(FSTDispatchQueue *)queue
- timerID:(FSTTimerID)timerID
- targetTime:(NSTimeInterval)targetTime
- callback:(void (^)(void))callback {
- if (self = [super init]) {
- _queue = queue;
- _timerID = timerID;
- _targetTime = targetTime;
- _callback = callback;
- _done = NO;
- }
- return self;
- }
- + (instancetype)createAndScheduleWithQueue:(FSTDispatchQueue *)queue
- timerID:(FSTTimerID)timerID
- delay:(NSTimeInterval)delay
- callback:(void (^)(void))callback {
- NSTimeInterval targetTime = [[NSDate date] timeIntervalSince1970] + delay;
- FSTDelayedCallback *delayedCallback = [[FSTDelayedCallback alloc] initWithQueue:queue
- timerID:timerID
- targetTime:targetTime
- callback:callback];
- [delayedCallback startWithDelay:delay];
- return delayedCallback;
- }
- /**
- * Starts the timer. This is called immediately after construction by createAndScheduleWithQueue.
- */
- - (void)startWithDelay:(NSTimeInterval)delay {
- dispatch_time_t delayNs = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC));
- dispatch_after(delayNs, self.queue.queue, ^{
- [self.queue enterCheckedOperation:^{
- [self delayDidElapse];
- }];
- });
- }
- - (void)skipDelay {
- [self.queue dispatchAsyncAllowingSameQueue:^{
- [self delayDidElapse];
- }];
- }
- - (void)cancel {
- [self.queue verifyIsCurrentQueue];
- if (!self.isDone) {
- // PORTING NOTE: There's no way to actually cancel the dispatched callback, but it'll be a no-op
- // since we set done to YES.
- [self markDone];
- }
- }
- - (void)delayDidElapse {
- [self.queue verifyIsCurrentQueue];
- if (!self.isDone) {
- [self markDone];
- self.callback();
- }
- }
- /**
- * Marks this delayed callback as done, and notifies the FSTDispatchQueue that it should be removed.
- */
- - (void)markDone {
- self.done = YES;
- [self.queue removeDelayedCallback:self];
- }
- @end
- #pragma mark - FSTDispatchQueue
- @interface FSTDispatchQueue ()
- /**
- * Callbacks scheduled to be queued in the future. Callbacks are automatically removed after they
- * are run or canceled.
- */
- @property(nonatomic, strong, readonly) NSMutableArray<FSTDelayedCallback *> *delayedCallbacks;
- - (instancetype)initWithQueue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER;
- @end
- @implementation FSTDispatchQueue {
- /**
- * Flag set while an FSTDispatchQueue operation is currently executing. Used for assertion
- * sanity-checks.
- */
- std::atomic<bool> _operationInProgress;
- }
- + (instancetype)queueWith:(dispatch_queue_t)dispatchQueue {
- return [[FSTDispatchQueue alloc] initWithQueue:dispatchQueue];
- }
- - (instancetype)initWithQueue:(dispatch_queue_t)queue {
- if (self = [super init]) {
- _operationInProgress = false;
- _queue = queue;
- _delayedCallbacks = [NSMutableArray array];
- }
- return self;
- }
- - (void)verifyIsCurrentQueue {
- FSTAssert([self onTargetQueue],
- @"We are running on the wrong dispatch queue. Expected '%@' Actual: '%@'",
- [self targetQueueLabel], [self currentQueueLabel]);
- FSTAssert(_operationInProgress,
- @"verifyIsCurrentQueue called outside enterCheckedOperation on queue '%@'",
- [self currentQueueLabel]);
- }
- - (void)enterCheckedOperation:(void (^)(void))block {
- FSTAssert(!_operationInProgress,
- @"enterCheckedOperation may not be called when an operation is in progress");
- @try {
- _operationInProgress = true;
- [self verifyIsCurrentQueue];
- block();
- } @finally {
- _operationInProgress = false;
- }
- }
- - (void)dispatchAsync:(void (^)(void))block {
- FSTAssert(![self onTargetQueue] || !_operationInProgress,
- @"dispatchAsync called when we are already running on target dispatch queue '%@'",
- [self targetQueueLabel]);
- dispatch_async(self.queue, ^{
- [self enterCheckedOperation:block];
- });
- }
- - (void)dispatchAsyncAllowingSameQueue:(void (^)(void))block {
- dispatch_async(self.queue, ^{
- [self enterCheckedOperation:block];
- });
- }
- - (void)dispatchSync:(void (^)(void))block {
- FSTAssert(![self onTargetQueue] || !_operationInProgress,
- @"dispatchSync called when we are already running on target dispatch queue '%@'",
- [self targetQueueLabel]);
- dispatch_sync(self.queue, ^{
- [self enterCheckedOperation:block];
- });
- }
- - (FSTDelayedCallback *)dispatchAfterDelay:(NSTimeInterval)delay
- timerID:(FSTTimerID)timerID
- block:(void (^)(void))block {
- // While not necessarily harmful, we currently don't expect to have multiple callbacks with the
- // same timerID in the queue, so defensively reject them.
- FSTAssert(![self containsDelayedCallbackWithTimerID:timerID],
- @"Attempted to schedule multiple callbacks with id %ld", (unsigned long)timerID);
- FSTDelayedCallback *delayedCallback = [FSTDelayedCallback createAndScheduleWithQueue:self
- timerID:timerID
- delay:delay
- callback:block];
- [self.delayedCallbacks addObject:delayedCallback];
- return delayedCallback;
- }
- - (BOOL)containsDelayedCallbackWithTimerID:(FSTTimerID)timerID {
- NSUInteger matchIndex = [self.delayedCallbacks
- indexOfObjectPassingTest:^BOOL(FSTDelayedCallback *obj, NSUInteger idx, BOOL *stop) {
- return obj.timerID == timerID;
- }];
- return matchIndex != NSNotFound;
- }
- - (void)runDelayedCallbacksUntil:(FSTTimerID)lastTimerID {
- dispatch_semaphore_t doneSemaphore = dispatch_semaphore_create(0);
- [self dispatchAsync:^{
- FSTAssert(lastTimerID == FSTTimerIDAll || [self containsDelayedCallbackWithTimerID:lastTimerID],
- @"Attempted to run callbacks until missing timer ID: %ld",
- (unsigned long)lastTimerID);
- [self sortDelayedCallbacks];
- for (FSTDelayedCallback *callback in self.delayedCallbacks) {
- [callback skipDelay];
- if (lastTimerID != FSTTimerIDAll && callback.timerID == lastTimerID) {
- break;
- }
- }
- // Now that the callbacks are queued, we want to enqueue an additional item to release the
- // 'done' semaphore.
- [self dispatchAsyncAllowingSameQueue:^{
- dispatch_semaphore_signal(doneSemaphore);
- }];
- }];
- dispatch_semaphore_wait(doneSemaphore, DISPATCH_TIME_FOREVER);
- }
- // NOTE: For performance we could store the callbacks sorted (e.g. using std::priority_queue),
- // but this sort only happens in tests (if runDelayedCallbacksUntil: is called), and the size
- // is guaranteed to be small since we don't allow duplicate TimerIds (of which there are only 4).
- - (void)sortDelayedCallbacks {
- // We want to run callbacks in the same order they'd run if they ran naturally.
- [self.delayedCallbacks
- sortUsingComparator:^NSComparisonResult(FSTDelayedCallback *a, FSTDelayedCallback *b) {
- return a.targetTime < b.targetTime
- ? NSOrderedAscending
- : a.targetTime > b.targetTime ? NSOrderedDescending : NSOrderedSame;
- }];
- }
- /** Called by FSTDelayedCallback when a callback is run or canceled. */
- - (void)removeDelayedCallback:(FSTDelayedCallback *)callback {
- NSUInteger index = [self.delayedCallbacks indexOfObject:callback];
- FSTAssert(index != NSNotFound, @"Delayed callback not found.");
- [self.delayedCallbacks removeObjectAtIndex:index];
- }
- #pragma mark - Private Methods
- - (NSString *)currentQueueLabel {
- return [NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)];
- }
- - (NSString *)targetQueueLabel {
- return [NSString stringWithUTF8String:dispatch_queue_get_label(self.queue)];
- }
- - (BOOL)onTargetQueue {
- return [[self currentQueueLabel] isEqualToString:[self targetQueueLabel]];
- }
- @end
- NS_ASSUME_NONNULL_END
|