|
67 | 67 | #import <pthread.h> |
68 | 68 | #import <SPMySQL/SPMySQL.h> |
69 | 69 |
|
| 70 | +#include <libkern/OSAtomic.h> |
| 71 | + |
| 72 | +typedef struct { |
| 73 | + NSUInteger query; |
| 74 | + NSUInteger total; |
| 75 | +} QueryProgress; |
| 76 | + |
| 77 | +typedef void (^QueryProgressHandler)(QueryProgress *); |
| 78 | + |
| 79 | +/** |
| 80 | + * This class is used to decouple the background thread running the SQL queries |
| 81 | + * from the main thread displaying the progress. |
| 82 | + * |
| 83 | + * It ensures that |
| 84 | + * - the background thread will not get blocked because it has to wait for the UI update to complete, |
| 85 | + * - the main thread does not have to process more progress updates than it can handle and |
| 86 | + * - the UI is updated at most once for each query (important with slow queries) |
| 87 | + * |
| 88 | + * This has quite a significant performance impact: |
| 89 | + * - Both threads synchronized on progress update => |
| 90 | + * running 2500 queries takes about 157 seconds wall time (SP reported time: 10.5s) and the UI is updated 2500 times |
| 91 | + * - With this class decoupling them => |
| 92 | + * running 2500 queries takes about 1 second wall time (SP reported time: 605ms) and the UI is updated 80 times |
| 93 | + */ |
| 94 | +@interface SPQueryProgressUpdateDecoupling : NSObject |
| 95 | +{ |
| 96 | + QueryProgressHandler updateHandler; |
| 97 | + |
| 98 | + volatile BOOL dirtyMarker; |
| 99 | + volatile OSSpinLock qpLock; |
| 100 | + volatile QueryProgress _queryProgress; |
| 101 | +} |
| 102 | +@property(atomic,assign) QueryProgress queryProgress; |
| 103 | +- (instancetype)initWithBlock:(QueryProgressHandler)anUpdateHandler; |
| 104 | +- (void)_updateProgress; |
| 105 | +@end |
| 106 | + |
| 107 | + |
70 | 108 | @interface SPCustomQuery () |
71 | 109 |
|
72 | 110 | - (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column preserveNULLs:(BOOL)preserveNULLs asPreview:(BOOL)asPreview; |
@@ -656,13 +694,18 @@ - (void)performQueriesTask:(NSDictionary *)taskArguments |
656 | 694 | taskButtonString = (queryCount > 1)? NSLocalizedString(@"Stop queries", @"Stop queries string") : NSLocalizedString(@"Stop query", @"Stop query string"); |
657 | 695 | [tableDocumentInstance enableTaskCancellationWithTitle:taskButtonString callbackObject:nil callbackFunction:NULL]; |
658 | 696 |
|
| 697 | + SPQueryProgressUpdateDecoupling *progressUpdater = [[SPQueryProgressUpdateDecoupling alloc] initWithBlock:^(QueryProgress *qp) { |
| 698 | + NSString *taskString = [NSString stringWithFormat:NSLocalizedString(@"Running query %ld of %lu...", @"Running multiple queries string"), (long)(qp->query+1), (unsigned long)(qp->total)]; |
| 699 | + [tableDocumentInstance setTaskDescription:taskString]; |
| 700 | + [errorText setString:taskString]; |
| 701 | + }]; |
| 702 | + |
659 | 703 | // Perform the supplied queries in series |
660 | 704 | for ( i = 0 ; i < queryCount ; i++ ) { |
661 | 705 |
|
662 | 706 | if (i > 0) { |
663 | | - NSString *taskString = [NSString stringWithFormat:NSLocalizedString(@"Running query %ld of %lu...", @"Running multiple queries string"), (long)(i+1), (unsigned long)queryCount]; |
664 | | - [[tableDocumentInstance onMainThread] setTaskDescription:taskString]; |
665 | | - [[errorText onMainThread] setString:taskString]; |
| 707 | + QueryProgress qp = { .query = i, .total = queryCount}; |
| 708 | + [progressUpdater setQueryProgress:qp]; |
666 | 709 | } |
667 | 710 |
|
668 | 711 | NSString *query = [NSArrayObjectAtIndex(queries, i) stringByTrimmingCharactersInSet:whitespaceAndNewlineSet]; |
@@ -811,6 +854,8 @@ - (void)performQueriesTask:(NSDictionary *)taskArguments |
811 | 854 | if ([mySQLConnection lastQueryWasCancelled]) break; |
812 | 855 | } |
813 | 856 |
|
| 857 | + [progressUpdater release]; |
| 858 | + |
814 | 859 | // Reload table list if at least one query began with drop, alter, rename, or create |
815 | 860 | if(tableListNeedsReload || databaseWasChanged) { |
816 | 861 | // Build database pulldown menu |
@@ -3606,3 +3651,59 @@ - (void)dealloc |
3606 | 3651 | } |
3607 | 3652 |
|
3608 | 3653 | @end |
| 3654 | + |
| 3655 | +#pragma mark - |
| 3656 | + |
| 3657 | +@implementation SPQueryProgressUpdateDecoupling |
| 3658 | + |
| 3659 | +- (instancetype)initWithBlock:(QueryProgressHandler)anUpdateHandler |
| 3660 | +{ |
| 3661 | + self = [super init]; |
| 3662 | + if (self) { |
| 3663 | + updateHandler = [anUpdateHandler copy]; |
| 3664 | + qpLock = OS_SPINLOCK_INIT; |
| 3665 | + } |
| 3666 | + |
| 3667 | + return self; |
| 3668 | +} |
| 3669 | + |
| 3670 | +- (void)setQueryProgress:(QueryProgress)newProgress |
| 3671 | +{ |
| 3672 | + BOOL notify = NO; |
| 3673 | + OSSpinLockLock(&qpLock); |
| 3674 | + _queryProgress = newProgress; |
| 3675 | + if(!dirtyMarker) { |
| 3676 | + dirtyMarker = 1; |
| 3677 | + notify = YES; |
| 3678 | + } |
| 3679 | + OSSpinLockUnlock(&qpLock); |
| 3680 | + |
| 3681 | + if(notify) [self performSelectorOnMainThread:@selector(_updateProgress) withObject:nil waitUntilDone:NO]; |
| 3682 | +} |
| 3683 | + |
| 3684 | +- (QueryProgress)queryProgress |
| 3685 | +{ |
| 3686 | + OSSpinLockLock(&qpLock); |
| 3687 | + QueryProgress cpy = _queryProgress; |
| 3688 | + OSSpinLockUnlock(&qpLock); |
| 3689 | + return cpy; |
| 3690 | +} |
| 3691 | + |
| 3692 | +- (void)_updateProgress |
| 3693 | +{ |
| 3694 | + OSSpinLockLock(&qpLock); |
| 3695 | + bool doRun = dirtyMarker; |
| 3696 | + dirtyMarker = NO; |
| 3697 | + QueryProgress p = _queryProgress; |
| 3698 | + OSSpinLockUnlock(&qpLock); |
| 3699 | + |
| 3700 | + if(doRun) updateHandler(&p); |
| 3701 | +} |
| 3702 | + |
| 3703 | +- (void)dealloc |
| 3704 | +{ |
| 3705 | + SPClear(updateHandler); |
| 3706 | + [super dealloc]; |
| 3707 | +} |
| 3708 | + |
| 3709 | +@end |
0 commit comments