~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/hashcache.py

  • Committer: Martin Pool
  • Date: 2005-09-13 05:22:41 UTC
  • Revision ID: mbp@sourcefrog.net-20050913052241-52dbd8e8ced620f6
- better BZR_DEBUG trace output

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# (C) 2005 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
# TODO: Up-front, stat all files in order and remove those which are deleted or 
 
18
# out-of-date.  Don't actually re-read them until they're needed.  That ought 
 
19
# to bring all the inodes into core so that future stats to them are fast, and 
 
20
# it preserves the nice property that any caller will always get up-to-date
 
21
# data except in unavoidable cases.
 
22
 
 
23
# TODO: Perhaps return more details on the file to avoid statting it
 
24
# again: nonexistent, file type, size, etc
 
25
 
 
26
# TODO: Perhaps use a Python pickle instead of a text file; might be faster.
 
27
 
 
28
 
 
29
 
 
30
CACHE_HEADER = "### bzr hashcache v5\n"
 
31
 
 
32
import os, stat, time
 
33
 
 
34
from bzrlib.osutils import sha_file
 
35
from bzrlib.trace import mutter, warning
 
36
from bzrlib.atomicfile import AtomicFile
 
37
 
 
38
 
 
39
 
 
40
 
 
41
def _fingerprint(abspath):
 
42
    try:
 
43
        fs = os.lstat(abspath)
 
44
    except OSError:
 
45
        # might be missing, etc
 
46
        return None
 
47
 
 
48
    if stat.S_ISDIR(fs.st_mode):
 
49
        return None
 
50
 
 
51
    # we discard any high precision because it's not reliable; perhaps we
 
52
    # could do better on some systems?
 
53
    return (fs.st_size, long(fs.st_mtime),
 
54
            long(fs.st_ctime), fs.st_ino, fs.st_dev)
 
55
 
 
56
 
 
57
class HashCache(object):
 
58
    """Cache for looking up file SHA-1.
 
59
 
 
60
    Files are considered to match the cached value if the fingerprint
 
61
    of the file has not changed.  This includes its mtime, ctime,
 
62
    device number, inode number, and size.  This should catch
 
63
    modifications or replacement of the file by a new one.
 
64
 
 
65
    This may not catch modifications that do not change the file's
 
66
    size and that occur within the resolution window of the
 
67
    timestamps.  To handle this we specifically do not cache files
 
68
    which have changed since the start of the present second, since
 
69
    they could undetectably change again.
 
70
 
 
71
    This scheme may fail if the machine's clock steps backwards.
 
72
    Don't do that.
 
73
 
 
74
    This does not canonicalize the paths passed in; that should be
 
75
    done by the caller.
 
76
 
 
77
    _cache
 
78
        Indexed by path, points to a two-tuple of the SHA-1 of the file.
 
79
        and its fingerprint.
 
80
 
 
81
    stat_count
 
82
        number of times files have been statted
 
83
 
 
84
    hit_count
 
85
        number of times files have been retrieved from the cache, avoiding a
 
86
        re-read
 
87
        
 
88
    miss_count
 
89
        number of misses (times files have been completely re-read)
 
90
    """
 
91
    needs_write = False
 
92
 
 
93
    def __init__(self, basedir):
 
94
        self.basedir = basedir
 
95
        self.hit_count = 0
 
96
        self.miss_count = 0
 
97
        self.stat_count = 0
 
98
        self.danger_count = 0
 
99
        self.removed_count = 0
 
100
        self.update_count = 0
 
101
        self._cache = {}
 
102
 
 
103
 
 
104
    def cache_file_name(self):
 
105
        return os.sep.join([self.basedir, '.bzr', 'stat-cache'])
 
106
 
 
107
 
 
108
 
 
109
 
 
110
    def clear(self):
 
111
        """Discard all cached information.
 
112
 
 
113
        This does not reset the counters."""
 
114
        if self._cache:
 
115
            self.needs_write = True
 
116
            self._cache = {}
 
117
 
 
118
 
 
119
    def scan(self):
 
120
        """Scan all files and remove entries where the cache entry is obsolete.
 
121
        
 
122
        Obsolete entries are those where the file has been modified or deleted
 
123
        since the entry was inserted.        
 
124
        """
 
125
        prep = [(ce[1][3], path, ce) for (path, ce) in self._cache.iteritems()]
 
126
        prep.sort()
 
127
        
 
128
        for inum, path, cache_entry in prep:
 
129
            abspath = os.sep.join([self.basedir, path])
 
130
            fp = _fingerprint(abspath)
 
131
            self.stat_count += 1
 
132
            
 
133
            cache_fp = cache_entry[1]
 
134
    
 
135
            if (not fp) or (cache_fp != fp):
 
136
                # not here or not a regular file anymore
 
137
                self.removed_count += 1
 
138
                self.needs_write = True
 
139
                del self._cache[path]
 
140
 
 
141
 
 
142
 
 
143
    def get_sha1(self, path):
 
144
        """Return the sha1 of a file.
 
145
        """
 
146
        abspath = os.sep.join([self.basedir, path])
 
147
        self.stat_count += 1
 
148
        file_fp = _fingerprint(abspath)
 
149
        
 
150
        if not file_fp:
 
151
            # not a regular file or not existing
 
152
            if path in self._cache:
 
153
                self.removed_count += 1
 
154
                self.needs_write = True
 
155
                del self._cache[path]
 
156
            return None        
 
157
 
 
158
        if path in self._cache:
 
159
            cache_sha1, cache_fp = self._cache[path]
 
160
        else:
 
161
            cache_sha1, cache_fp = None, None
 
162
 
 
163
        if cache_fp == file_fp:
 
164
            self.hit_count += 1
 
165
            return cache_sha1
 
166
        
 
167
        self.miss_count += 1
 
168
        digest = sha_file(file(abspath, 'rb', buffering=65000))
 
169
 
 
170
        now = int(time.time())
 
171
        if file_fp[1] >= now or file_fp[2] >= now:
 
172
            # changed too recently; can't be cached.  we can
 
173
            # return the result and it could possibly be cached
 
174
            # next time.
 
175
            self.danger_count += 1 
 
176
            if cache_fp:
 
177
                self.removed_count += 1
 
178
                self.needs_write = True
 
179
                del self._cache[path]
 
180
        else:
 
181
            self.update_count += 1
 
182
            self.needs_write = True
 
183
            self._cache[path] = (digest, file_fp)
 
184
        
 
185
        return digest
 
186
        
 
187
 
 
188
 
 
189
 
 
190
    def write(self):
 
191
        """Write contents of cache to file."""
 
192
        outf = AtomicFile(self.cache_file_name(), 'wb')
 
193
        try:
 
194
            print >>outf, CACHE_HEADER,
 
195
 
 
196
            for path, c  in self._cache.iteritems():
 
197
                assert '//' not in path, path
 
198
                outf.write(path.encode('utf-8'))
 
199
                outf.write('// ')
 
200
                print >>outf, c[0],     # hex sha1
 
201
                for fld in c[1]:
 
202
                    print >>outf, "%d" % fld,
 
203
                print >>outf
 
204
 
 
205
            outf.commit()
 
206
            self.needs_write = False
 
207
        finally:
 
208
            if not outf.closed:
 
209
                outf.abort()
 
210
        
 
211
 
 
212
 
 
213
    def read(self):
 
214
        """Reinstate cache from file.
 
215
 
 
216
        Overwrites existing cache.
 
217
 
 
218
        If the cache file has the wrong version marker, this just clears 
 
219
        the cache."""
 
220
        self._cache = {}
 
221
 
 
222
        fn = self.cache_file_name()
 
223
        try:
 
224
            inf = file(fn, 'rb', buffering=65000)
 
225
        except IOError, e:
 
226
            mutter("failed to open %s: %s" % (fn, e))
 
227
            # better write it now so it is valid
 
228
            self.needs_write = True
 
229
            return
 
230
 
 
231
 
 
232
        hdr = inf.readline()
 
233
        if hdr != CACHE_HEADER:
 
234
            mutter('cache header marker not found at top of %s; discarding cache'
 
235
                   % fn)
 
236
            self.needs_write = True
 
237
            return
 
238
 
 
239
        for l in inf:
 
240
            pos = l.index('// ')
 
241
            path = l[:pos].decode('utf-8')
 
242
            if path in self._cache:
 
243
                warning('duplicated path %r in cache' % path)
 
244
                continue
 
245
 
 
246
            pos += 3
 
247
            fields = l[pos:].split(' ')
 
248
            if len(fields) != 6:
 
249
                warning("bad line in hashcache: %r" % l)
 
250
                continue
 
251
 
 
252
            sha1 = fields[0]
 
253
            if len(sha1) != 40:
 
254
                warning("bad sha1 in hashcache: %r" % sha1)
 
255
                continue
 
256
 
 
257
            fp = tuple(map(long, fields[1:]))
 
258
 
 
259
            self._cache[path] = (sha1, fp)
 
260
 
 
261
        self.needs_write = False
 
262
           
 
263
 
 
264
 
 
265