Skip to content

Commit 4cfc9f9

Browse files
committed
Merge pull request #3 from mandrigin/master
Faster graph rendering for repositories with lots of branches
2 parents 08f7aa2 + 047750f commit 4cfc9f9

File tree

6 files changed

+236
-20
lines changed

6 files changed

+236
-20
lines changed

GitUpKit/Core/GCCore.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#endif
2828
#import "GCMacros.h"
2929
#import "GCObject.h"
30+
#import "GCOrderedSet.h"
3031
#import "GCReference.h"
3132
#import "GCSnapshot.h"
3233
#import "GCReferenceTransform.h"

GitUpKit/Core/GCOrderedSet-Tests.m

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (C) 2015 Pierre-Olivier Latour <info@pol-online.net>
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#import "GCTestCase.h"
17+
#import "GCOrderedSet.h"
18+
19+
@implementation GCMultipleCommitsRepositoryTests(GCOrderedSetTests)
20+
21+
- (void)testOrderedSetAddObject {
22+
GCOrderedSet* collection = [[GCOrderedSet alloc] init];
23+
[collection addObject:self.commit1];
24+
XCTAssertTrue([collection containsObject:self.commit1]);
25+
NSArray* commits = collection.objects;
26+
XCTAssertEqual(commits.count, 1);
27+
XCTAssertEqual(commits[0], self.commit1);
28+
}
29+
30+
- (void)testOrderedSetAddDuplicateObject {
31+
GCOrderedSet* collection = [[GCOrderedSet alloc] init];
32+
[collection addObject:self.commit1];
33+
[collection addObject:self.commit1];
34+
[collection addObject:self.commit2];
35+
XCTAssertTrue([collection containsObject:self.commit1]);
36+
NSArray* commits = collection.objects;
37+
XCTAssertEqual(commits.count, 2);
38+
XCTAssertEqual(commits[0], self.commit1);
39+
XCTAssertEqual(commits[1], self.commit2);
40+
}
41+
42+
- (void)testOrderedSetRemoveObject {
43+
GCOrderedSet* collection = [[GCOrderedSet alloc] init];
44+
[collection addObject:self.commit1];
45+
[collection addObject:self.commit2];
46+
XCTAssertTrue([collection containsObject:self.commit1]);
47+
XCTAssertTrue([collection containsObject:self.commit2]);
48+
[collection removeObject:self.commit1];
49+
XCTAssertFalse([collection containsObject:self.commit1]);
50+
XCTAssertTrue([collection containsObject:self.commit2]);
51+
NSArray* commits = collection.objects;
52+
XCTAssertEqual(commits.count, 1);
53+
XCTAssertEqual(commits[0], self.commit2);
54+
}
55+
56+
- (void)testOrderedSetReAddObject {
57+
GCOrderedSet* collection = [[GCOrderedSet alloc] init];
58+
[collection addObject:self.commit1];
59+
[collection addObject:self.commit2];
60+
XCTAssertTrue([collection containsObject:self.commit1]);
61+
XCTAssertTrue([collection containsObject:self.commit2]);
62+
[collection removeObject:self.commit1];
63+
XCTAssertFalse([collection containsObject:self.commit1]);
64+
XCTAssertTrue([collection containsObject:self.commit2]);
65+
[collection addObject:self.commit1];
66+
XCTAssertTrue([collection containsObject:self.commit1]);
67+
XCTAssertTrue([collection containsObject:self.commit2]);
68+
// NOTE: this collection preserves the original place of commit if re-added
69+
NSArray* commits = collection.objects;
70+
XCTAssertEqual(commits.count, 2);
71+
XCTAssertEqual(commits[0], self.commit1);
72+
XCTAssertEqual(commits[1], self.commit2);
73+
}
74+
75+
- (void)testOrderedSetObjectsOrdering {
76+
GCOrderedSet* collection = [[GCOrderedSet alloc] init];
77+
[collection addObject:self.commit1];
78+
[collection addObject:self.commit2];
79+
[collection addObject:self.commit3];
80+
NSArray* commits = collection.objects;
81+
XCTAssertEqual(commits.count, 3);
82+
XCTAssertEqual(commits[0], self.commit1);
83+
XCTAssertEqual(commits[1], self.commit2);
84+
XCTAssertEqual(commits[2], self.commit3);
85+
}
86+
87+
@end

GitUpKit/Core/GCOrderedSet.h

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (C) 2015 Pierre-Olivier Latour <info@pol-online.net>
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#import <Foundation/Foundation.h>
17+
18+
@class GCObject;
19+
20+
/**
21+
* This class is optimized to be fast when you have a lot of calls to
22+
* - addObject: , -containsObject: and -removeObject: methods.
23+
*/
24+
@interface GCOrderedSet : NSObject
25+
26+
/**
27+
* Accessing this property is CPU-expensive.
28+
* It makes a copy of internal storage, filtered by existing objects.
29+
* Try to store the value somewhere else and don't access this property if you don't have to.
30+
*/
31+
@property(nonatomic, readonly) NSArray* objects;
32+
33+
/**
34+
* NOTE: Usually it is unnecessary to add an object, then remove it, then add it again.
35+
* But if you will do this, it will appear in the object array
36+
* at the SAME PLACE AS IT WAS ADDED.
37+
*/
38+
- (void)addObject:(GCObject*)object;
39+
- (BOOL)containsObject:(GCObject*)object;
40+
- (void)removeObject:(GCObject*)object;
41+
42+
@end

GitUpKit/Core/GCOrderedSet.m

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (C) 2015 Pierre-Olivier Latour <info@pol-online.net>
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#if !__has_feature(objc_arc)
17+
#error This file requires ARC
18+
#endif
19+
20+
#import "GCPrivate.h"
21+
22+
@implementation GCOrderedSet {
23+
NSMutableArray* _objects; // Contains all the objects, even removed ones
24+
CFMutableSetRef _actualObjectHashes; // Objects that were added but have not been removed
25+
CFMutableSetRef _removedObjectHashes;
26+
}
27+
28+
- (instancetype)init {
29+
if ((self = [super init])) {
30+
_objects = [[NSMutableArray alloc] init];
31+
_actualObjectHashes = CFSetCreateMutable(kCFAllocatorDefault, 0, NULL);
32+
_removedObjectHashes = CFSetCreateMutable(kCFAllocatorDefault, 0, NULL);
33+
}
34+
return self;
35+
}
36+
37+
- (void)dealloc {
38+
CFRelease(_actualObjectHashes);
39+
CFRelease(_removedObjectHashes);
40+
}
41+
42+
- (void)addObject:(GCObject*)object {
43+
if (![self containsObject:object]) {
44+
if (CFSetContainsValue(_removedObjectHashes, (__bridge const void*)(object.SHA1))) {
45+
CFSetRemoveValue(_removedObjectHashes, (__bridge const void*)(object.SHA1));
46+
} else {
47+
[_objects addObject:object];
48+
}
49+
CFSetAddValue(_actualObjectHashes, (__bridge const void*)(object.SHA1));
50+
}
51+
}
52+
53+
- (void)removeObject:(GCObject*)object {
54+
if ([self containsObject:object]) {
55+
// Removing object from NSMutableArray is expensive,
56+
// so we just moving SHA from one set to another.
57+
CFSetRemoveValue(_actualObjectHashes, (__bridge const void*)(object.SHA1));
58+
CFSetAddValue(_removedObjectHashes, (__bridge const void*)(object.SHA1));
59+
}
60+
}
61+
62+
- (BOOL)containsObject:(GCObject*)object {
63+
return CFSetContainsValue(_actualObjectHashes, (__bridge const void*)(object.SHA1));
64+
}
65+
66+
- (NSArray*)objects {
67+
NSMutableArray* result = [[NSMutableArray alloc] initWithCapacity:_objects.count];
68+
for (GCObject* object in _objects) {
69+
if ([self containsObject:object]) { // Return only objects that were not removed
70+
[result addObject:object];
71+
}
72+
}
73+
return result;
74+
}
75+
76+
@end

GitUpKit/GitUpKit.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
743BF1841B871C0200E1CA49 /* GCOrderedSet.h in Headers */ = {isa = PBXBuildFile; fileRef = 74EDB5D01B84D4C400F00E79 /* GCOrderedSet.h */; settings = {ATTRIBUTES = (Public, ); }; };
11+
749335CA1B9B7FF200225513 /* GCOrderedSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 74EDB5D11B84D4C400F00E79 /* GCOrderedSet.m */; };
12+
749786941B85AAB10065BD55 /* GCOrderedSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 74EDB5D11B84D4C400F00E79 /* GCOrderedSet.m */; };
13+
74EDB5D61B84E06500F00E79 /* GCOrderedSet-Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 74EDB5D51B84E06500F00E79 /* GCOrderedSet-Tests.m */; };
14+
74EDB5D71B8517F500F00E79 /* GCOrderedSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 74EDB5D11B84D4C400F00E79 /* GCOrderedSet.m */; };
1015
E200A3BA1B02DDA100C4E39D /* GCPrivate.m in Sources */ = {isa = PBXBuildFile; fileRef = E200A3B81B02DDA100C4E39D /* GCPrivate.m */; };
1116
E20EB08C19FC75CA0031A075 /* GCRepository+Reset.m in Sources */ = {isa = PBXBuildFile; fileRef = E20EB08A19FC75CA0031A075 /* GCRepository+Reset.m */; };
1217
E20EB09019FC76160031A075 /* GCRepository+Status.m in Sources */ = {isa = PBXBuildFile; fileRef = E20EB08E19FC76160031A075 /* GCRepository+Status.m */; };
@@ -411,6 +416,9 @@
411416
/* End PBXContainerItemProxy section */
412417

413418
/* Begin PBXFileReference section */
419+
74EDB5D01B84D4C400F00E79 /* GCOrderedSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCOrderedSet.h; sourceTree = "<group>"; };
420+
74EDB5D11B84D4C400F00E79 /* GCOrderedSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCOrderedSet.m; sourceTree = "<group>"; };
421+
74EDB5D51B84E06500F00E79 /* GCOrderedSet-Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GCOrderedSet-Tests.m"; sourceTree = "<group>"; };
414422
E200A3B81B02DDA100C4E39D /* GCPrivate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCPrivate.m; sourceTree = "<group>"; };
415423
E20EB08919FC75CA0031A075 /* GCRepository+Reset.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GCRepository+Reset.h"; sourceTree = "<group>"; };
416424
E20EB08A19FC75CA0031A075 /* GCRepository+Reset.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GCRepository+Reset.m"; sourceTree = "<group>"; };
@@ -925,6 +933,9 @@
925933
E244538F1A70CDA200E61DE7 /* GCMacros.h */,
926934
E2146C8C1A57F3BC00F4550B /* GCObject.h */,
927935
E2146C8D1A57F3BC00F4550B /* GCObject.m */,
936+
74EDB5D01B84D4C400F00E79 /* GCOrderedSet.h */,
937+
74EDB5D11B84D4C400F00E79 /* GCOrderedSet.m */,
938+
74EDB5D51B84E06500F00E79 /* GCOrderedSet-Tests.m */,
928939
E2C338DF19F85C8600063D95 /* GCPrivate.h */,
929940
E200A3B81B02DDA100C4E39D /* GCPrivate.m */,
930941
E2C338E019F85C8600063D95 /* GCReference.h */,
@@ -1125,6 +1136,7 @@
11251136
E267E2091B84DB4200BAB377 /* GIBranch.h in Headers */,
11261137
E267E20A1B84DB4200BAB377 /* GIConstants.h in Headers */,
11271138
E267E20B1B84DB4200BAB377 /* GIDiffView.h in Headers */,
1139+
743BF1841B871C0200E1CA49 /* GCOrderedSet.h in Headers */,
11281140
E267E20C1B84DB4200BAB377 /* GIFunctions.h in Headers */,
11291141
E267E20D1B84DB4200BAB377 /* GIGraph.h in Headers */,
11301142
E267E20E1B84DB4200BAB377 /* GIGraphView.h in Headers */,
@@ -1374,6 +1386,7 @@
13741386
E217536C1B91634C00BE234A /* GCHistory.m in Sources */,
13751387
E217536D1B91634C00BE234A /* GCIndex.m in Sources */,
13761388
E217536F1B91634C00BE234A /* GCObject.m in Sources */,
1389+
749335CA1B9B7FF200225513 /* GCOrderedSet.m in Sources */,
13771390
E21753701B91634C00BE234A /* GCPrivate.m in Sources */,
13781391
E21753711B91634C00BE234A /* GCReference.m in Sources */,
13791392
E21753721B91634C00BE234A /* GCReferenceTransform.m in Sources */,
@@ -1427,6 +1440,7 @@
14271440
E267E1D81B84D83100BAB377 /* GCIndex.m in Sources */,
14281441
E267E1D91B84D83100BAB377 /* GCLiveRepository.m in Sources */,
14291442
E267E1DA1B84D83100BAB377 /* GCObject.m in Sources */,
1443+
749786941B85AAB10065BD55 /* GCOrderedSet.m in Sources */,
14301444
E267E1DB1B84D83100BAB377 /* GCPrivate.m in Sources */,
14311445
E267E1DC1B84D83100BAB377 /* GCReference.m in Sources */,
14321446
E267E1DD1B84D83100BAB377 /* GCReferenceTransform.m in Sources */,
@@ -1518,6 +1532,8 @@
15181532
E24509031A9A50F3003E602D /* GCRepository+Config-Tests.m in Sources */,
15191533
E2B1BF361A85C5ED00A999DF /* GIFunctions-Tests.m in Sources */,
15201534
E299D0141A749C26005035F7 /* GCRepository+Mock.m in Sources */,
1535+
74EDB5D71B8517F500F00E79 /* GCOrderedSet.m in Sources */,
1536+
74EDB5D61B84E06500F00E79 /* GCOrderedSet-Tests.m in Sources */,
15211537
E27E43031A74A96000D04ED1 /* GIBranch.m in Sources */,
15221538
E27E43081A74A96000D04ED1 /* GINode.m in Sources */,
15231539
E299D0161A749D27005035F7 /* GCRepository+Mock-Tests.m in Sources */,

GitUpKit/Interface/GIGraph.m

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ - (BOOL)isEmpty {
109109

110110
- (void)_generateGraph {
111111
NSTimeInterval staleTime = [NSDate timeIntervalSinceReferenceDate] - kStaleBranchInterval;
112-
NSMutableArray* tips = [[NSMutableArray alloc] init];
112+
GCOrderedSet* tips = [[GCOrderedSet alloc] init];
113113
NSMutableSet* upstreamTips = [[NSMutableSet alloc] init];
114114
GC_POINTER_LIST_ALLOCATE(skipList, 32);
115115
GC_POINTER_LIST_ALLOCATE(newSkipList, 32);
@@ -133,33 +133,31 @@ - (void)_generateGraph {
133133
GCHistoryCommit* upstreamTip = [(GCHistoryLocalBranch*)branch.upstream tipCommit];
134134
if (upstreamTip && ((_options & kGIGraphOption_ShowVirtualTips) || upstreamTip.leaf)) {
135135
[upstreamTips addObject:upstreamTip];
136-
if (![tips containsObject:upstreamTip]) {
137-
[tips addObject:upstreamTip];
138-
}
136+
[tips addObject:upstreamTip];
139137
}
140138
}
141139

142-
if (((_options & kGIGraphOption_ShowVirtualTips) || branch.tipCommit.leaf) && ![tips containsObject:branch.tipCommit]) {
140+
if (((_options & kGIGraphOption_ShowVirtualTips) || branch.tipCommit.leaf)) {
143141
[tips addObject:branch.tipCommit];
144142
}
145143
}
146144

147145
// Add remote branches to tips
148146
for (GCHistoryRemoteBranch* branch in _history.remoteBranches) {
149-
if (((_options & kGIGraphOption_ShowVirtualTips) || branch.tipCommit.leaf) && ![tips containsObject:branch.tipCommit]) {
147+
if (((_options & kGIGraphOption_ShowVirtualTips) || branch.tipCommit.leaf)) {
150148
[tips addObject:branch.tipCommit];
151149
}
152150
}
153151

154152
// Add leaf tags
155153
for (GCHistoryTag* tag in _history.tags) {
156-
if (tag.commit.leaf && ![tips containsObject:tag.commit]) {
154+
if (tag.commit.leaf) {
157155
[tips addObject:tag.commit];
158156
}
159157
}
160158

161159
// Verify all leaves are included in tips
162-
XLOG_DEBUG_CHECK([[NSSet setWithArray:_history.leafCommits] isSubsetOfSet:[NSSet setWithArray:tips]]);
160+
XLOG_DEBUG_CHECK([[NSSet setWithArray:_history.leafCommits] isSubsetOfSet:[NSSet setWithArray:tips.objects]]);
163161

164162
// Remove stale branch tips if needed
165163
if (_options & kGIGraphOption_SkipStaleBranchTips) {
@@ -234,9 +232,7 @@ - (void)_generateGraph {
234232
if (updateTips) {
235233
XLOG_DEBUG_CHECK(!parent.leaf);
236234
if ([headCommit isEqualToCommit:parent]) {
237-
if (![tips containsObject:parent]) {
238-
[tips addObject:parent];
239-
}
235+
[tips addObject:parent];
240236
continue;
241237
}
242238
if (!(_options & kGIGraphOption_SkipStaleBranchTips) || (parent.timeIntervalSinceReferenceDate >= staleTime)) {
@@ -249,9 +245,7 @@ - (void)_generateGraph {
249245
}
250246
}
251247
if (resuscitate) {
252-
if (![tips containsObject:parent]) {
253-
[tips addObject:parent];
254-
}
248+
[tips addObject:parent];
255249
continue;
256250
}
257251
}
@@ -264,9 +258,7 @@ - (void)_generateGraph {
264258
}
265259
}
266260
if (resuscitate) {
267-
if (![tips containsObject:parent]) {
268-
[tips addObject:parent];
269-
}
261+
[tips addObject:parent];
270262
continue;
271263
}
272264
}
@@ -312,20 +304,22 @@ - (void)_generateGraph {
312304
skipBlock(YES);
313305
}
314306

307+
NSArray* tipsArray = tips.objects;
308+
315309
// Make sure we have some tips left
316-
if (tips.count == 0) {
310+
if (tipsArray.count == 0) {
317311
goto cleanup;
318312
}
319313

320314
// Re-sort all tips in descending chronological order (this ensures virtual tips will be on the rightside of the tips descending from the same commits)
321315
if (_options & kGIGraphOption_ShowVirtualTips) {
322-
[tips sortUsingSelector:@selector(reverseTimeCompare:)];
316+
tipsArray = [tipsArray sortedArrayUsingSelector:@selector(reverseTimeCompare:)];
323317
}
324318

325319
// Create initial layer made of tips
326320
GILayer* layer = [[GILayer alloc] initWithIndex:CFArrayGetCount(_layers)];
327321
@autoreleasepool {
328-
for (GCHistoryCommit* commit in tips) {
322+
for (GCHistoryCommit* commit in tipsArray) {
329323
// Create new branch
330324
GIBranch* branch = [[GIBranch alloc] init];
331325
CFArrayAppendValue(_branches, branch);

0 commit comments

Comments
 (0)