~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/hashcache.py

  • Committer: Martin Pool
  • Date: 2005-08-12 15:41:44 UTC
  • Revision ID: mbp@sourcefrog.net-20050812154144-bc98570a78b8f633
- merge in deferred revfile work

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