/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/_known_graph_pyx.pyx

[merge] robertc's integration, updated tests to check for retcode=3

Show diffs side-by-side

added added

removed removed

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