I think you should test this on some files from MySQL or Launchpad,
which will have a longer but still very bushy history?
This looks fairly plausible to me, but I think there should be a unit
test for the fairly nontrivial function you've added. The approach here
of falling back to the old code means that any bugs here may not
actually be exercised by the test suite.
+* ``bzr log FILE`` is now substantially faster on flat-ish histories.
+ On OpenOffice.org for example, logging a typical file now takes
+ a second or so instead of 29 seconds. (Ian Clatworthy)
+
* Resolving a revno to a revision id on a branch accessed via ``bzr://``
or ``bzr+ssh://`` is now much faster and involves no VFS operations.
This speeds up commands like ``bzr pull -r 123``. (Andrew Bennetts)
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2009-06-15 06:47:14 +0000
+++ bzrlib/builtins.py 2009-06-16 12:46:50 +0000
@@ -2237,16 +2237,14 @@
# the underlying repository format is faster at generating
# deltas or can provide everything we need from the indices.
# The default algorithm - match-using-deltas - works for
- # multiple files and directories and is faster for small
- # amounts of history (200 revisions say). However, it's too
+ # multiple files and directories. However, it's too
# slow for logging a single file in a repository with deep
# history, i.e. > 10K revisions. In the spirit of "do no
# evil when adding features", we continue to use the
# original algorithm - per-file-graph - for the "single
# file that isn't a directory without showing a delta" case.
- partial_history = revision and b.repository._format.supports_chks match_using_deltas = (len(file_ids) != 1 or filter_by_dir
- or delta_type or partial_history)
+ or delta_type)
# Build the LogRequest and execute it
if len(file_ids) == 0:
def _log_revision_iterator_using_per_file_graph(self):
- # Get the base revisions, filtering by the revision range.
- # Note that we always generate the merge revisions because
- # filter_revisions_touching_file_id() requires them ...
rqst = self.rqst
- view_revisions = _calc_view_revisions(self.branch, self.start_rev_id,
- self.end_rev_id, rqst.get('direction'), True)
- if not isinstance(view_revisions, list):
- view_revisions = list(view_revisions)
- view_revisions = _filter_revisions_touching_file_id(self.branch,
- rqst.get('specific_fileids')[0], view_revisions,
- include_merges=rqst.get('levels') != 1)
+ direction = rqst.get('direction')
+ file_id = rqst.get('specific_fileids')[0]
+ multi_level = rqst.get('levels') != 1
+ try:
+ file_graph, graph_tip = _per_file_graph(self.branch, file_id,
+ self.end_rev_id)
+ except errors.NoSuchId:
+ # File doesn't exist at end of range - fall back to old algorithm
+ view_revisions = None
+ else:
+ # Try iterating over the revisions given by the per-file graph.
+ # This returns None if it fails.
+ view_revisions = _calc_view_revisions_for_file(self.branch,
+ file_graph, graph_tip, self.start_rev_id, self.end_rev_id,
+ direction, multi_level)
+
+ if view_revisions is None:
+ # Get the base revisions, filtering by the revision range.
+ # Note that we always generate the merge revisions because
+ # filter_revisions_touching_file_id() requires them ...
+ view_revisions = _calc_view_revisions(self.branch,
+ self.start_rev_id, self.end_rev_id, direction, True)
+ if not isinstance(view_revisions, list):
+ view_revisions = list(view_revisions)
+ # TODO: pass in the already calculated file graph and re-use it
+ view_revisions = _filter_revisions_touching_file_id(self.branch,
+ file_id, view_revisions, include_merges=multi_level)
return make_log_rev_iterator(self.branch, view_revisions, rqst.get('delta_type'), rqst.get('message_search'))
+def _per_file_graph(branch, file_id, end_rev_id):
+ """Get the per file graph.
+
+ :param end_rev_id: the last interesting revision-id or None to use
+ the basis tree. If non-None, the file must exist in that revision
+ or NoSuchId will be raised.
+ :return: graph, tip where
+ graph is a Graph with (file_id,rev_id) tuple keys and
+ tip is the graph tip
+ """
+ # Find when the file was last modified
+ if end_rev_id is None:
+ rev_tree = branch.basis_tree()
+ else:
+ rev_tree = branch.repository.revision_tree(end_rev_id)
+ last_modified = rev_tree.inventory[file_id].revision
+
+ # Return the result
+ tip = (file_id, last_modified)
+ return graph.Graph(branch.repository.texts), tip
Should this be a branch method?
+
+
+def _calc_view_revisions_for_file(branch, file_graph, graph_tip, start_rev_id,
+ end_rev_id, direction, include_merges):
+ """Calculate the revisions to view for a file.
+
+ :param file_graph: the per-file graph
+ :param graph_tip: the tip of the per-file graph
+ :param include_merges: if True, include all revisions, not just the top
+ level
+ :return: An list of (revision_id, dotted_revno, merge_depth) tuples OR
+ None if the algorithm fails (and another one should be used).
+ """
+ br_revno, br_rev_id = branch.last_revision_info()
+ if br_revno == 0:
+ return []
+
+ # Find the revisions where the file was changed and merged
+ file_rev_ids = []
+ file_merges = []
+ for (_, rev_id), parents in file_graph.iter_ancestry([graph_tip]):
+ file_rev_ids.append(rev_id)
+ if len(parents) > 1:
+ file_merges.append(rev_id)
+
+ # Handle the simple cases
+ if len(file_rev_ids) == 1:
+ return _generate_one_revision(branch, file_rev_ids[0], br_rev_id,
+ br_revno)
+ elif len(file_rev_ids) == 0:
+ # Should this ever happen?
+ return []
+ elif file_merges and include_merges:
+ # Fall back to the old algorithm for now
+ return None
+
+ # Find all the revisions we can using a linear search
+ result = []
+ missing = set(file_rev_ids)
+ merges_to_search = 0
+ created_timestamp = None
+ try:
+ candidates = _linear_view_revisions(branch, start_rev_id, end_rev_id)
+ for index, (rev_id, revno, depth) in enumerate(candidates):
+ if rev_id in missing:
+ result.append((rev_id, revno, depth))
+ missing.remove(rev_id)
+ if len(missing) == 0:
+ break
+ else:
+ if _has_merges(branch, rev_id):
+ merges_to_search += 1
+ # If this is a dense tree, this optimisation is unlikely
+ # to result in a net win - fall back to old algorithm.
+ if merges_to_search > 100:
+ return None
+ # Check the timestamp to avoid going back too far on the
+ # mainline for files created in merge revisions. We don't
+ # do this every revision, just regularly, to minimise the
+ # number of revisions that we load at this point.
+ if index and index % 500 == 0:
+ if created_timestamp is None:
+ created_rev = branch.repository.get_revision(
+ file_rev_ids[-1])
+ created_timestamp = created_rev.timestamp
+ rev = branch.repository.get_revision(rev_id)
+ if created_timestamp > rev.timestamp:
+ return None
+
+ except _StartNotLinearAncestor:
+ raise errors.BzrCommandError('Start revision not found in'
+ ' left-hand history of end revision.')
If this causes a bug report, it'll be easier to debug if you include
some of the relevant variables in the message.
+
+ # If no merges were found in the revision range, then we can be
+ # certain that we've found all the revisions we care about.
+ if missing and merges_to_search:
+ # TODO: search the deltas of the merges, splicing successful
+ # matches into their rightful spots. That should work well on
+ # chk repositories for typical histories but we need to benchmark
+ # it to confirm. There's most likely a sweet spot above which
+ # the O(history) traditional way - generating the full graph of
+ # history and post-filtering - remains the best performer.
+ trace.mutter("log file fastpath failed to find %d revisions" %
+ len(missing))
+ return None
+
+ # We came, we saw, we walked away victorious ...
+ if direction == 'forward':
+ result = reversed(result)
+ return result
+
+
def _calc_view_revisions(branch, start_rev_id, end_rev_id, direction,
generate_merge_revisions, delayed_graph_generation=False):
"""Calculate the revisions to view.
I think you should test this on some files from MySQL or Launchpad,
which will have a longer but still very bushy history?
This looks fairly plausible to me, but I think there should be a unit
test for the fairly nontrivial function you've added. The approach here
of falling back to the old code means that any bugs here may not
actually be exercised by the test suite.
=== modified file 'NEWS'
--- NEWS 2009-06-16 09:05:34 +0000
+++ NEWS 2009-06-17 05:48:37 +0000
@@ -29,6 +29,10 @@
Improvements
************
+* ``bzr log FILE`` is now substantially faster on flat-ish histories.
+ On OpenOffice.org for example, logging a typical file now takes
+ a second or so instead of 29 seconds. (Ian Clatworthy)
+
* Resolving a revno to a revision id on a branch accessed via ``bzr://``
or ``bzr+ssh://`` is now much faster and involves no VFS operations.
This speeds up commands like ``bzr pull -r 123``. (Andrew Bennetts)
=== modified file 'bzrlib/ builtins. py' _format. supports_ chks
match_ using_deltas = (len(file_ids) != 1 or filter_by_dir
--- bzrlib/builtins.py 2009-06-15 06:47:14 +0000
+++ bzrlib/builtins.py 2009-06-16 12:46:50 +0000
@@ -2237,16 +2237,14 @@
# the underlying repository format is faster at generating
# deltas or can provide everything we need from the indices.
# The default algorithm - match-using-deltas - works for
- # multiple files and directories and is faster for small
- # amounts of history (200 revisions say). However, it's too
+ # multiple files and directories. However, it's too
# slow for logging a single file in a repository with deep
# history, i.e. > 10K revisions. In the spirit of "do no
# evil when adding features", we continue to use the
# original algorithm - per-file-graph - for the "single
# file that isn't a directory without showing a delta" case.
- partial_history = revision and b.repository.
- or delta_type or partial_history)
+ or delta_type)
# Build the LogRequest and execute it
if len(file_ids) == 0:
=== modified file 'bzrlib/log.py'
direction =rqst.get( 'direction' ))
--- bzrlib/log.py 2009-06-10 03:56:49 +0000
+++ bzrlib/log.py 2009-06-17 05:35:10 +0000
@@ -70,6 +70,7 @@
diff,
errors,
foreign,
+ graph,
repository as _mod_repository,
revision as _mod_revision,
revisionspec,
@@ -460,21 +461,150 @@
def _log_revision_ iterator_ using_per_ file_graph( self): revisions_ touching_ file_id( ) requires them ... revisions( self.branch, self.start_rev_id, 'direction' ), True) view_revisions, list): revisions) revisions_ touching_ file_id( self.branch, 'specific_ fileids' )[0], view_revisions, merges= rqst.get( 'levels' ) != 1) 'direction' ) 'specific_ fileids' )[0] graph(self. branch, file_id, revisions_ for_file( self.branch, revisions_ touching_ file_id( ) requires them ... revisions( self.branch, view_revisions, list): revisions) revisions_ touching_ file_id( self.branch, merges= multi_level) rev_iterator( self.branch, view_revisions,
rqst. get('delta_ type'), rqst.get( 'message_ search' ))
- # Get the base revisions, filtering by the revision range.
- # Note that we always generate the merge revisions because
- # filter_
rqst = self.rqst
- view_revisions = _calc_view_
- self.end_rev_id, rqst.get(
- if not isinstance(
- view_revisions = list(view_
- view_revisions = _filter_
- rqst.get(
- include_
+ direction = rqst.get(
+ file_id = rqst.get(
+ multi_level = rqst.get('levels') != 1
+ try:
+ file_graph, graph_tip = _per_file_
+ self.end_rev_id)
+ except errors.NoSuchId:
+ # File doesn't exist at end of range - fall back to old algorithm
+ view_revisions = None
+ else:
+ # Try iterating over the revisions given by the per-file graph.
+ # This returns None if it fails.
+ view_revisions = _calc_view_
+ file_graph, graph_tip, self.start_rev_id, self.end_rev_id,
+ direction, multi_level)
+
+ if view_revisions is None:
+ # Get the base revisions, filtering by the revision range.
+ # Note that we always generate the merge revisions because
+ # filter_
+ view_revisions = _calc_view_
+ self.start_rev_id, self.end_rev_id, direction, True)
+ if not isinstance(
+ view_revisions = list(view_
+ # TODO: pass in the already calculated file graph and re-use it
+ view_revisions = _filter_
+ file_id, view_revisions, include_
return make_log_
+def _per_file_ graph(branch, file_id, end_rev_id): repository. revision_ tree(end_ rev_id) inventory[ file_id] .revision branch. repository. texts), tip
+ """Get the per file graph.
+
+ :param end_rev_id: the last interesting revision-id or None to use
+ the basis tree. If non-None, the file must exist in that revision
+ or NoSuchId will be raised.
+ :return: graph, tip where
+ graph is a Graph with (file_id,rev_id) tuple keys and
+ tip is the graph tip
+ """
+ # Find when the file was last modified
+ if end_rev_id is None:
+ rev_tree = branch.basis_tree()
+ else:
+ rev_tree = branch.
+ last_modified = rev_tree.
+
+ # Return the result
+ tip = (file_id, last_modified)
+ return graph.Graph(
Should this be a branch method?
+ revisions_ for_file( branch, file_graph, graph_tip, start_rev_id, last_revision_ info() iter_ancestry( [graph_ tip]): ids.append( rev_id) append( rev_id) one_revision( branch, file_rev_ids[0], br_rev_id, view_revisions( branch, start_rev_id, end_rev_id) candidates) : append( (rev_id, revno, depth)) remove( rev_id) repository. get_revision( rev.timestamp repository. get_revision( rev_id) Ancestor: BzrCommandError ('Start revision not found in'
+
+def _calc_view_
+ end_rev_id, direction, include_merges):
+ """Calculate the revisions to view for a file.
+
+ :param file_graph: the per-file graph
+ :param graph_tip: the tip of the per-file graph
+ :param include_merges: if True, include all revisions, not just the top
+ level
+ :return: An list of (revision_id, dotted_revno, merge_depth) tuples OR
+ None if the algorithm fails (and another one should be used).
+ """
+ br_revno, br_rev_id = branch.
+ if br_revno == 0:
+ return []
+
+ # Find the revisions where the file was changed and merged
+ file_rev_ids = []
+ file_merges = []
+ for (_, rev_id), parents in file_graph.
+ file_rev_
+ if len(parents) > 1:
+ file_merges.
+
+ # Handle the simple cases
+ if len(file_rev_ids) == 1:
+ return _generate_
+ br_revno)
+ elif len(file_rev_ids) == 0:
+ # Should this ever happen?
+ return []
+ elif file_merges and include_merges:
+ # Fall back to the old algorithm for now
+ return None
+
+ # Find all the revisions we can using a linear search
+ result = []
+ missing = set(file_rev_ids)
+ merges_to_search = 0
+ created_timestamp = None
+ try:
+ candidates = _linear_
+ for index, (rev_id, revno, depth) in enumerate(
+ if rev_id in missing:
+ result.
+ missing.
+ if len(missing) == 0:
+ break
+ else:
+ if _has_merges(branch, rev_id):
+ merges_to_search += 1
+ # If this is a dense tree, this optimisation is unlikely
+ # to result in a net win - fall back to old algorithm.
+ if merges_to_search > 100:
+ return None
+ # Check the timestamp to avoid going back too far on the
+ # mainline for files created in merge revisions. We don't
+ # do this every revision, just regularly, to minimise the
+ # number of revisions that we load at this point.
+ if index and index % 500 == 0:
+ if created_timestamp is None:
+ created_rev = branch.
+ file_rev_ids[-1])
+ created_timestamp = created_
+ rev = branch.
+ if created_timestamp > rev.timestamp:
+ return None
+
+ except _StartNotLinear
+ raise errors.
+ ' left-hand history of end revision.')
If this causes a bug report, it'll be easier to debug if you include
some of the relevant variables in the message.
+ revisions( branch, start_rev_id, end_rev_id, direction, merge_revisions , delayed_ graph_generatio n=False) :
+ # If no merges were found in the revision range, then we can be
+ # certain that we've found all the revisions we care about.
+ if missing and merges_to_search:
+ # TODO: search the deltas of the merges, splicing successful
+ # matches into their rightful spots. That should work well on
+ # chk repositories for typical histories but we need to benchmark
+ # it to confirm. There's most likely a sweet spot above which
+ # the O(history) traditional way - generating the full graph of
+ # history and post-filtering - remains the best performer.
+ trace.mutter("log file fastpath failed to find %d revisions" %
+ len(missing))
+ return None
+
+ # We came, we saw, we walked away victorious ...
+ if direction == 'forward':
+ result = reversed(result)
+ return result
+
+
def _calc_view_
generate_
"""Calculate the revisions to view.