1. Jens Alfke
  2. Murky

Commits

Jens Alfke  committed 0cbde1f

Optimized loading of large repositories, by not reading the nonessential details of revisions (comment, author, email, date) up-front. Instead, these are now loaded lazily when needed.

  • Participants
  • Parent commits 04134e2
  • Branches default

Comments (0)

Files changed (7)

File Resources/xmlstyle/xmlminimal.style

View file
 header = '<?xml version="1.0" encoding="UTF-8" ?>\n<repository name="{repo}">\n\n'
 footer = '</repository>\n'
-changeset = '<changeset rev="{rev}" />\n{parents}</changeset>\n'
-parent = '<parent rev="{rev}" />\n'
+changeset = '<changeset rev="{rev}" node="{node|escape}">{parents}</changeset>\n'
+parent = '<parent rev="{rev}" />'

File Source/HgLogOperation.h

View file
 @class HgFile, HgRepository, HgRevision;
 
 
+typedef enum {
+    kHgLogModeListRevs = 0,
+    kHgLogModeMinimal,
+    kHgLogModeFull
+} HgLogMode;
+
+
 /** Runs an "hg log" command and parses the output into an array of HgRevision objects. */
 @interface HgLogOperation : HgOperation
 {
     HgRepository *_repository;
-    BOOL _full;
+    HgLogMode _mode;
     NSMutableArray *_revisions;
     NSMutableDictionary *_revsByNumber;
     NSMutableIndexSet *_revisionNumbers;
-    unsigned _limit;
+    NSRange _range;
 }
 
 - (id) initWithRepository: (HgRepository*)repo 
                      file: (HgFile*)file
-                     full: (BOOL)full;
+                     mode: (HgLogMode)mode;
 
 - (id) initOutgoingWithRevision: (HgRevision*)revision
                       otherRepo: (NSURL*)otherRepo;
 - (id) initIncomingWithRepository: (HgRepository*)repo
                         otherRepo: (NSURL*)otherRepo;
 
-@property unsigned limit;
+//@property unsigned limit;
+@property NSRange range;
 
 @property (readonly,nonatomic) NSArray* revisions;
 @property (readonly,nonatomic) NSIndexSet* revisionNumbers;

File Source/HgLogOperation.m

View file
 
 - (id) initWithRepository: (HgRepository*)repo 
                      file: (HgFile*)file
-                     full: (BOOL)full
+                     mode: (HgLogMode)mode
 {
     id dir;
     NSString *filename;
                             command: @"log", filename, nil];
     if( self ) {
         _repository = repo;
-        _full = full;
+        _mode = mode;
     }
     return self;
 }
                                      nil];
     if( self ) {
         _repository = revision.repository;
-        _full = NO;
+        _mode = kHgLogModeListRevs;
     }
     return self;
 }
                                      nil];
     if( self ) {
         _repository = repo;
-        _full = YES;
+        _mode = kHgLogModeFull;
     }
     return self;
 }
 
 - (NSTask*) createTask
 {
-    if( _limit > 0 )
-        [self prependArguments: @"--limit", [NSNumber numberWithUnsignedInt:_limit], nil];
-    if( _full ) {
-        static NSString *sFullStylePath;
-        if( ! sFullStylePath ) {
-            sFullStylePath = [[NSBundle bundleForClass: [self class]] pathForResource: @"xml" ofType: @"style"];
-            NSAssert(sFullStylePath,@"Missing xml.style");
-        }        
-        [self prependArguments: @"--verbose", @"--style", sFullStylePath,nil];
-    } else
-        [self prependArguments: @"--template", @"{rev},",nil];
+    if (_range.length > 0)
+        [self prependArguments: @"--rev",
+                                [NSString stringWithFormat: @"%u:%u",
+                                    _range.location, _range.location+_range.length-1],
+                                nil];
+    
+    switch (_mode) {
+        case kHgLogModeListRevs:
+            [self prependArguments: @"--template", @"{rev},",nil];
+            break;
+        case kHgLogModeMinimal: {
+            NSString *stylePath = [[NSBundle bundleForClass: [self class]] pathForResource: @"xmlminimal" ofType: @"style"];
+            NSAssert(stylePath,@"Missing xmlminimal.style");
+            [self prependArguments: @"--style", stylePath,nil];
+            break;
+        }
+        case kHgLogModeFull: {
+            NSString *stylePath = [[NSBundle bundleForClass: [self class]] pathForResource: @"xml" ofType: @"style"];
+            NSAssert(stylePath,@"Missing xml.style");
+            [self prependArguments: @"--verbose", @"--style", stylePath,nil];
+            break;
+        }
+    }
     return [super createTask];
 }
 
 - (void) finished
 {
     if( ! self.error ) {
-        if( _full )
+        NSLog(@"HgLogOperation is parsing...");
+        CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
+        if( _mode == kHgLogModeListRevs )
+            [self _parseRevNumbers];
+        else
             [self _parseFullXML];
-        else
-            [self _parseRevNumbers];
+        time = CFAbsoluteTimeGetCurrent() - time;
+        NSLog(@"HgLogOperation took %.1f sec to parse %u revs", time, _revisions.count);
     }
 }
 
 
-@synthesize limit=_limit, revisions=_revisions, revisionNumbers=_revisionNumbers;
+@synthesize range=_range, revisions=_revisions, revisionNumbers=_revisionNumbers;
 
 
 - (BOOL) _parseRevNumbers
     if( nodes == nil )
         return nil;
     if( [nodes count] == 0 ) {
-        NSLog(@"No results for '%@'",xpath);
+        //NSLog(@"No results for '%@'",xpath);
         return nil;
     }
     NSMutableString *str = [NSMutableString string];
 
 static NSDate* parseDate( NSString *hgDateStr )
 {
-    // 'hgdate' format contains two integers: seconds since 1970, space, then GMT offset in seconds.
-    NSArray *bits = [hgDateStr componentsSeparatedByString: @" "];
-    if( bits.count >= 2 ) {
-    NSTimeInterval time = [[bits objectAtIndex: 0] doubleValue];
-        if( time > 0.0 ) {
-            //NSTimeInterval offset = [[bits objectAtIndex: 1] doubleValue];
-            //FIX: Ignoring the offset since NSDate can't hold a timezone. Use a 2nd object for that?
-            return [NSDate dateWithTimeIntervalSince1970: time];
+    if (hgDateStr) {
+        // 'hgdate' format contains two integers: seconds since 1970, space, then GMT offset in seconds.
+        NSArray *bits = [hgDateStr componentsSeparatedByString: @" "];
+        if( bits.count >= 2 ) {
+        NSTimeInterval time = [[bits objectAtIndex: 0] doubleValue];
+            if( time > 0.0 ) {
+                //NSTimeInterval offset = [[bits objectAtIndex: 1] doubleValue];
+                //FIX: Ignoring the offset since NSDate can't hold a timezone. Use a 2nd object for that?
+                return [NSDate dateWithTimeIntervalSince1970: time];
+            }
         }
+        NSLog(@"Warning: Failed to parse date '%@'",hgDateStr);
     }
-    NSLog(@"Warning: Failed to parse date '%@'",hgDateStr);
     return nil;
 }
 
                 numbered: (int)revNo
            fromChangeset: (NSXMLElement*)changeset
 {
-    revision.date = parseDate( [[changeset attributeForName: @"date"] stringValue] );
-    revision.comment = strForXPath(changeset,@"description/child::text()");
-    revision.author = strForXPath(changeset,@"author/@name");
-    revision.email = strForXPath(changeset,@"author/@email");
-    //revision.files = stringsForXPath(changeset,@"files/file/@path");
+    if (_mode == kHgLogModeFull) {
+        revision.date = parseDate( [[changeset attributeForName: @"date"] stringValue] );
+        revision.comment = strForXPath(changeset,@"description/child::text()");
+        revision.author = strForXPath(changeset,@"author/@name");
+        revision.email = strForXPath(changeset,@"author/@email");
+    }
     
     // Link to its parent(s):
     if( revNo > 0 ) {
             [self _updateRevision: revision numbered: revNo fromChangeset: changeset];
         } else {
             revision = [[HgRevision alloc] initWithRepository: _repository
-                                                              localNumber: revNo
-                                                               identifier: nodeID];
+                                                  localNumber: revNo
+                                                   identifier: nodeID];
             [self _updateRevision: revision numbered: revNo fromChangeset: changeset];
             [_revisions addObject: revision];
             [_revsByNumber setObject: revision forKey: [NSNumber numberWithInt: revNo]];

File Source/HgRepository.h

View file
 
 // internal:
 - (void) ignoreNextFileChanges;
+- (BOOL) getDetailsOfRevisions: (NSRange)revNoRange;
 
 @end

File Source/HgRepository.m

View file
             _revisions = [NSArray array];   // Because HgLogOperation will call my -revisions
         HgLogOperation *op = [[HgLogOperation alloc] initWithRepository: self 
                                                                    file: nil 
-                                                                   full: YES];
-        if( _revisions.count > 0 )
-            op.limit = tip-curTip;      // only need to ask about the new revisions
+                                                                   mode: kHgLogModeMinimal];
+        if( curTip >= 0 )
+            op.range = NSMakeRange(curTip+1, tip-curTip); // only need to ask about the new revisions
         if( ! [op run] )
             return NO;
         NSAssert([[op.revisions objectAtIndex: 0] localNumber]==curTip+1,@"Unexpected revision numbers from log");
 }
 
 
+/** Batch-loads the detail info of a range of revisions. */
+- (BOOL) getDetailsOfRevisions: (NSRange)revNoRange {
+    unsigned nRevisions = _revisions.count;
+    if (nRevisions > 0 && [[_revisions lastObject] isUncommitted])
+        nRevisions--;
+    NSAssert(revNoRange.location < nRevisions, @"Invalid start revision");
+    revNoRange.length = MIN(revNoRange.length, nRevisions - revNoRange.location);
+    
+    NSLog(@"HgRepository: Getting details of revisions [%u..%u]", 
+            revNoRange.location, revNoRange.location+revNoRange.length-1);
+    HgLogOperation *op = [[HgLogOperation alloc] initWithRepository: self 
+                                                               file: nil 
+                                                               mode: kHgLogModeFull];
+    op.range = revNoRange;
+    return [op run];
+}
+
+
 - (NSArray*) revisions
 {
     if( ! _revisions ) {
 
 - (NSArray*) revisionsOfFile: (HgFile*)file error: (NSError**)outError
 {
-    HgLogOperation *op = [[HgLogOperation alloc] initWithRepository: self file: file full: NO];
+    HgLogOperation *op = [[HgLogOperation alloc] initWithRepository: self 
+                                                               file: file 
+                                                               mode: kHgLogModeListRevs];
     if( ! [op run: outError] )
         return nil;
     NSIndexSet *revs = op.revisionNumbers;  //! Could cache this per file-path

File Source/HgRevision.m

View file
 
 
 @synthesize repository=_repository, localNumber=_localNumber, identifier=_identifier;
-@synthesize comment=_comment, author=_author, email=_email, date=_date;
+
+
+/** Fills in the full info of this revision (and nearby ones) */
+- (BOOL) _getDetails {
+    NSRange revs = {_localNumber - (_localNumber % 20), 20};
+    return [_repository getDetailsOfRevisions: revs];
+}
+
+- (NSDate*) date {
+    if (!_date) [self _getDetails];
+    return _date;
+}
+
+- (void) setDate: (NSDate*)date {
+    _date = date;
+}
+
+- (NSString*) comment {
+    // Comment might legitimately be nil, so use _date as a test for getting details:
+    if (!_date) [self _getDetails];
+    return _comment;
+}
+
+- (void) setComment: (NSString*)comment {
+    _comment = comment;
+}
+
+- (NSString*) author {
+    if (!_date) [self _getDetails];
+    return _author;
+}
+
+- (void) setAuthor: (NSString*)author {
+    _author = author;
+}
+
+- (NSString*) email {
+    if (!_date) [self _getDetails];
+    return _email;
+}
+
+- (void) setEmail: (NSString*)email {
+    _email = email;
+}
+
 
 - (NSArray*) parents
 {

File Source/HgUncommittedRevision.m

View file
 
 - (BOOL) isUncommitted              {return YES;}
 
-- (NSString*) localString        {return @"current";}
+- (NSString*) localString           {return @"current";}
 
 - (NSString*) identifierString      {return nil;}
 
+- (BOOL) _getDetails                {return YES;}
+
 - (NSString*) comment
 {
     return _comment.length ?_comment :@"--locally modified--";
 
 - (BOOL) updatePaths: (NSSet*)paths error: (NSError**)outError
 {
+    if (!_root) {
+        // If I haven't loaded my files at all, just load 'em and return. Otherwise
+        // the code below will end up doing that and then calling -updateStatus again,
+        // which is inefficient.
+        self.root;
+        return YES;
+    }
+    
     BOOL any = NO;
     for( NSString *path in paths ) {
         HgFile *dir = [self fileAtPath: path];