Skip to content

Faster graph rendering for repositories with huge amount of branches. #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 5, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions GitUpKit/Core/GCCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#endif
#import "GCMacros.h"
#import "GCObject.h"
#import "GCOrderedSet.h"
#import "GCReference.h"
#import "GCSnapshot.h"
#import "GCReferenceTransform.h"
Expand Down
87 changes: 87 additions & 0 deletions GitUpKit/Core/GCOrderedSet-Tests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (C) 2015 Pierre-Olivier Latour <info@pol-online.net>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#import "GCTestCase.h"
#import "GCOrderedSet.h"

@implementation GCMultipleCommitsRepositoryTests(GCOrderedSetTests)

- (void)testOrderedSetAddObject {
GCOrderedSet* collection = [[GCOrderedSet alloc] init];
[collection addObject:self.commit1];
XCTAssertTrue([collection containsObject:self.commit1]);
NSArray* commits = collection.objects;
XCTAssertEqual(commits.count, 1);
XCTAssertEqual(commits[0], self.commit1);
}

- (void)testOrderedSetAddDuplicateObject {
GCOrderedSet* collection = [[GCOrderedSet alloc] init];
[collection addObject:self.commit1];
[collection addObject:self.commit1];
[collection addObject:self.commit2];
XCTAssertTrue([collection containsObject:self.commit1]);
NSArray* commits = collection.objects;
XCTAssertEqual(commits.count, 2);
XCTAssertEqual(commits[0], self.commit1);
XCTAssertEqual(commits[1], self.commit2);
}

- (void)testOrderedSetRemoveObject {
GCOrderedSet* collection = [[GCOrderedSet alloc] init];
[collection addObject:self.commit1];
[collection addObject:self.commit2];
XCTAssertTrue([collection containsObject:self.commit1]);
XCTAssertTrue([collection containsObject:self.commit2]);
[collection removeObject:self.commit1];
XCTAssertFalse([collection containsObject:self.commit1]);
XCTAssertTrue([collection containsObject:self.commit2]);
NSArray* commits = collection.objects;
XCTAssertEqual(commits.count, 1);
XCTAssertEqual(commits[0], self.commit2);
}

- (void)testOrderedSetReAddObject {
GCOrderedSet* collection = [[GCOrderedSet alloc] init];
[collection addObject:self.commit1];
[collection addObject:self.commit2];
XCTAssertTrue([collection containsObject:self.commit1]);
XCTAssertTrue([collection containsObject:self.commit2]);
[collection removeObject:self.commit1];
XCTAssertFalse([collection containsObject:self.commit1]);
XCTAssertTrue([collection containsObject:self.commit2]);
[collection addObject:self.commit1];
XCTAssertTrue([collection containsObject:self.commit1]);
XCTAssertTrue([collection containsObject:self.commit2]);
// NOTE: this collection preserves the original place of commit if re-added
NSArray* commits = collection.objects;
XCTAssertEqual(commits.count, 2);
XCTAssertEqual(commits[0], self.commit1);
XCTAssertEqual(commits[1], self.commit2);
}

- (void)testOrderedSetObjectsOrdering {
GCOrderedSet* collection = [[GCOrderedSet alloc] init];
[collection addObject:self.commit1];
[collection addObject:self.commit2];
[collection addObject:self.commit3];
NSArray* commits = collection.objects;
XCTAssertEqual(commits.count, 3);
XCTAssertEqual(commits[0], self.commit1);
XCTAssertEqual(commits[1], self.commit2);
XCTAssertEqual(commits[2], self.commit3);
}

@end
42 changes: 42 additions & 0 deletions GitUpKit/Core/GCOrderedSet.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (C) 2015 Pierre-Olivier Latour <info@pol-online.net>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#import <Foundation/Foundation.h>

@class GCObject;

/**
* This class is optimized to be fast when you have a lot of calls to
* - addObject: , -containsObject: and -removeObject: methods.
*/
@interface GCOrderedSet : NSObject

/**
* Accessing this property is CPU-expensive.
* It makes a copy of internal storage, filtered by existing objects.
* Try to store the value somewhere else and don't access this property if you don't have to.
*/
@property(nonatomic, readonly) NSArray* objects;

/**
* NOTE: Usually it is unnecessary to add an object, then remove it, then add it again.
* But if you will do this, it will appear in the object array
* at the SAME PLACE AS IT WAS ADDED.
*/
- (void)addObject:(GCObject*)object;
- (BOOL)containsObject:(GCObject*)object;
- (void)removeObject:(GCObject*)object;

@end
76 changes: 76 additions & 0 deletions GitUpKit/Core/GCOrderedSet.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (C) 2015 Pierre-Olivier Latour <info@pol-online.net>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#if !__has_feature(objc_arc)
#error This file requires ARC
#endif

#import "GCPrivate.h"

@implementation GCOrderedSet {
NSMutableArray* _objects; // Contains all the objects, even removed ones
CFMutableSetRef _actualObjectHashes; // Objects that were added but have not been removed
CFMutableSetRef _removedObjectHashes;
}

- (instancetype)init {
if ((self = [super init])) {
_objects = [[NSMutableArray alloc] init];
_actualObjectHashes = CFSetCreateMutable(kCFAllocatorDefault, 0, NULL);
_removedObjectHashes = CFSetCreateMutable(kCFAllocatorDefault, 0, NULL);
}
return self;
}

- (void)dealloc {
CFRelease(_actualObjectHashes);
CFRelease(_removedObjectHashes);
}

- (void)addObject:(GCObject*)object {
if (![self containsObject:object]) {
if (CFSetContainsValue(_removedObjectHashes, (__bridge const void*)(object.SHA1))) {
CFSetRemoveValue(_removedObjectHashes, (__bridge const void*)(object.SHA1));
} else {
[_objects addObject:object];
}
CFSetAddValue(_actualObjectHashes, (__bridge const void*)(object.SHA1));
}
}

- (void)removeObject:(GCObject*)object {
if ([self containsObject:object]) {
// Removing object from NSMutableArray is expensive,
// so we just moving SHA from one set to another.
CFSetRemoveValue(_actualObjectHashes, (__bridge const void*)(object.SHA1));
CFSetAddValue(_removedObjectHashes, (__bridge const void*)(object.SHA1));
}
}

- (BOOL)containsObject:(GCObject*)object {
return CFSetContainsValue(_actualObjectHashes, (__bridge const void*)(object.SHA1));
}

- (NSArray*)objects {
NSMutableArray* result = [[NSMutableArray alloc] initWithCapacity:_objects.count];
for (GCObject* object in _objects) {
if ([self containsObject:object]) { // Return only objects that were not removed
[result addObject:object];
}
}
return result;
}

@end
16 changes: 16 additions & 0 deletions GitUpKit/GitUpKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
objects = {

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

/* Begin PBXFileReference section */
74EDB5D01B84D4C400F00E79 /* GCOrderedSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCOrderedSet.h; sourceTree = "<group>"; };
74EDB5D11B84D4C400F00E79 /* GCOrderedSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCOrderedSet.m; sourceTree = "<group>"; };
74EDB5D51B84E06500F00E79 /* GCOrderedSet-Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GCOrderedSet-Tests.m"; sourceTree = "<group>"; };
E200A3B81B02DDA100C4E39D /* GCPrivate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCPrivate.m; sourceTree = "<group>"; };
E20EB08919FC75CA0031A075 /* GCRepository+Reset.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GCRepository+Reset.h"; sourceTree = "<group>"; };
E20EB08A19FC75CA0031A075 /* GCRepository+Reset.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GCRepository+Reset.m"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -925,6 +933,9 @@
E244538F1A70CDA200E61DE7 /* GCMacros.h */,
E2146C8C1A57F3BC00F4550B /* GCObject.h */,
E2146C8D1A57F3BC00F4550B /* GCObject.m */,
74EDB5D01B84D4C400F00E79 /* GCOrderedSet.h */,
74EDB5D11B84D4C400F00E79 /* GCOrderedSet.m */,
74EDB5D51B84E06500F00E79 /* GCOrderedSet-Tests.m */,
E2C338DF19F85C8600063D95 /* GCPrivate.h */,
E200A3B81B02DDA100C4E39D /* GCPrivate.m */,
E2C338E019F85C8600063D95 /* GCReference.h */,
Expand Down Expand Up @@ -1125,6 +1136,7 @@
E267E2091B84DB4200BAB377 /* GIBranch.h in Headers */,
E267E20A1B84DB4200BAB377 /* GIConstants.h in Headers */,
E267E20B1B84DB4200BAB377 /* GIDiffView.h in Headers */,
743BF1841B871C0200E1CA49 /* GCOrderedSet.h in Headers */,
E267E20C1B84DB4200BAB377 /* GIFunctions.h in Headers */,
E267E20D1B84DB4200BAB377 /* GIGraph.h in Headers */,
E267E20E1B84DB4200BAB377 /* GIGraphView.h in Headers */,
Expand Down Expand Up @@ -1374,6 +1386,7 @@
E217536C1B91634C00BE234A /* GCHistory.m in Sources */,
E217536D1B91634C00BE234A /* GCIndex.m in Sources */,
E217536F1B91634C00BE234A /* GCObject.m in Sources */,
749335CA1B9B7FF200225513 /* GCOrderedSet.m in Sources */,
E21753701B91634C00BE234A /* GCPrivate.m in Sources */,
E21753711B91634C00BE234A /* GCReference.m in Sources */,
E21753721B91634C00BE234A /* GCReferenceTransform.m in Sources */,
Expand Down Expand Up @@ -1427,6 +1440,7 @@
E267E1D81B84D83100BAB377 /* GCIndex.m in Sources */,
E267E1D91B84D83100BAB377 /* GCLiveRepository.m in Sources */,
E267E1DA1B84D83100BAB377 /* GCObject.m in Sources */,
749786941B85AAB10065BD55 /* GCOrderedSet.m in Sources */,
E267E1DB1B84D83100BAB377 /* GCPrivate.m in Sources */,
E267E1DC1B84D83100BAB377 /* GCReference.m in Sources */,
E267E1DD1B84D83100BAB377 /* GCReferenceTransform.m in Sources */,
Expand Down Expand Up @@ -1518,6 +1532,8 @@
E24509031A9A50F3003E602D /* GCRepository+Config-Tests.m in Sources */,
E2B1BF361A85C5ED00A999DF /* GIFunctions-Tests.m in Sources */,
E299D0141A749C26005035F7 /* GCRepository+Mock.m in Sources */,
74EDB5D71B8517F500F00E79 /* GCOrderedSet.m in Sources */,
74EDB5D61B84E06500F00E79 /* GCOrderedSet-Tests.m in Sources */,
E27E43031A74A96000D04ED1 /* GIBranch.m in Sources */,
E27E43081A74A96000D04ED1 /* GINode.m in Sources */,
E299D0161A749D27005035F7 /* GCRepository+Mock-Tests.m in Sources */,
Expand Down
34 changes: 14 additions & 20 deletions GitUpKit/Interface/GIGraph.m
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ - (BOOL)isEmpty {

- (void)_generateGraph {
NSTimeInterval staleTime = [NSDate timeIntervalSinceReferenceDate] - kStaleBranchInterval;
NSMutableArray* tips = [[NSMutableArray alloc] init];
GCOrderedSet* tips = [[GCOrderedSet alloc] init];
NSMutableSet* upstreamTips = [[NSMutableSet alloc] init];
GC_POINTER_LIST_ALLOCATE(skipList, 32);
GC_POINTER_LIST_ALLOCATE(newSkipList, 32);
Expand All @@ -133,33 +133,31 @@ - (void)_generateGraph {
GCHistoryCommit* upstreamTip = [(GCHistoryLocalBranch*)branch.upstream tipCommit];
if (upstreamTip && ((_options & kGIGraphOption_ShowVirtualTips) || upstreamTip.leaf)) {
[upstreamTips addObject:upstreamTip];
if (![tips containsObject:upstreamTip]) {
[tips addObject:upstreamTip];
}
[tips addObject:upstreamTip];
}
}

if (((_options & kGIGraphOption_ShowVirtualTips) || branch.tipCommit.leaf) && ![tips containsObject:branch.tipCommit]) {
if (((_options & kGIGraphOption_ShowVirtualTips) || branch.tipCommit.leaf)) {
[tips addObject:branch.tipCommit];
}
}

// Add remote branches to tips
for (GCHistoryRemoteBranch* branch in _history.remoteBranches) {
if (((_options & kGIGraphOption_ShowVirtualTips) || branch.tipCommit.leaf) && ![tips containsObject:branch.tipCommit]) {
if (((_options & kGIGraphOption_ShowVirtualTips) || branch.tipCommit.leaf)) {
[tips addObject:branch.tipCommit];
}
}

// Add leaf tags
for (GCHistoryTag* tag in _history.tags) {
if (tag.commit.leaf && ![tips containsObject:tag.commit]) {
if (tag.commit.leaf) {
[tips addObject:tag.commit];
}
}

// Verify all leaves are included in tips
XLOG_DEBUG_CHECK([[NSSet setWithArray:_history.leafCommits] isSubsetOfSet:[NSSet setWithArray:tips]]);
XLOG_DEBUG_CHECK([[NSSet setWithArray:_history.leafCommits] isSubsetOfSet:[NSSet setWithArray:tips.objects]]);

// Remove stale branch tips if needed
if (_options & kGIGraphOption_SkipStaleBranchTips) {
Expand Down Expand Up @@ -234,9 +232,7 @@ - (void)_generateGraph {
if (updateTips) {
XLOG_DEBUG_CHECK(!parent.leaf);
if ([headCommit isEqualToCommit:parent]) {
if (![tips containsObject:parent]) {
[tips addObject:parent];
}
[tips addObject:parent];
continue;
}
if (!(_options & kGIGraphOption_SkipStaleBranchTips) || (parent.timeIntervalSinceReferenceDate >= staleTime)) {
Expand All @@ -249,9 +245,7 @@ - (void)_generateGraph {
}
}
if (resuscitate) {
if (![tips containsObject:parent]) {
[tips addObject:parent];
}
[tips addObject:parent];
continue;
}
}
Expand All @@ -264,9 +258,7 @@ - (void)_generateGraph {
}
}
if (resuscitate) {
if (![tips containsObject:parent]) {
[tips addObject:parent];
}
[tips addObject:parent];
continue;
}
}
Expand Down Expand Up @@ -312,20 +304,22 @@ - (void)_generateGraph {
skipBlock(YES);
}

NSArray* tipsArray = tips.objects;

// Make sure we have some tips left
if (tips.count == 0) {
if (tipsArray.count == 0) {
goto cleanup;
}

// 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)
if (_options & kGIGraphOption_ShowVirtualTips) {
[tips sortUsingSelector:@selector(reverseTimeCompare:)];
tipsArray = [tipsArray sortedArrayUsingSelector:@selector(reverseTimeCompare:)];
}

// Create initial layer made of tips
GILayer* layer = [[GILayer alloc] initWithIndex:CFArrayGetCount(_layers)];
@autoreleasepool {
for (GCHistoryCommit* commit in tips) {
for (GCHistoryCommit* commit in tipsArray) {
// Create new branch
GIBranch* branch = [[GIBranch alloc] init];
CFArrayAppendValue(_branches, branch);
Expand Down