~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/_known_graph_pyx.pyx

Some code cleanup passes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2009 Canonical Ltd
 
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 2 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, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Implementation of Graph algorithms when we have already loaded everything.
 
18
"""
 
19
 
 
20
cdef extern from "python-compat.h":
 
21
    pass
 
22
 
 
23
cdef extern from "Python.h":
 
24
    ctypedef int Py_ssize_t
 
25
    ctypedef struct PyObject:
 
26
        pass
 
27
 
 
28
    int PyString_CheckExact(object)
 
29
 
 
30
    int PyObject_RichCompareBool(object, object, int)
 
31
    int Py_LT
 
32
 
 
33
    int PyTuple_CheckExact(object)
 
34
    object PyTuple_New(Py_ssize_t n)
 
35
    Py_ssize_t PyTuple_GET_SIZE(object t)
 
36
    PyObject * PyTuple_GET_ITEM(object t, Py_ssize_t o)
 
37
    void PyTuple_SET_ITEM(object t, Py_ssize_t o, object v)
 
38
 
 
39
    int PyList_CheckExact(object)
 
40
    Py_ssize_t PyList_GET_SIZE(object l)
 
41
    PyObject * PyList_GET_ITEM(object l, Py_ssize_t o)
 
42
    int PyList_SetItem(object l, Py_ssize_t o, object l) except -1
 
43
    int PyList_Append(object l, object v) except -1
 
44
 
 
45
    int PyDict_CheckExact(object d)
 
46
    Py_ssize_t PyDict_Size(object d) except -1
 
47
    PyObject * PyDict_GetItem(object d, object k)
 
48
    int PyDict_SetItem(object d, object k, object v) except -1
 
49
    int PyDict_DelItem(object d, object k) except -1
 
50
    int PyDict_Next(object d, Py_ssize_t *pos, PyObject **k, PyObject **v)
 
51
 
 
52
    void Py_INCREF(object)
 
53
 
 
54
import gc
 
55
 
 
56
from bzrlib import errors, revision
 
57
 
 
58
cdef object NULL_REVISION
 
59
NULL_REVISION = revision.NULL_REVISION
 
60
 
 
61
 
 
62
cdef class _KnownGraphNode:
 
63
    """Represents a single object in the known graph."""
 
64
 
 
65
    cdef object key
 
66
    cdef object parents
 
67
    cdef object children
 
68
    cdef public long gdfo
 
69
    cdef int seen
 
70
    cdef object extra
 
71
 
 
72
    def __init__(self, key):
 
73
        self.key = key
 
74
        self.parents = None
 
75
 
 
76
        self.children = []
 
77
        # Greatest distance from origin
 
78
        self.gdfo = -1
 
79
        self.seen = 0
 
80
        self.extra = None
 
81
 
 
82
    property child_keys:
 
83
        def __get__(self):
 
84
            cdef _KnownGraphNode child
 
85
 
 
86
            keys = []
 
87
            for child in self.children:
 
88
                PyList_Append(keys, child.key)
 
89
            return keys
 
90
 
 
91
    property parent_keys:
 
92
        def __get__(self):
 
93
            if self.parents is None:
 
94
                return None
 
95
            
 
96
            cdef _KnownGraphNode parent
 
97
 
 
98
            keys = []
 
99
            for parent in self.parents:
 
100
                PyList_Append(keys, parent.key)
 
101
            return keys
 
102
    
 
103
    cdef clear_references(self):
 
104
        self.parents = None
 
105
        self.children = None
 
106
 
 
107
    def __repr__(self):
 
108
        cdef _KnownGraphNode node
 
109
 
 
110
        parent_keys = []
 
111
        if self.parents is not None:
 
112
            for node in self.parents:
 
113
                parent_keys.append(node.key)
 
114
        child_keys = []
 
115
        if self.children is not None:
 
116
            for node in self.children:
 
117
                child_keys.append(node.key)
 
118
        return '%s(%s  gdfo:%s par:%s child:%s)' % (
 
119
            self.__class__.__name__, self.key, self.gdfo,
 
120
            parent_keys, child_keys)
 
121
 
 
122
 
 
123
cdef _KnownGraphNode _get_list_node(lst, Py_ssize_t pos):
 
124
    cdef PyObject *temp_node
 
125
 
 
126
    temp_node = PyList_GET_ITEM(lst, pos)
 
127
    return <_KnownGraphNode>temp_node
 
128
 
 
129
 
 
130
cdef _KnownGraphNode _get_tuple_node(tpl, Py_ssize_t pos):
 
131
    cdef PyObject *temp_node
 
132
 
 
133
    temp_node = PyTuple_GET_ITEM(tpl, pos)
 
134
    return <_KnownGraphNode>temp_node
 
135
 
 
136
 
 
137
def get_key(node):
 
138
    cdef _KnownGraphNode real_node
 
139
    real_node = node
 
140
    return real_node.key
 
141
 
 
142
 
 
143
cdef object _sort_list_nodes(object lst_or_tpl, int reverse):
 
144
    """Sort a list of _KnownGraphNode objects.
 
145
 
 
146
    If lst_or_tpl is a list, it is allowed to mutate in place. It may also
 
147
    just return the input list if everything is already sorted.
 
148
    """
 
149
    cdef _KnownGraphNode node1, node2
 
150
    cdef int do_swap, is_tuple
 
151
    cdef Py_ssize_t length
 
152
 
 
153
    is_tuple = PyTuple_CheckExact(lst_or_tpl)
 
154
    if not (is_tuple or PyList_CheckExact(lst_or_tpl)):
 
155
        raise TypeError('lst_or_tpl must be a list or tuple.')
 
156
    length = len(lst_or_tpl)
 
157
    if length == 0 or length == 1:
 
158
        return lst_or_tpl
 
159
    if length == 2:
 
160
        if is_tuple:
 
161
            node1 = _get_tuple_node(lst_or_tpl, 0)
 
162
            node2 = _get_tuple_node(lst_or_tpl, 1)
 
163
        else:
 
164
            node1 = _get_list_node(lst_or_tpl, 0)
 
165
            node2 = _get_list_node(lst_or_tpl, 1)
 
166
        if reverse:
 
167
            do_swap = PyObject_RichCompareBool(node1.key, node2.key, Py_LT)
 
168
        else:
 
169
            do_swap = PyObject_RichCompareBool(node2.key, node1.key, Py_LT)
 
170
        if not do_swap:
 
171
            return lst_or_tpl
 
172
        if is_tuple:
 
173
            return (node2, node1)
 
174
        else:
 
175
            # Swap 'in-place', since lists are mutable
 
176
            Py_INCREF(node1)
 
177
            PyList_SetItem(lst_or_tpl, 1, node1)
 
178
            Py_INCREF(node2)
 
179
            PyList_SetItem(lst_or_tpl, 0, node2)
 
180
            return lst_or_tpl
 
181
    # For all other sizes, we just use 'sorted()'
 
182
    if is_tuple:
 
183
        # Note that sorted() is just list(iterable).sort()
 
184
        lst_or_tpl = list(lst_or_tpl)
 
185
    lst_or_tpl.sort(key=get_key, reverse=reverse)
 
186
    return lst_or_tpl
 
187
 
 
188
 
 
189
cdef class _MergeSorter
 
190
 
 
191
cdef class KnownGraph:
 
192
    """This is a class which assumes we already know the full graph."""
 
193
 
 
194
    cdef public object _nodes
 
195
    cdef object _known_heads
 
196
    cdef public int do_cache
 
197
 
 
198
    def __init__(self, parent_map, do_cache=True):
 
199
        """Create a new KnownGraph instance.
 
200
 
 
201
        :param parent_map: A dictionary mapping key => parent_keys
 
202
        """
 
203
        # tests at pre-allocating the node dict actually slowed things down
 
204
        self._nodes = {}
 
205
        # Maps {sorted(revision_id, revision_id): heads}
 
206
        self._known_heads = {}
 
207
        self.do_cache = int(do_cache)
 
208
        # TODO: consider disabling gc since we are allocating a lot of nodes
 
209
        #       that won't be collectable anyway. real world testing has not
 
210
        #       shown a specific impact, yet.
 
211
        self._initialize_nodes(parent_map)
 
212
        self._find_gdfo()
 
213
 
 
214
    def __dealloc__(self):
 
215
        cdef _KnownGraphNode child
 
216
        cdef Py_ssize_t pos
 
217
        cdef PyObject *temp_node
 
218
 
 
219
        while PyDict_Next(self._nodes, &pos, NULL, &temp_node):
 
220
            child = <_KnownGraphNode>temp_node
 
221
            child.clear_references()
 
222
 
 
223
    cdef _KnownGraphNode _get_or_create_node(self, key):
 
224
        cdef PyObject *temp_node
 
225
        cdef _KnownGraphNode node
 
226
 
 
227
        temp_node = PyDict_GetItem(self._nodes, key)
 
228
        if temp_node == NULL:
 
229
            node = _KnownGraphNode(key)
 
230
            PyDict_SetItem(self._nodes, key, node)
 
231
        else:
 
232
            node = <_KnownGraphNode>temp_node
 
233
        return node
 
234
 
 
235
    def _initialize_nodes(self, parent_map):
 
236
        """Populate self._nodes.
 
237
 
 
238
        After this has finished:
 
239
        - self._nodes will have an entry for every entry in parent_map.
 
240
        - ghosts will have a parent_keys = None,
 
241
        - all nodes found will also have child_keys populated with all known
 
242
          child keys,
 
243
        """
 
244
        cdef PyObject *temp_key, *temp_parent_keys, *temp_node
 
245
        cdef Py_ssize_t pos, pos2, num_parent_keys
 
246
        cdef _KnownGraphNode node
 
247
        cdef _KnownGraphNode parent_node
 
248
 
 
249
        if not PyDict_CheckExact(parent_map):
 
250
            raise TypeError('parent_map should be a dict of {key:parent_keys}')
 
251
        # for key, parent_keys in parent_map.iteritems():
 
252
        pos = 0
 
253
        while PyDict_Next(parent_map, &pos, &temp_key, &temp_parent_keys):
 
254
            key = <object>temp_key
 
255
            parent_keys = <object>temp_parent_keys
 
256
            num_parent_keys = len(parent_keys)
 
257
            node = self._get_or_create_node(key)
 
258
            # We know how many parents, so we pre allocate the tuple
 
259
            parent_nodes = PyTuple_New(num_parent_keys)
 
260
            for pos2 from 0 <= pos2 < num_parent_keys:
 
261
                # Note: it costs us 10ms out of 40ms to lookup all of these
 
262
                #       parents, it doesn't seem to be an allocation overhead,
 
263
                #       but rather a lookup overhead. There doesn't seem to be
 
264
                #       a way around it, and that is one reason why
 
265
                #       KnownGraphNode maintains a direct pointer to the parent
 
266
                #       node.
 
267
                # We use [] because parent_keys may be a tuple or list
 
268
                parent_node = self._get_or_create_node(parent_keys[pos2])
 
269
                # PyTuple_SET_ITEM will steal a reference, so INCREF first
 
270
                Py_INCREF(parent_node)
 
271
                PyTuple_SET_ITEM(parent_nodes, pos2, parent_node)
 
272
                PyList_Append(parent_node.children, node)
 
273
            node.parents = parent_nodes
 
274
 
 
275
    def _find_tails(self):
 
276
        cdef PyObject *temp_node
 
277
        cdef _KnownGraphNode node
 
278
        cdef Py_ssize_t pos
 
279
 
 
280
        tails = []
 
281
        pos = 0
 
282
        while PyDict_Next(self._nodes, &pos, NULL, &temp_node):
 
283
            node = <_KnownGraphNode>temp_node
 
284
            if node.parents is None or PyTuple_GET_SIZE(node.parents) == 0:
 
285
                node.gdfo = 1
 
286
                PyList_Append(tails, node)
 
287
        return tails
 
288
 
 
289
    def _find_tips(self):
 
290
        cdef PyObject *temp_node
 
291
        cdef _KnownGraphNode node
 
292
        cdef Py_ssize_t pos
 
293
 
 
294
        tips = []
 
295
        pos = 0
 
296
        while PyDict_Next(self._nodes, &pos, NULL, &temp_node):
 
297
            node = <_KnownGraphNode>temp_node
 
298
            if PyList_GET_SIZE(node.children) == 0:
 
299
                PyList_Append(tips, node)
 
300
        return tips
 
301
 
 
302
    def _find_gdfo(self):
 
303
        cdef _KnownGraphNode node
 
304
        cdef _KnownGraphNode child
 
305
        cdef PyObject *temp
 
306
        cdef Py_ssize_t pos
 
307
        cdef int replace
 
308
        cdef Py_ssize_t last_item
 
309
        cdef long next_gdfo
 
310
 
 
311
        pending = self._find_tails()
 
312
 
 
313
        last_item = PyList_GET_SIZE(pending) - 1
 
314
        while last_item >= 0:
 
315
            # Avoid pop followed by push, instead, peek, and replace
 
316
            # timing shows this is 930ms => 770ms for OOo
 
317
            node = _get_list_node(pending, last_item)
 
318
            last_item = last_item - 1
 
319
            next_gdfo = node.gdfo + 1
 
320
            for pos from 0 <= pos < PyList_GET_SIZE(node.children):
 
321
                child = _get_list_node(node.children, pos)
 
322
                if next_gdfo > child.gdfo:
 
323
                    child.gdfo = next_gdfo
 
324
                child.seen = child.seen + 1
 
325
                if child.seen == PyTuple_GET_SIZE(child.parents):
 
326
                    # This child is populated, queue it to be walked
 
327
                    last_item = last_item + 1
 
328
                    if last_item < PyList_GET_SIZE(pending):
 
329
                        Py_INCREF(child) # SetItem steals a ref
 
330
                        PyList_SetItem(pending, last_item, child)
 
331
                    else:
 
332
                        PyList_Append(pending, child)
 
333
                    # We have queued this node, we don't need to track it
 
334
                    # anymore
 
335
                    child.seen = 0
 
336
 
 
337
    def heads(self, keys):
 
338
        """Return the heads from amongst keys.
 
339
 
 
340
        This is done by searching the ancestries of each key.  Any key that is
 
341
        reachable from another key is not returned; all the others are.
 
342
 
 
343
        This operation scales with the relative depth between any two keys. It
 
344
        uses gdfo to avoid walking all ancestry.
 
345
 
 
346
        :param keys: An iterable of keys.
 
347
        :return: A set of the heads. Note that as a set there is no ordering
 
348
            information. Callers will need to filter their input to create
 
349
            order if they need it.
 
350
        """
 
351
        cdef PyObject *maybe_node
 
352
        cdef PyObject *maybe_heads
 
353
        cdef PyObject *temp_node
 
354
        cdef _KnownGraphNode node
 
355
        cdef Py_ssize_t pos, last_item
 
356
        cdef long min_gdfo
 
357
 
 
358
        heads_key = frozenset(keys)
 
359
        maybe_heads = PyDict_GetItem(self._known_heads, heads_key)
 
360
        if maybe_heads != NULL:
 
361
            return <object>maybe_heads
 
362
        # Not cached, compute it ourselves
 
363
        candidate_nodes = {}
 
364
        for key in keys:
 
365
            maybe_node = PyDict_GetItem(self._nodes, key)
 
366
            if maybe_node == NULL:
 
367
                raise KeyError('key %s not in nodes' % (key,))
 
368
            PyDict_SetItem(candidate_nodes, key, <object>maybe_node)
 
369
        maybe_node = PyDict_GetItem(candidate_nodes, NULL_REVISION)
 
370
        if maybe_node != NULL:
 
371
            # NULL_REVISION is only a head if it is the only entry
 
372
            candidate_nodes.pop(NULL_REVISION)
 
373
            if not candidate_nodes:
 
374
                return frozenset([NULL_REVISION])
 
375
            # The keys changed, so recalculate heads_key
 
376
            heads_key = frozenset(candidate_nodes)
 
377
        if PyDict_Size(candidate_nodes) < 2:
 
378
            return heads_key
 
379
 
 
380
        cleanup = []
 
381
        pending = []
 
382
        # we know a gdfo cannot be longer than a linear chain of all nodes
 
383
        min_gdfo = PyDict_Size(self._nodes) + 1
 
384
        # Build up nodes that need to be walked, note that starting nodes are
 
385
        # not added to seen()
 
386
        pos = 0
 
387
        while PyDict_Next(candidate_nodes, &pos, NULL, &temp_node):
 
388
            node = <_KnownGraphNode>temp_node
 
389
            if node.parents is not None:
 
390
                pending.extend(node.parents)
 
391
            if node.gdfo < min_gdfo:
 
392
                min_gdfo = node.gdfo
 
393
 
 
394
        # Now do all the real work
 
395
        last_item = PyList_GET_SIZE(pending) - 1
 
396
        while last_item >= 0:
 
397
            node = _get_list_node(pending, last_item)
 
398
            last_item = last_item - 1
 
399
            if node.seen:
 
400
                # node already appears in some ancestry
 
401
                continue
 
402
            PyList_Append(cleanup, node)
 
403
            node.seen = 1
 
404
            if node.gdfo <= min_gdfo:
 
405
                continue
 
406
            if node.parents is not None and PyTuple_GET_SIZE(node.parents) > 0:
 
407
                for pos from 0 <= pos < PyTuple_GET_SIZE(node.parents):
 
408
                    parent_node = _get_tuple_node(node.parents, pos)
 
409
                    last_item = last_item + 1
 
410
                    if last_item < PyList_GET_SIZE(pending):
 
411
                        Py_INCREF(parent_node) # SetItem steals a ref
 
412
                        PyList_SetItem(pending, last_item, parent_node)
 
413
                    else:
 
414
                        PyList_Append(pending, parent_node)
 
415
        heads = []
 
416
        pos = 0
 
417
        while PyDict_Next(candidate_nodes, &pos, NULL, &temp_node):
 
418
            node = <_KnownGraphNode>temp_node
 
419
            if not node.seen:
 
420
                PyList_Append(heads, node.key)
 
421
        heads = frozenset(heads)
 
422
        for pos from 0 <= pos < PyList_GET_SIZE(cleanup):
 
423
            node = _get_list_node(cleanup, pos)
 
424
            node.seen = 0
 
425
        if self.do_cache:
 
426
            PyDict_SetItem(self._known_heads, heads_key, heads)
 
427
        return heads
 
428
 
 
429
    def topo_sort(self):
 
430
        """Return the nodes in topological order.
 
431
 
 
432
        All parents must occur before all children.
 
433
        """
 
434
        # This is, for the most part, the same iteration order that we used for
 
435
        # _find_gdfo, consider finding a way to remove the duplication
 
436
        # In general, we find the 'tails' (nodes with no parents), and then
 
437
        # walk to the children. For children that have all of their parents
 
438
        # yielded, we queue up the child to be yielded as well.
 
439
        cdef _KnownGraphNode node
 
440
        cdef _KnownGraphNode child
 
441
        cdef PyObject *temp
 
442
        cdef Py_ssize_t pos
 
443
        cdef int replace
 
444
        cdef Py_ssize_t last_item
 
445
 
 
446
        pending = self._find_tails()
 
447
        if PyList_GET_SIZE(pending) == 0 and len(self._nodes) > 0:
 
448
            raise errors.GraphCycleError(self._nodes)
 
449
 
 
450
        topo_order = []
 
451
 
 
452
        last_item = PyList_GET_SIZE(pending) - 1
 
453
        while last_item >= 0:
 
454
            # Avoid pop followed by push, instead, peek, and replace
 
455
            # timing shows this is 930ms => 770ms for OOo
 
456
            node = _get_list_node(pending, last_item)
 
457
            last_item = last_item - 1
 
458
            if node.parents is not None:
 
459
                # We don't include ghost parents
 
460
                PyList_Append(topo_order, node.key)
 
461
            for pos from 0 <= pos < PyList_GET_SIZE(node.children):
 
462
                child = _get_list_node(node.children, pos)
 
463
                if child.gdfo == -1:
 
464
                    # We know we have a graph cycle because a node has a parent
 
465
                    # which we couldn't find
 
466
                    raise errors.GraphCycleError(self._nodes)
 
467
                child.seen = child.seen + 1
 
468
                if child.seen == PyTuple_GET_SIZE(child.parents):
 
469
                    # All parents of this child have been yielded, queue this
 
470
                    # one to be yielded as well
 
471
                    last_item = last_item + 1
 
472
                    if last_item < PyList_GET_SIZE(pending):
 
473
                        Py_INCREF(child) # SetItem steals a ref
 
474
                        PyList_SetItem(pending, last_item, child)
 
475
                    else:
 
476
                        PyList_Append(pending, child)
 
477
                    # We have queued this node, we don't need to track it
 
478
                    # anymore
 
479
                    child.seen = 0
 
480
        # We started from the parents, so we don't need to do anymore work
 
481
        return topo_order
 
482
 
 
483
    def gc_sort(self):
 
484
        """Return a reverse topological ordering which is 'stable'.
 
485
 
 
486
        There are a few constraints:
 
487
          1) Reverse topological (all children before all parents)
 
488
          2) Grouped by prefix
 
489
          3) 'stable' sorting, so that we get the same result, independent of
 
490
             machine, or extra data.
 
491
        To do this, we use the same basic algorithm as topo_sort, but when we
 
492
        aren't sure what node to access next, we sort them lexicographically.
 
493
        """
 
494
        cdef PyObject *temp
 
495
        cdef Py_ssize_t pos, last_item
 
496
        cdef _KnownGraphNode node, node2, parent_node
 
497
 
 
498
        tips = self._find_tips()
 
499
        # Split the tips based on prefix
 
500
        prefix_tips = {}
 
501
        for pos from 0 <= pos < PyList_GET_SIZE(tips):
 
502
            node = _get_list_node(tips, pos)
 
503
            if PyString_CheckExact(node.key) or len(node.key) == 1:
 
504
                prefix = ''
 
505
            else:
 
506
                prefix = node.key[0]
 
507
            temp = PyDict_GetItem(prefix_tips, prefix)
 
508
            if temp == NULL:
 
509
                prefix_tips[prefix] = [node]
 
510
            else:
 
511
                tip_nodes = <object>temp
 
512
                PyList_Append(tip_nodes, node)
 
513
 
 
514
        result = []
 
515
        for prefix in sorted(prefix_tips):
 
516
            temp = PyDict_GetItem(prefix_tips, prefix)
 
517
            assert temp != NULL
 
518
            tip_nodes = <object>temp
 
519
            pending = _sort_list_nodes(tip_nodes, 1)
 
520
            last_item = PyList_GET_SIZE(pending) - 1
 
521
            while last_item >= 0:
 
522
                node = _get_list_node(pending, last_item)
 
523
                last_item = last_item - 1
 
524
                if node.parents is None:
 
525
                    # Ghost
 
526
                    continue
 
527
                PyList_Append(result, node.key)
 
528
                # Sorting the parent keys isn't strictly necessary for stable
 
529
                # sorting of a given graph. But it does help minimize the
 
530
                # differences between graphs
 
531
                # For bzr.dev ancestry:
 
532
                #   4.73ms  no sort
 
533
                #   7.73ms  RichCompareBool sort
 
534
                parents = _sort_list_nodes(node.parents, 1)
 
535
                for pos from 0 <= pos < len(parents):
 
536
                    if PyTuple_CheckExact(parents):
 
537
                        parent_node = _get_tuple_node(parents, pos)
 
538
                    else:
 
539
                        parent_node = _get_list_node(parents, pos)
 
540
                    # TODO: GraphCycle detection
 
541
                    parent_node.seen = parent_node.seen + 1
 
542
                    if (parent_node.seen
 
543
                        == PyList_GET_SIZE(parent_node.children)):
 
544
                        # All children have been processed, queue up this
 
545
                        # parent
 
546
                        last_item = last_item + 1
 
547
                        if last_item < PyList_GET_SIZE(pending):
 
548
                            Py_INCREF(parent_node) # SetItem steals a ref
 
549
                            PyList_SetItem(pending, last_item, parent_node)
 
550
                        else:
 
551
                            PyList_Append(pending, parent_node)
 
552
                        parent_node.seen = 0
 
553
        return result
 
554
 
 
555
    def merge_sort(self, tip_key):
 
556
        """Compute the merge sorted graph output."""
 
557
        cdef _MergeSorter sorter
 
558
 
 
559
        # TODO: consider disabling gc since we are allocating a lot of nodes
 
560
        #       that won't be collectable anyway. real world testing has not
 
561
        #       shown a specific impact, yet.
 
562
        sorter = _MergeSorter(self, tip_key)
 
563
        return sorter.topo_order()
 
564
    
 
565
    def get_parent_keys(self, key):
 
566
        """Get the parents for a key
 
567
        
 
568
        Returns a list containg the parents keys. If the key is a ghost,
 
569
        None is returned. A KeyError will be raised if the key is not in
 
570
        the graph.
 
571
        
 
572
        :param keys: Key to check (eg revision_id)
 
573
        :return: A list of parents
 
574
        """
 
575
        return self._nodes[key].parent_keys 
 
576
 
 
577
    def get_child_keys(self, key):
 
578
        """Get the children for a key
 
579
        
 
580
        Returns a list containg the children keys. A KeyError will be raised
 
581
        if the key is not in the graph.
 
582
        
 
583
        :param keys: Key to check (eg revision_id)
 
584
        :return: A list of children
 
585
        """
 
586
        return self._nodes[key].child_keys    
 
587
 
 
588
 
 
589
cdef class _MergeSortNode:
 
590
    """Tracks information about a node during the merge_sort operation."""
 
591
 
 
592
    # Public api
 
593
    cdef public object key
 
594
    cdef public long merge_depth
 
595
    cdef public object end_of_merge # True/False Is this the end of the current merge
 
596
 
 
597
    # Private api, used while computing the information
 
598
    cdef _KnownGraphNode left_parent
 
599
    cdef _KnownGraphNode left_pending_parent
 
600
    cdef object pending_parents # list of _KnownGraphNode for non-left parents
 
601
    cdef long _revno_first
 
602
    cdef long _revno_second
 
603
    cdef long _revno_last
 
604
    # TODO: turn these into flag/bit fields rather than individual members
 
605
    cdef int is_first_child # Is this the first child?
 
606
    cdef int seen_by_child # A child node has seen this parent
 
607
    cdef int completed # Fully Processed
 
608
 
 
609
    def __init__(self, key):
 
610
        self.key = key
 
611
        self.merge_depth = -1
 
612
        self.left_parent = None
 
613
        self.left_pending_parent = None
 
614
        self.pending_parents = None
 
615
        self._revno_first = -1
 
616
        self._revno_second = -1
 
617
        self._revno_last = -1
 
618
        self.is_first_child = 0
 
619
        self.seen_by_child = 0
 
620
        self.completed = 0
 
621
 
 
622
    def __repr__(self):
 
623
        return '%s(%s depth:%s rev:%s,%s,%s first:%s seen:%s)' % (
 
624
            self.__class__.__name__, self.key,
 
625
            self.merge_depth,
 
626
            self._revno_first, self._revno_second, self._revno_last,
 
627
            self.is_first_child, self.seen_by_child)
 
628
 
 
629
    cdef int has_pending_parents(self):
 
630
        if self.left_pending_parent is not None or self.pending_parents:
 
631
            return 1
 
632
        return 0
 
633
 
 
634
    cdef object _revno(self):
 
635
        if self._revno_first == -1:
 
636
            if self._revno_second != -1:
 
637
                raise RuntimeError('Something wrong with: %s' % (self,))
 
638
            return (self._revno_last,)
 
639
        else:
 
640
            return (self._revno_first, self._revno_second, self._revno_last)
 
641
 
 
642
    property revno:
 
643
        def __get__(self):
 
644
            return self._revno()
 
645
 
 
646
 
 
647
cdef class _MergeSorter:
 
648
    """This class does the work of computing the merge_sort ordering.
 
649
 
 
650
    We have some small advantages, in that we get all the extra information
 
651
    that KnownGraph knows, like knowing the child lists, etc.
 
652
    """
 
653
 
 
654
    # Current performance numbers for merge_sort(bzr_dev_parent_map):
 
655
    #  302ms tsort.merge_sort()
 
656
    #   91ms graph.KnownGraph().merge_sort()
 
657
    #   40ms kg.merge_sort()
 
658
 
 
659
    cdef KnownGraph graph
 
660
    cdef object _depth_first_stack  # list
 
661
    cdef Py_ssize_t _last_stack_item # offset to last item on stack
 
662
    # cdef object _ms_nodes # dict of key => _MergeSortNode
 
663
    cdef object _revno_to_branch_count # {revno => num child branches}
 
664
    cdef object _scheduled_nodes # List of nodes ready to be yielded
 
665
 
 
666
    def __init__(self, known_graph, tip_key):
 
667
        cdef _KnownGraphNode node
 
668
 
 
669
        self.graph = known_graph
 
670
        # self._ms_nodes = {}
 
671
        self._revno_to_branch_count = {}
 
672
        self._depth_first_stack = []
 
673
        self._last_stack_item = -1
 
674
        self._scheduled_nodes = []
 
675
        if (tip_key is not None and tip_key != NULL_REVISION
 
676
            and tip_key != (NULL_REVISION,)):
 
677
            node = self.graph._nodes[tip_key]
 
678
            self._push_node(node, 0)
 
679
 
 
680
    cdef _MergeSortNode _get_ms_node(self, _KnownGraphNode node):
 
681
        cdef PyObject *temp_node
 
682
        cdef _MergeSortNode ms_node
 
683
 
 
684
        if node.extra is None:
 
685
            ms_node = _MergeSortNode(node.key)
 
686
            node.extra = ms_node
 
687
        else:
 
688
            ms_node = <_MergeSortNode>node.extra
 
689
        return ms_node
 
690
 
 
691
    cdef _push_node(self, _KnownGraphNode node, long merge_depth):
 
692
        cdef _KnownGraphNode parent_node
 
693
        cdef _MergeSortNode ms_node, ms_parent_node
 
694
        cdef Py_ssize_t pos
 
695
 
 
696
        ms_node = self._get_ms_node(node)
 
697
        ms_node.merge_depth = merge_depth
 
698
        if node.parents is None:
 
699
            raise RuntimeError('ghost nodes should not be pushed'
 
700
                               ' onto the stack: %s' % (node,))
 
701
        if PyTuple_GET_SIZE(node.parents) > 0:
 
702
            parent_node = _get_tuple_node(node.parents, 0)
 
703
            ms_node.left_parent = parent_node
 
704
            if parent_node.parents is None: # left-hand ghost
 
705
                ms_node.left_pending_parent = None
 
706
                ms_node.left_parent = None
 
707
            else:
 
708
                ms_node.left_pending_parent = parent_node
 
709
        if PyTuple_GET_SIZE(node.parents) > 1:
 
710
            ms_node.pending_parents = []
 
711
            for pos from 1 <= pos < PyTuple_GET_SIZE(node.parents):
 
712
                parent_node = _get_tuple_node(node.parents, pos)
 
713
                if parent_node.parents is None: # ghost
 
714
                    continue
 
715
                PyList_Append(ms_node.pending_parents, parent_node)
 
716
 
 
717
        ms_node.is_first_child = 1
 
718
        if ms_node.left_parent is not None:
 
719
            ms_parent_node = self._get_ms_node(ms_node.left_parent)
 
720
            if ms_parent_node.seen_by_child:
 
721
                ms_node.is_first_child = 0
 
722
            ms_parent_node.seen_by_child = 1
 
723
        self._last_stack_item = self._last_stack_item + 1
 
724
        if self._last_stack_item < PyList_GET_SIZE(self._depth_first_stack):
 
725
            Py_INCREF(node) # SetItem steals a ref
 
726
            PyList_SetItem(self._depth_first_stack, self._last_stack_item,
 
727
                           node)
 
728
        else:
 
729
            PyList_Append(self._depth_first_stack, node)
 
730
 
 
731
    cdef _pop_node(self):
 
732
        cdef PyObject *temp
 
733
        cdef _MergeSortNode ms_node, ms_parent_node, ms_prev_node
 
734
        cdef _KnownGraphNode node, parent_node, prev_node
 
735
 
 
736
        node = _get_list_node(self._depth_first_stack, self._last_stack_item)
 
737
        ms_node = <_MergeSortNode>node.extra
 
738
        self._last_stack_item = self._last_stack_item - 1
 
739
        if ms_node.left_parent is not None:
 
740
            # Assign the revision number from the left-hand parent
 
741
            ms_parent_node = <_MergeSortNode>ms_node.left_parent.extra
 
742
            if ms_node.is_first_child:
 
743
                # First child just increments the final digit
 
744
                ms_node._revno_first = ms_parent_node._revno_first
 
745
                ms_node._revno_second = ms_parent_node._revno_second
 
746
                ms_node._revno_last = ms_parent_node._revno_last + 1
 
747
            else:
 
748
                # Not the first child, make a new branch
 
749
                #  (mainline_revno, branch_count, 1)
 
750
                if ms_parent_node._revno_first == -1:
 
751
                    # Mainline ancestor, the increment is on the last digit
 
752
                    base_revno = ms_parent_node._revno_last
 
753
                else:
 
754
                    base_revno = ms_parent_node._revno_first
 
755
                temp = PyDict_GetItem(self._revno_to_branch_count,
 
756
                                      base_revno)
 
757
                if temp == NULL:
 
758
                    branch_count = 1
 
759
                else:
 
760
                    branch_count = (<object>temp) + 1
 
761
                PyDict_SetItem(self._revno_to_branch_count, base_revno,
 
762
                               branch_count)
 
763
                ms_node._revno_first = base_revno
 
764
                ms_node._revno_second = branch_count
 
765
                ms_node._revno_last = 1
 
766
        else:
 
767
            temp = PyDict_GetItem(self._revno_to_branch_count, 0)
 
768
            if temp == NULL:
 
769
                # The first root node doesn't have a 3-digit revno
 
770
                root_count = 0
 
771
                ms_node._revno_first = -1
 
772
                ms_node._revno_second = -1
 
773
                ms_node._revno_last = 1
 
774
            else:
 
775
                root_count = (<object>temp) + 1
 
776
                ms_node._revno_first = 0
 
777
                ms_node._revno_second = root_count
 
778
                ms_node._revno_last = 1
 
779
            PyDict_SetItem(self._revno_to_branch_count, 0, root_count)
 
780
        ms_node.completed = 1
 
781
        if PyList_GET_SIZE(self._scheduled_nodes) == 0:
 
782
            # The first scheduled node is always the end of merge
 
783
            ms_node.end_of_merge = True
 
784
        else:
 
785
            prev_node = _get_list_node(self._scheduled_nodes,
 
786
                                    PyList_GET_SIZE(self._scheduled_nodes) - 1)
 
787
            ms_prev_node = <_MergeSortNode>prev_node.extra
 
788
            if ms_prev_node.merge_depth < ms_node.merge_depth:
 
789
                # The previously pushed node is to our left, so this is the end
 
790
                # of this right-hand chain
 
791
                ms_node.end_of_merge = True
 
792
            elif (ms_prev_node.merge_depth == ms_node.merge_depth
 
793
                  and prev_node not in node.parents):
 
794
                # The next node is not a direct parent of this node
 
795
                ms_node.end_of_merge = True
 
796
            else:
 
797
                ms_node.end_of_merge = False
 
798
        PyList_Append(self._scheduled_nodes, node)
 
799
 
 
800
    cdef _schedule_stack(self):
 
801
        cdef _KnownGraphNode last_node, next_node
 
802
        cdef _MergeSortNode ms_node, ms_last_node, ms_next_node
 
803
        cdef long next_merge_depth
 
804
        ordered = []
 
805
        while self._last_stack_item >= 0:
 
806
            # Peek at the last item on the stack
 
807
            last_node = _get_list_node(self._depth_first_stack,
 
808
                                       self._last_stack_item)
 
809
            if last_node.gdfo == -1:
 
810
                # if _find_gdfo skipped a node, that means there is a graph
 
811
                # cycle, error out now
 
812
                raise errors.GraphCycleError(self.graph._nodes)
 
813
            ms_last_node = <_MergeSortNode>last_node.extra
 
814
            if not ms_last_node.has_pending_parents():
 
815
                # Processed all parents, pop this node
 
816
                self._pop_node()
 
817
                continue
 
818
            while ms_last_node.has_pending_parents():
 
819
                if ms_last_node.left_pending_parent is not None:
 
820
                    # recurse depth first into the primary parent
 
821
                    next_node = ms_last_node.left_pending_parent
 
822
                    ms_last_node.left_pending_parent = None
 
823
                else:
 
824
                    # place any merges in right-to-left order for scheduling
 
825
                    # which gives us left-to-right order after we reverse
 
826
                    # the scheduled queue.
 
827
                    # Note: This has the effect of allocating common-new
 
828
                    #       revisions to the right-most subtree rather than the
 
829
                    #       left most, which will display nicely (you get
 
830
                    #       smaller trees at the top of the combined merge).
 
831
                    next_node = ms_last_node.pending_parents.pop()
 
832
                ms_next_node = self._get_ms_node(next_node)
 
833
                if ms_next_node.completed:
 
834
                    # this parent was completed by a child on the
 
835
                    # call stack. skip it.
 
836
                    continue
 
837
                # otherwise transfer it from the source graph into the
 
838
                # top of the current depth first search stack.
 
839
 
 
840
                if next_node is ms_last_node.left_parent:
 
841
                    next_merge_depth = ms_last_node.merge_depth
 
842
                else:
 
843
                    next_merge_depth = ms_last_node.merge_depth + 1
 
844
                self._push_node(next_node, next_merge_depth)
 
845
                # and do not continue processing parents until this 'call'
 
846
                # has recursed.
 
847
                break
 
848
 
 
849
    cdef topo_order(self):
 
850
        cdef _MergeSortNode ms_node
 
851
        cdef _KnownGraphNode node
 
852
        cdef Py_ssize_t pos
 
853
        cdef PyObject *temp_key, *temp_node
 
854
 
 
855
        # Note: allocating a _MergeSortNode and deallocating it for all nodes
 
856
        #       costs approx 8.52ms (21%) of the total runtime
 
857
        #       We might consider moving the attributes into the base
 
858
        #       KnownGraph object.
 
859
        self._schedule_stack()
 
860
 
 
861
        # We've set up the basic schedule, now we can continue processing the
 
862
        # output.
 
863
        # Note: This final loop costs us 40.0ms => 28.8ms (11ms, 25%) on
 
864
        #       bzr.dev, to convert the internal Object representation into a
 
865
        #       Tuple representation...
 
866
        #       2ms is walking the data and computing revno tuples
 
867
        #       7ms is computing the return tuple
 
868
        #       4ms is PyList_Append()
 
869
        ordered = []
 
870
        # output the result in reverse order, and separate the generated info
 
871
        for pos from PyList_GET_SIZE(self._scheduled_nodes) > pos >= 0:
 
872
            node = _get_list_node(self._scheduled_nodes, pos)
 
873
            ms_node = <_MergeSortNode>node.extra
 
874
            PyList_Append(ordered, ms_node)
 
875
            node.extra = None
 
876
        # Clear out the scheduled nodes now that we're done
 
877
        self._scheduled_nodes = []
 
878
        return ordered