Merge lp:~jameinel/bzr/1.16-chkmap-updates into lp:~bzr/bzr/trunk-old

Proposed by John A Meinel
Status: Merged
Approved by: Ian Clatworthy
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~jameinel/bzr/1.16-chkmap-updates
Merge into: lp:~bzr/bzr/trunk-old
Diff against target: 278 lines
To merge this branch: bzr merge lp:~jameinel/bzr/1.16-chkmap-updates
Reviewer Review Type Date Requested Status
Ian Clatworthy Approve
Review via email: mp+7113@code.launchpad.net
To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

This is a slight rework of InternalNode._iter_nodes() to provide some shortcuts to finding nodes to return.

This changes the "initial commit" time for a mysql tree (7.8k files, 140MB) from 15.2s down to 13.7s on my machine (about 10% improvement.)

The big win here is that "CHKMap.apply_delta()" is adding all of the keys by calling .map() one at a time. For each of these, we have to look up what page it fits in, and possibly create the page. The lookup *was* being done by iterating all of the children of this InternalNode, and seeing if each child matched the search key.

This changes the search in 2 ways.

1) If we are searching for a single key, and that results in a _node_width search key, then we can do a single dict lookup for that item, and know if we have it or not, without building up *any* other structures.

2) If we are looking for multiple keys, we can aggregate their search keys. If at that point, all search keys are "_node_width", then again, we can do simple dict lookups for each key.

The main win here, is that root nodes are going to be 255-entries wide. So we avoid iterating across 255 items and doing a comparison, in exchange for doing a dict lookup for each item.

Part (1) is probably what makes the biggest improvement for 'bzr commit' time, but I think both are still useful improvments. (I'm guessing the latter will help stuff like 'bzr ls' more.)

Note that this, coupled with my fulltext commit changes bring initial commit time down to 12.7s, though --1.9 format commit is still around 11.7s. (But that is down from ~15s without those changes.)
So there are still bits of work to be done, but it is getting closer.

Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

This looks pretty good. I want to take another look when I'm a bit more awake though before approving it.

One small thing: you changed one instance of "type(node) == tuple" to "type(node) is tuple" but you missed 2 other spots in that routine. I'll finish reviewing it tomorrow.

Revision history for this message
John A Meinel (jameinel) wrote :

> One small thing: you changed one instance of "type(node) == tuple" to
> "type(node) is tuple" but you missed 2 other spots in that routine. I'll
> finish reviewing it tomorrow.

Sure, I wasn't on a crusade against "== tuple", I just happened to see one and fixed it. I'm happy to do the others.

Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

I'm awake now and it all still looks ok. :-) If you get a moment, tweaking the ''== tuple' to 'is tuple' would be nice as you're merging but it's not a big deal either way IIUIC.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'bzrlib/chk_map.py'
--- bzrlib/chk_map.py 2009-05-13 21:59:57 +0000
+++ bzrlib/chk_map.py 2009-06-15 15:35:24 +0000
@@ -121,7 +121,7 @@
121121
122 def _ensure_root(self):122 def _ensure_root(self):
123 """Ensure that the root node is an object not a key."""123 """Ensure that the root node is an object not a key."""
124 if type(self._root_node) == tuple:124 if type(self._root_node) is tuple:
125 # Demand-load the root125 # Demand-load the root
126 self._root_node = self._get_node(self._root_node)126 self._root_node = self._get_node(self._root_node)
127127
@@ -135,7 +135,7 @@
135 :param node: A tuple key or node object.135 :param node: A tuple key or node object.
136 :return: A node object.136 :return: A node object.
137 """137 """
138 if type(node) == tuple:138 if type(node) is tuple:
139 bytes = self._read_bytes(node)139 bytes = self._read_bytes(node)
140 return _deserialise(bytes, node,140 return _deserialise(bytes, node,
141 search_key_func=self._search_key_func)141 search_key_func=self._search_key_func)
@@ -465,7 +465,7 @@
465465
466 def _node_key(self, node):466 def _node_key(self, node):
467 """Get the key for a node whether it's a tuple or node."""467 """Get the key for a node whether it's a tuple or node."""
468 if type(node) == tuple:468 if type(node) is tuple:
469 return node469 return node
470 else:470 else:
471 return node._key471 return node._key
@@ -491,7 +491,7 @@
491491
492 :return: The key of the root node.492 :return: The key of the root node.
493 """493 """
494 if type(self._root_node) == tuple:494 if type(self._root_node) is tuple:
495 # Already saved.495 # Already saved.
496 return self._root_node496 return self._root_node
497 keys = list(self._root_node.serialise(self._store))497 keys = list(self._root_node.serialise(self._store))
@@ -955,34 +955,99 @@
955 # prefix is the key in self._items to use, key_filter is the key_filter955 # prefix is the key in self._items to use, key_filter is the key_filter
956 # entries that would match this node956 # entries that would match this node
957 keys = {}957 keys = {}
958 shortcut = False
958 if key_filter is None:959 if key_filter is None:
960 # yielding all nodes, yield whatever we have, and queue up a read
961 # for whatever we are missing
962 shortcut = True
959 for prefix, node in self._items.iteritems():963 for prefix, node in self._items.iteritems():
960 if type(node) == tuple:964 if node.__class__ is tuple:
961 keys[node] = (prefix, None)965 keys[node] = (prefix, None)
962 else:966 else:
963 yield node, None967 yield node, None
964 else:968 elif len(key_filter) == 1:
965 # XXX defaultdict ?969 # Technically, this path could also be handled by the first check
970 # in 'self._node_width' in length_filters. However, we can handle
971 # this case without spending any time building up the
972 # prefix_to_keys, etc state.
973
974 # This is a bit ugly, but TIMEIT showed it to be by far the fastest
975 # 0.626us list(key_filter)[0]
976 # is a func() for list(), 2 mallocs, and a getitem
977 # 0.489us [k for k in key_filter][0]
978 # still has the mallocs, avoids the func() call
979 # 0.350us iter(key_filter).next()
980 # has a func() call, and mallocs an iterator
981 # 0.125us for key in key_filter: pass
982 # no func() overhead, might malloc an iterator
983 # 0.105us for key in key_filter: break
984 # no func() overhead, might malloc an iterator, probably
985 # avoids checking an 'else' clause as part of the for
986 for key in key_filter:
987 break
988 search_prefix = self._search_prefix_filter(key)
989 if len(search_prefix) == self._node_width:
990 # This item will match exactly, so just do a dict lookup, and
991 # see what we can return
992 shortcut = True
993 try:
994 node = self._items[search_prefix]
995 except KeyError:
996 # A given key can only match 1 child node, if it isn't
997 # there, then we can just return nothing
998 return
999 if node.__class__ is tuple:
1000 keys[node] = (search_prefix, [key])
1001 else:
1002 # This is loaded, and the only thing that can match,
1003 # return
1004 yield node, [key]
1005 return
1006 if not shortcut:
1007 # First, convert all keys into a list of search prefixes
1008 # Aggregate common prefixes, and track the keys they come from
966 prefix_to_keys = {}1009 prefix_to_keys = {}
967 length_filters = {}1010 length_filters = {}
968 for key in key_filter:1011 for key in key_filter:
969 search_key = self._search_prefix_filter(key)1012 search_prefix = self._search_prefix_filter(key)
970 length_filter = length_filters.setdefault(1013 length_filter = length_filters.setdefault(
971 len(search_key), set())1014 len(search_prefix), set())
972 length_filter.add(search_key)1015 length_filter.add(search_prefix)
973 prefix_to_keys.setdefault(search_key, []).append(key)1016 prefix_to_keys.setdefault(search_prefix, []).append(key)
974 length_filters = length_filters.items()1017
975 for prefix, node in self._items.iteritems():1018 if (self._node_width in length_filters
976 node_key_filter = []1019 and len(length_filters) == 1):
977 for length, length_filter in length_filters:1020 # all of the search prefixes match exactly _node_width. This
978 sub_prefix = prefix[:length]1021 # means that everything is an exact match, and we can do a
979 if sub_prefix in length_filter:1022 # lookup into self._items, rather than iterating over the items
980 node_key_filter.extend(prefix_to_keys[sub_prefix])1023 # dict.
981 if node_key_filter: # this key matched something, yield it1024 search_prefixes = length_filters[self._node_width]
982 if type(node) == tuple:1025 for search_prefix in search_prefixes:
983 keys[node] = (prefix, node_key_filter)1026 try:
1027 node = self._items[search_prefix]
1028 except KeyError:
1029 # We can ignore this one
1030 continue
1031 node_key_filter = prefix_to_keys[search_prefix]
1032 if node.__class__ is tuple:
1033 keys[node] = (search_prefix, node_key_filter)
984 else:1034 else:
985 yield node, node_key_filter1035 yield node, node_key_filter
1036 else:
1037 # The slow way. We walk every item in self._items, and check to
1038 # see if there are any matches
1039 length_filters = length_filters.items()
1040 for prefix, node in self._items.iteritems():
1041 node_key_filter = []
1042 for length, length_filter in length_filters:
1043 sub_prefix = prefix[:length]
1044 if sub_prefix in length_filter:
1045 node_key_filter.extend(prefix_to_keys[sub_prefix])
1046 if node_key_filter: # this key matched something, yield it
1047 if node.__class__ is tuple:
1048 keys[node] = (prefix, node_key_filter)
1049 else:
1050 yield node, node_key_filter
986 if keys:1051 if keys:
987 # Look in the page cache for some more bytes1052 # Look in the page cache for some more bytes
988 found_keys = set()1053 found_keys = set()
@@ -1117,7 +1182,7 @@
1117 :return: An iterable of the keys inserted by this operation.1182 :return: An iterable of the keys inserted by this operation.
1118 """1183 """
1119 for node in self._items.itervalues():1184 for node in self._items.itervalues():
1120 if type(node) == tuple:1185 if type(node) is tuple:
1121 # Never deserialised.1186 # Never deserialised.
1122 continue1187 continue
1123 if node._key is not None:1188 if node._key is not None:
@@ -1134,7 +1199,7 @@
1134 lines.append('%s\n' % (self._search_prefix,))1199 lines.append('%s\n' % (self._search_prefix,))
1135 prefix_len = len(self._search_prefix)1200 prefix_len = len(self._search_prefix)
1136 for prefix, node in sorted(self._items.items()):1201 for prefix, node in sorted(self._items.items()):
1137 if type(node) == tuple:1202 if type(node) is tuple:
1138 key = node[0]1203 key = node[0]
1139 else:1204 else:
1140 key = node._key[0]1205 key = node._key[0]
@@ -1179,7 +1244,7 @@
1179 raise AssertionError("unserialised nodes have no refs.")1244 raise AssertionError("unserialised nodes have no refs.")
1180 refs = []1245 refs = []
1181 for value in self._items.itervalues():1246 for value in self._items.itervalues():
1182 if type(value) == tuple:1247 if type(value) is tuple:
1183 refs.append(value)1248 refs.append(value)
1184 else:1249 else:
1185 refs.append(value.key())1250 refs.append(value.key())
11861251
=== modified file 'bzrlib/tests/test_chk_map.py'
--- bzrlib/tests/test_chk_map.py 2009-05-13 21:59:57 +0000
+++ bzrlib/tests/test_chk_map.py 2009-06-15 15:35:24 +0000
@@ -1560,13 +1560,66 @@
1560 child.map(None, ("baz",), "val")1560 child.map(None, ("baz",), "val")
1561 node.add_node("b", child)1561 node.add_node("b", child)
15621562
1563 key_filter = (('foo',), ('fob',), ('bar',), ('baz',))1563 # Note that 'ram' doesn't match anything, so it should be freely
1564 # ignored
1565 key_filter = (('foo',), ('fob',), ('bar',), ('baz',), ('ram',))
1564 for child, node_key_filter in node._iter_nodes(None,1566 for child, node_key_filter in node._iter_nodes(None,
1565 key_filter=key_filter):1567 key_filter=key_filter):
1566 # each child could matches two key filters, so make sure they were1568 # each child could match two key filters, so make sure they were
1567 # both included.1569 # both included.
1568 self.assertEqual(2, len(node_key_filter))1570 self.assertEqual(2, len(node_key_filter))
15691571
1572 def make_fo_fa_node(self):
1573 node = InternalNode('f')
1574 child = LeafNode()
1575 child.set_maximum_size(100)
1576 child.map(None, ("foo",), "val")
1577 child.map(None, ("fob",), "val")
1578 node.add_node('fo', child)
1579 child = LeafNode()
1580 child.set_maximum_size(100)
1581 child.map(None, ("far",), "val")
1582 child.map(None, ("faz",), "val")
1583 node.add_node("fa", child)
1584 return node
1585
1586 def test__iter_nodes_single_entry(self):
1587 node = self.make_fo_fa_node()
1588 key_filter = [('foo',)]
1589 nodes = list(node._iter_nodes(None, key_filter=key_filter))
1590 self.assertEqual(1, len(nodes))
1591 self.assertEqual(key_filter, nodes[0][1])
1592
1593 def test__iter_nodes_single_entry_misses(self):
1594 node = self.make_fo_fa_node()
1595 key_filter = [('bar',)]
1596 nodes = list(node._iter_nodes(None, key_filter=key_filter))
1597 self.assertEqual(0, len(nodes))
1598
1599 def test__iter_nodes_mixed_key_width(self):
1600 node = self.make_fo_fa_node()
1601 key_filter = [('foo', 'bar'), ('foo',), ('fo',), ('b',)]
1602 nodes = list(node._iter_nodes(None, key_filter=key_filter))
1603 self.assertEqual(1, len(nodes))
1604 matches = key_filter[:]
1605 matches.remove(('b',))
1606 self.assertEqual(sorted(matches), sorted(nodes[0][1]))
1607
1608 def test__iter_nodes_match_all(self):
1609 node = self.make_fo_fa_node()
1610 key_filter = [('foo', 'bar'), ('foo',), ('fo',), ('f',)]
1611 nodes = list(node._iter_nodes(None, key_filter=key_filter))
1612 self.assertEqual(2, len(nodes))
1613
1614 def test__iter_nodes_fixed_widths_and_misses(self):
1615 node = self.make_fo_fa_node()
1616 # foo and faa should both match one child, baz should miss
1617 key_filter = [('foo',), ('faa',), ('baz',)]
1618 nodes = list(node._iter_nodes(None, key_filter=key_filter))
1619 self.assertEqual(2, len(nodes))
1620 for node, matches in nodes:
1621 self.assertEqual(1, len(matches))
1622
1570 def test_iteritems_empty_new(self):1623 def test_iteritems_empty_new(self):
1571 node = InternalNode()1624 node = InternalNode()
1572 self.assertEqual([], sorted(node.iteritems(None)))1625 self.assertEqual([], sorted(node.iteritems(None)))
15731626
=== modified file 'bzrlib/workingtree.py'
--- bzrlib/workingtree.py 2009-06-10 03:56:49 +0000
+++ bzrlib/workingtree.py 2009-06-15 15:35:24 +0000
@@ -451,7 +451,7 @@
451 path = self.id2path(file_id)451 path = self.id2path(file_id)
452 file_obj = self.get_file_byname(path, filtered=False)452 file_obj = self.get_file_byname(path, filtered=False)
453 stat_value = _fstat(file_obj.fileno())453 stat_value = _fstat(file_obj.fileno())
454 if self.supports_content_filtering() and filtered:454 if filtered and self.supports_content_filtering():
455 filters = self._content_filter_stack(path)455 filters = self._content_filter_stack(path)
456 file_obj = filtered_input_file(file_obj, filters)456 file_obj = filtered_input_file(file_obj, filters)
457 return (file_obj, stat_value)457 return (file_obj, stat_value)
@@ -462,7 +462,7 @@
462 def get_file_byname(self, filename, filtered=True):462 def get_file_byname(self, filename, filtered=True):
463 path = self.abspath(filename)463 path = self.abspath(filename)
464 f = file(path, 'rb')464 f = file(path, 'rb')
465 if self.supports_content_filtering() and filtered:465 if filtered and self.supports_content_filtering():
466 filters = self._content_filter_stack(filename)466 filters = self._content_filter_stack(filename)
467 return filtered_input_file(f, filters)467 return filtered_input_file(f, filters)
468 else:468 else: