~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/__init__.py

merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Copyright (C) 2005, 2006 Canonical Ltd
2
 
 
 
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
164
164
    return (scheme, username, password, host, port, path)
165
165
 
166
166
 
 
167
class _CoalescedOffset(object):
 
168
    """A data container for keeping track of coalesced offsets."""
 
169
 
 
170
    __slots__ = ['start', 'length', 'ranges']
 
171
 
 
172
    def __init__(self, start, length, ranges):
 
173
        self.start = start
 
174
        self.length = length
 
175
        self.ranges = ranges
 
176
 
 
177
    def __cmp__(self, other):
 
178
        return cmp((self.start, self.length, self.ranges),
 
179
                   (other.start, other.length, other.ranges))
 
180
 
 
181
 
167
182
class Transport(object):
168
183
    """This class encapsulates methods for retrieving or putting a file
169
184
    from/to a storage location.
176
191
    as an argument (ie always iterate, never index)
177
192
    """
178
193
 
 
194
    # implementations can override this if it is more efficient
 
195
    # for them to combine larger read chunks together
 
196
    _max_readv_combine = 50
 
197
    # It is better to read this much more data in order, rather
 
198
    # than doing another seek. Even for the local filesystem,
 
199
    # there is a benefit in just reading.
 
200
    # TODO: jam 20060714 Do some real benchmarking to figure out
 
201
    #       where the biggest benefit between combining reads and
 
202
    #       and seeking is. Consider a runtime auto-tune.
 
203
    _bytes_to_read_before_seek = 0
 
204
 
179
205
    def __init__(self, base):
180
206
        super(Transport, self).__init__()
181
207
        self.base = base
307
333
        
308
334
        Note that some transports MAY allow querying on directories, but this
309
335
        is not part of the protocol.  In other words, the results of 
310
 
        t.has("a_directory_name") are undefined."
 
336
        t.has("a_directory_name") are undefined.
311
337
        """
312
338
        raise NotImplementedError(self.has)
313
339
 
353
379
        :offsets: A list of (offset, size) tuples.
354
380
        :return: A list or generator of (offset, data) tuples
355
381
        """
356
 
        def do_combined_read(combined_offsets):
357
 
            total_size = 0
358
 
            for offset, size in combined_offsets:
359
 
                total_size += size
360
 
            mutter('readv coalesced %d reads.', len(combined_offsets))
361
 
            offset = combined_offsets[0][0]
362
 
            fp.seek(offset)
363
 
            data = fp.read(total_size)
364
 
            pos = 0
365
 
            for offset, size in combined_offsets:
366
 
                yield offset, data[pos:pos + size]
367
 
                pos += size
 
382
        if not offsets:
 
383
            return
368
384
 
369
 
        if not len(offsets):
370
 
            return
371
385
        fp = self.get(relpath)
372
 
        pending_offsets = deque(offsets)
373
 
        combined_offsets = []
374
 
        while len(pending_offsets):
375
 
            offset, size = pending_offsets.popleft()
376
 
            if not combined_offsets:
377
 
                combined_offsets = [[offset, size]]
 
386
        return self._seek_and_read(fp, offsets)
 
387
 
 
388
    def _seek_and_read(self, fp, offsets):
 
389
        """An implementation of readv that uses fp.seek and fp.read.
 
390
 
 
391
        This uses _coalesce_offsets to issue larger reads and fewer seeks.
 
392
 
 
393
        :param fp: A file-like object that supports seek() and read(size)
 
394
        :param offsets: A list of offsets to be read from the given file.
 
395
        :return: yield (pos, data) tuples for each request
 
396
        """
 
397
        # We are going to iterate multiple times, we need a list
 
398
        offsets = list(offsets)
 
399
        sorted_offsets = sorted(offsets)
 
400
 
 
401
        # turn the list of offsets into a stack
 
402
        offset_stack = iter(offsets)
 
403
        cur_offset_and_size = offset_stack.next()
 
404
        coalesced = self._coalesce_offsets(sorted_offsets,
 
405
                               limit=self._max_readv_combine,
 
406
                               fudge_factor=self._bytes_to_read_before_seek)
 
407
 
 
408
        # Cache the results, but only until they have been fulfilled
 
409
        data_map = {}
 
410
        for c_offset in coalesced:
 
411
            # TODO: jam 20060724 it might be faster to not issue seek if 
 
412
            #       we are already at the right location. This should be
 
413
            #       benchmarked.
 
414
            fp.seek(c_offset.start)
 
415
            data = fp.read(c_offset.length)
 
416
            for suboffset, subsize in c_offset.ranges:
 
417
                key = (c_offset.start+suboffset, subsize)
 
418
                data_map[key] = data[suboffset:suboffset+subsize]
 
419
 
 
420
            # Now that we've read some data, see if we can yield anything back
 
421
            while cur_offset_and_size in data_map:
 
422
                this_data = data_map.pop(cur_offset_and_size)
 
423
                yield cur_offset_and_size[0], this_data
 
424
                cur_offset_and_size = offset_stack.next()
 
425
 
 
426
    @staticmethod
 
427
    def _coalesce_offsets(offsets, limit, fudge_factor):
 
428
        """Yield coalesced offsets.
 
429
 
 
430
        With a long list of neighboring requests, combine them
 
431
        into a single large request, while retaining the original
 
432
        offsets.
 
433
        Turns  [(15, 10), (25, 10)] => [(15, 20, [(0, 10), (10, 10)])]
 
434
 
 
435
        :param offsets: A list of (start, length) pairs
 
436
        :param limit: Only combine a maximum of this many pairs
 
437
                      Some transports penalize multiple reads more than
 
438
                      others, and sometimes it is better to return early.
 
439
                      0 means no limit
 
440
        :param fudge_factor: All transports have some level of 'it is
 
441
                better to read some more data and throw it away rather 
 
442
                than seek', so collapse if we are 'close enough'
 
443
        :return: yield _CoalescedOffset objects, which have members for wher
 
444
                to start, how much to read, and how to split those 
 
445
                chunks back up
 
446
        """
 
447
        last_end = None
 
448
        cur = _CoalescedOffset(None, None, [])
 
449
 
 
450
        for start, size in offsets:
 
451
            end = start + size
 
452
            if (last_end is not None 
 
453
                and start <= last_end + fudge_factor
 
454
                and start >= cur.start
 
455
                and (limit <= 0 or len(cur.ranges) < limit)):
 
456
                cur.length = end - cur.start
 
457
                cur.ranges.append((start-cur.start, size))
378
458
            else:
379
 
                if (len (combined_offsets) < 50 and
380
 
                    combined_offsets[-1][0] + combined_offsets[-1][1] == offset):
381
 
                    # combatible offset:
382
 
                    combined_offsets.append([offset, size])
383
 
                else:
384
 
                    # incompatible, or over the threshold issue a read and yield
385
 
                    pending_offsets.appendleft((offset, size))
386
 
                    for result in do_combined_read(combined_offsets):
387
 
                        yield result
388
 
                    combined_offsets = []
389
 
        # whatever is left is a single coalesced request
390
 
        if len(combined_offsets):
391
 
            for result in do_combined_read(combined_offsets):
392
 
                yield result
 
459
                if cur.start is not None:
 
460
                    yield cur
 
461
                cur = _CoalescedOffset(start, size, [(0, size)])
 
462
            last_end = end
 
463
 
 
464
        if cur.start is not None:
 
465
            yield cur
 
466
 
 
467
        return
393
468
 
394
469
    def get_multi(self, relpaths, pb=None):
395
470
        """Get a list of file-like objects, one for each entry in relpaths.