blob: 19a2e3090527f988d9dd33d3a124164c96710420 [file] [log] [blame]
Owen Lina2fba682011-08-17 22:07:43 +08001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17// This is an on-disk cache which maps a 64-bits key to a byte array.
18//
19// It consists of three files: one index file and two data files. One of the
20// data files is "active", and the other is "inactive". New entries are
21// appended into the active region until it reaches the size limit. At that
22// point the active file and the inactive file are swapped, and the new active
23// file is truncated to empty (and the index for that file is also cleared).
24// The index is a hash table with linear probing. When the load factor reaches
25// 0.5, it does the same thing like when the size limit is reached.
26//
27// The index file format: (all numbers are stored in little-endian)
28// [0] Magic number: 0xB3273030
29// [4] MaxEntries: Max number of hash entries per region.
30// [8] MaxBytes: Max number of data bytes per region (including header).
31// [12] ActiveRegion: The active growing region: 0 or 1.
32// [16] ActiveEntries: The number of hash entries used in the active region.
33// [20] ActiveBytes: The number of data bytes used in the active region.
34// [24] Version number.
35// [28] Checksum of [0..28).
36// [32] Hash entries for region 0. The size is X = (12 * MaxEntries bytes).
37// [32 + X] Hash entries for region 1. The size is also X.
38//
39// Each hash entry is 12 bytes: 8 bytes key and 4 bytes offset into the data
40// file. The offset is 0 when the slot is free. Note that 0 is a valid value
41// for key. The keys are used directly as index into a hash table, so they
42// should be suitably distributed.
43//
44// Each data file stores data for one region. The data file is concatenated
45// blobs followed by the magic number 0xBD248510.
46//
47// The blob format:
48// [0] Key of this blob
49// [8] Checksum of this blob
50// [12] Offset of this blob
51// [16] Length of this blob (not including header)
52// [20] Blob
53//
54// Below are the interface for BlobCache. The instance of this class does not
55// support concurrent use by multiple threads.
56//
57// public BlobCache(String path, int maxEntries, int maxBytes, boolean reset) throws IOException;
58// public void insert(long key, byte[] data) throws IOException;
59// public byte[] lookup(long key) throws IOException;
60// public void lookup(LookupRequest req) throws IOException;
61// public void close();
62// public void syncIndex();
63// public void syncAll();
64// public static void deleteFiles(String path);
65//
66package com.android.gallery3d.common;
67
68import android.util.Log;
69
70import java.io.Closeable;
71import java.io.File;
72import java.io.IOException;
73import java.io.RandomAccessFile;
74import java.nio.ByteOrder;
75import java.nio.MappedByteBuffer;
76import java.nio.channels.FileChannel;
77import java.util.zip.Adler32;
78
79public class BlobCache {
80 private static final String TAG = "BlobCache";
81
82 private static final int MAGIC_INDEX_FILE = 0xB3273030;
83 private static final int MAGIC_DATA_FILE = 0xBD248510;
84
85 // index header offset
86 private static final int IH_MAGIC = 0;
87 private static final int IH_MAX_ENTRIES = 4;
88 private static final int IH_MAX_BYTES = 8;
89 private static final int IH_ACTIVE_REGION = 12;
90 private static final int IH_ACTIVE_ENTRIES = 16;
91 private static final int IH_ACTIVE_BYTES = 20;
92 private static final int IH_VERSION = 24;
93 private static final int IH_CHECKSUM = 28;
94 private static final int INDEX_HEADER_SIZE = 32;
95
96 private static final int DATA_HEADER_SIZE = 4;
97
98 // blob header offset
99 private static final int BH_KEY = 0;
100 private static final int BH_CHECKSUM = 8;
101 private static final int BH_OFFSET = 12;
102 private static final int BH_LENGTH = 16;
103 private static final int BLOB_HEADER_SIZE = 20;
104
105 private RandomAccessFile mIndexFile;
106 private RandomAccessFile mDataFile0;
107 private RandomAccessFile mDataFile1;
108 private FileChannel mIndexChannel;
109 private MappedByteBuffer mIndexBuffer;
110
111 private int mMaxEntries;
112 private int mMaxBytes;
113 private int mActiveRegion;
114 private int mActiveEntries;
115 private int mActiveBytes;
116 private int mVersion;
117
118 private RandomAccessFile mActiveDataFile;
119 private RandomAccessFile mInactiveDataFile;
120 private int mActiveHashStart;
121 private int mInactiveHashStart;
122 private byte[] mIndexHeader = new byte[INDEX_HEADER_SIZE];
123 private byte[] mBlobHeader = new byte[BLOB_HEADER_SIZE];
124 private Adler32 mAdler32 = new Adler32();
125
126 // Creates the cache. Three files will be created:
127 // path + ".idx", path + ".0", and path + ".1"
128 // The ".0" file and the ".1" file each stores data for a region. Each of
129 // them can grow to the size specified by maxBytes. The maxEntries parameter
130 // specifies the maximum number of entries each region can have. If the
131 // "reset" parameter is true, the cache will be cleared before use.
132 public BlobCache(String path, int maxEntries, int maxBytes, boolean reset)
133 throws IOException {
134 this(path, maxEntries, maxBytes, reset, 0);
135 }
136
137 public BlobCache(String path, int maxEntries, int maxBytes, boolean reset,
138 int version) throws IOException {
139 mIndexFile = new RandomAccessFile(path + ".idx", "rw");
140 mDataFile0 = new RandomAccessFile(path + ".0", "rw");
141 mDataFile1 = new RandomAccessFile(path + ".1", "rw");
142 mVersion = version;
143
144 if (!reset && loadIndex()) {
145 return;
146 }
147
148 resetCache(maxEntries, maxBytes);
149
150 if (!loadIndex()) {
151 closeAll();
152 throw new IOException("unable to load index");
153 }
154 }
155
156 // Delete the files associated with the given path previously created
157 // by the BlobCache constructor.
158 public static void deleteFiles(String path) {
159 deleteFileSilently(path + ".idx");
160 deleteFileSilently(path + ".0");
161 deleteFileSilently(path + ".1");
162 }
163
164 private static void deleteFileSilently(String path) {
165 try {
166 new File(path).delete();
167 } catch (Throwable t) {
168 // ignore;
169 }
170 }
171
172 // Close the cache. All resources are released. No other method should be
173 // called after this is called.
174 public void close() {
175 syncAll();
176 closeAll();
177 }
178
179 private void closeAll() {
180 closeSilently(mIndexChannel);
181 closeSilently(mIndexFile);
182 closeSilently(mDataFile0);
183 closeSilently(mDataFile1);
184 }
185
186 // Returns true if loading index is successful. After this method is called,
187 // mIndexHeader and index header in file should be kept sync.
188 private boolean loadIndex() {
189 try {
190 mIndexFile.seek(0);
191 mDataFile0.seek(0);
192 mDataFile1.seek(0);
193
194 byte[] buf = mIndexHeader;
195 if (mIndexFile.read(buf) != INDEX_HEADER_SIZE) {
196 Log.w(TAG, "cannot read header");
197 return false;
198 }
199
200 if (readInt(buf, IH_MAGIC) != MAGIC_INDEX_FILE) {
201 Log.w(TAG, "cannot read header magic");
202 return false;
203 }
204
205 if (readInt(buf, IH_VERSION) != mVersion) {
206 Log.w(TAG, "version mismatch");
207 return false;
208 }
209
210 mMaxEntries = readInt(buf, IH_MAX_ENTRIES);
211 mMaxBytes = readInt(buf, IH_MAX_BYTES);
212 mActiveRegion = readInt(buf, IH_ACTIVE_REGION);
213 mActiveEntries = readInt(buf, IH_ACTIVE_ENTRIES);
214 mActiveBytes = readInt(buf, IH_ACTIVE_BYTES);
215
216 int sum = readInt(buf, IH_CHECKSUM);
217 if (checkSum(buf, 0, IH_CHECKSUM) != sum) {
218 Log.w(TAG, "header checksum does not match");
219 return false;
220 }
221
222 // Sanity check
223 if (mMaxEntries <= 0) {
224 Log.w(TAG, "invalid max entries");
225 return false;
226 }
227 if (mMaxBytes <= 0) {
228 Log.w(TAG, "invalid max bytes");
229 return false;
230 }
231 if (mActiveRegion != 0 && mActiveRegion != 1) {
232 Log.w(TAG, "invalid active region");
233 return false;
234 }
235 if (mActiveEntries < 0 || mActiveEntries > mMaxEntries) {
236 Log.w(TAG, "invalid active entries");
237 return false;
238 }
239 if (mActiveBytes < DATA_HEADER_SIZE || mActiveBytes > mMaxBytes) {
240 Log.w(TAG, "invalid active bytes");
241 return false;
242 }
243 if (mIndexFile.length() !=
244 INDEX_HEADER_SIZE + mMaxEntries * 12 * 2) {
245 Log.w(TAG, "invalid index file length");
246 return false;
247 }
248
249 // Make sure data file has magic
250 byte[] magic = new byte[4];
251 if (mDataFile0.read(magic) != 4) {
252 Log.w(TAG, "cannot read data file magic");
253 return false;
254 }
255 if (readInt(magic, 0) != MAGIC_DATA_FILE) {
256 Log.w(TAG, "invalid data file magic");
257 return false;
258 }
259 if (mDataFile1.read(magic) != 4) {
260 Log.w(TAG, "cannot read data file magic");
261 return false;
262 }
263 if (readInt(magic, 0) != MAGIC_DATA_FILE) {
264 Log.w(TAG, "invalid data file magic");
265 return false;
266 }
267
268 // Map index file to memory
269 mIndexChannel = mIndexFile.getChannel();
270 mIndexBuffer = mIndexChannel.map(FileChannel.MapMode.READ_WRITE,
271 0, mIndexFile.length());
272 mIndexBuffer.order(ByteOrder.LITTLE_ENDIAN);
273
274 setActiveVariables();
275 return true;
276 } catch (IOException ex) {
277 Log.e(TAG, "loadIndex failed.", ex);
278 return false;
279 }
280 }
281
282 private void setActiveVariables() throws IOException {
283 mActiveDataFile = (mActiveRegion == 0) ? mDataFile0 : mDataFile1;
284 mInactiveDataFile = (mActiveRegion == 1) ? mDataFile0 : mDataFile1;
285 mActiveDataFile.setLength(mActiveBytes);
286 mActiveDataFile.seek(mActiveBytes);
287
288 mActiveHashStart = INDEX_HEADER_SIZE;
289 mInactiveHashStart = INDEX_HEADER_SIZE;
290
291 if (mActiveRegion == 0) {
292 mInactiveHashStart += mMaxEntries * 12;
293 } else {
294 mActiveHashStart += mMaxEntries * 12;
295 }
296 }
297
298 private void resetCache(int maxEntries, int maxBytes) throws IOException {
299 mIndexFile.setLength(0); // truncate to zero the index
300 mIndexFile.setLength(INDEX_HEADER_SIZE + maxEntries * 12 * 2);
301 mIndexFile.seek(0);
302 byte[] buf = mIndexHeader;
303 writeInt(buf, IH_MAGIC, MAGIC_INDEX_FILE);
304 writeInt(buf, IH_MAX_ENTRIES, maxEntries);
305 writeInt(buf, IH_MAX_BYTES, maxBytes);
306 writeInt(buf, IH_ACTIVE_REGION, 0);
307 writeInt(buf, IH_ACTIVE_ENTRIES, 0);
308 writeInt(buf, IH_ACTIVE_BYTES, DATA_HEADER_SIZE);
309 writeInt(buf, IH_VERSION, mVersion);
310 writeInt(buf, IH_CHECKSUM, checkSum(buf, 0, IH_CHECKSUM));
311 mIndexFile.write(buf);
312 // This is only needed if setLength does not zero the extended part.
313 // writeZero(mIndexFile, maxEntries * 12 * 2);
314
315 mDataFile0.setLength(0);
316 mDataFile1.setLength(0);
317 mDataFile0.seek(0);
318 mDataFile1.seek(0);
319 writeInt(buf, 0, MAGIC_DATA_FILE);
320 mDataFile0.write(buf, 0, 4);
321 mDataFile1.write(buf, 0, 4);
322 }
323
324 // Flip the active region and the inactive region.
325 private void flipRegion() throws IOException {
326 mActiveRegion = 1 - mActiveRegion;
327 mActiveEntries = 0;
328 mActiveBytes = DATA_HEADER_SIZE;
329
330 writeInt(mIndexHeader, IH_ACTIVE_REGION, mActiveRegion);
331 writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
332 writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes);
333 updateIndexHeader();
334
335 setActiveVariables();
336 clearHash(mActiveHashStart);
337 syncIndex();
338 }
339
340 // Sync mIndexHeader to the index file.
341 private void updateIndexHeader() {
342 writeInt(mIndexHeader, IH_CHECKSUM,
343 checkSum(mIndexHeader, 0, IH_CHECKSUM));
344 mIndexBuffer.position(0);
345 mIndexBuffer.put(mIndexHeader);
346 }
347
348 // Clear the hash table starting from the specified offset.
349 private void clearHash(int hashStart) {
350 byte[] zero = new byte[1024];
351 mIndexBuffer.position(hashStart);
352 for (int count = mMaxEntries * 12; count > 0;) {
353 int todo = Math.min(count, 1024);
354 mIndexBuffer.put(zero, 0, todo);
355 count -= todo;
356 }
357 }
358
359 // Inserts a (key, data) pair into the cache.
360 public void insert(long key, byte[] data) throws IOException {
361 if (DATA_HEADER_SIZE + BLOB_HEADER_SIZE + data.length > mMaxBytes) {
362 throw new RuntimeException("blob is too large!");
363 }
364
365 if (mActiveBytes + BLOB_HEADER_SIZE + data.length > mMaxBytes
366 || mActiveEntries * 2 >= mMaxEntries) {
367 flipRegion();
368 }
369
370 if (!lookupInternal(key, mActiveHashStart)) {
371 // If we don't have an existing entry with the same key, increase
372 // the entry count.
373 mActiveEntries++;
374 writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
375 }
376
377 insertInternal(key, data, data.length);
378 updateIndexHeader();
379 }
380
381 // Appends the data to the active file. It also updates the hash entry.
382 // The proper hash entry (suitable for insertion or replacement) must be
383 // pointed by mSlotOffset.
384 private void insertInternal(long key, byte[] data, int length)
385 throws IOException {
386 byte[] header = mBlobHeader;
387 int sum = checkSum(data);
388 writeLong(header, BH_KEY, key);
389 writeInt(header, BH_CHECKSUM, sum);
390 writeInt(header, BH_OFFSET, mActiveBytes);
391 writeInt(header, BH_LENGTH, length);
392 mActiveDataFile.write(header);
393 mActiveDataFile.write(data, 0, length);
394
395 mIndexBuffer.putLong(mSlotOffset, key);
396 mIndexBuffer.putInt(mSlotOffset + 8, mActiveBytes);
397 mActiveBytes += BLOB_HEADER_SIZE + length;
398 writeInt(mIndexHeader, IH_ACTIVE_BYTES, mActiveBytes);
399 }
400
401 public static class LookupRequest {
402 public long key; // input: the key to find
403 public byte[] buffer; // input/output: the buffer to store the blob
404 public int length; // output: the length of the blob
405 }
406
407 // This method is for one-off lookup. For repeated lookup, use the version
408 // accepting LookupRequest to avoid repeated memory allocation.
409 private LookupRequest mLookupRequest = new LookupRequest();
410 public byte[] lookup(long key) throws IOException {
411 mLookupRequest.key = key;
412 mLookupRequest.buffer = null;
413 if (lookup(mLookupRequest)) {
414 return mLookupRequest.buffer;
415 } else {
416 return null;
417 }
418 }
419
420 // Returns true if the associated blob for the given key is available.
421 // The blob is stored in the buffer pointed by req.buffer, and the length
422 // is in stored in the req.length variable.
423 //
424 // The user can input a non-null value in req.buffer, and this method will
425 // try to use that buffer. If that buffer is not large enough, this method
426 // will allocate a new buffer and assign it to req.buffer.
427 //
428 // This method tries not to throw IOException even if the data file is
429 // corrupted, but it can still throw IOException if things get strange.
430 public boolean lookup(LookupRequest req) throws IOException {
431 // Look up in the active region first.
432 if (lookupInternal(req.key, mActiveHashStart)) {
433 if (getBlob(mActiveDataFile, mFileOffset, req)) {
434 return true;
435 }
436 }
437
438 // We want to copy the data from the inactive file to the active file
439 // if it's available. So we keep the offset of the hash entry so we can
440 // avoid looking it up again.
441 int insertOffset = mSlotOffset;
442
443 // Look up in the inactive region.
444 if (lookupInternal(req.key, mInactiveHashStart)) {
445 if (getBlob(mInactiveDataFile, mFileOffset, req)) {
446 // If we don't have enough space to insert this blob into
447 // the active file, just return it.
448 if (mActiveBytes + BLOB_HEADER_SIZE + req.length > mMaxBytes
449 || mActiveEntries * 2 >= mMaxEntries) {
450 return true;
451 }
452 // Otherwise copy it over.
453 mSlotOffset = insertOffset;
454 try {
455 insertInternal(req.key, req.buffer, req.length);
456 mActiveEntries++;
457 writeInt(mIndexHeader, IH_ACTIVE_ENTRIES, mActiveEntries);
458 updateIndexHeader();
459 } catch (Throwable t) {
460 Log.e(TAG, "cannot copy over");
461 }
462 return true;
463 }
464 }
465
466 return false;
467 }
468
469
470 // Copies the blob for the specified offset in the specified file to
471 // req.buffer. If req.buffer is null or too small, allocate a buffer and
472 // assign it to req.buffer.
473 // Returns false if the blob is not available (either the index file is
474 // not sync with the data file, or one of them is corrupted). The length
475 // of the blob is stored in the req.length variable.
476 private boolean getBlob(RandomAccessFile file, int offset,
477 LookupRequest req) throws IOException {
478 byte[] header = mBlobHeader;
479 long oldPosition = file.getFilePointer();
480 try {
481 file.seek(offset);
482 if (file.read(header) != BLOB_HEADER_SIZE) {
483 Log.w(TAG, "cannot read blob header");
484 return false;
485 }
486 long blobKey = readLong(header, BH_KEY);
487 if (blobKey != req.key) {
488 Log.w(TAG, "blob key does not match: " + blobKey);
489 return false;
490 }
491 int sum = readInt(header, BH_CHECKSUM);
492 int blobOffset = readInt(header, BH_OFFSET);
493 if (blobOffset != offset) {
494 Log.w(TAG, "blob offset does not match: " + blobOffset);
495 return false;
496 }
497 int length = readInt(header, BH_LENGTH);
498 if (length < 0 || length > mMaxBytes - offset - BLOB_HEADER_SIZE) {
499 Log.w(TAG, "invalid blob length: " + length);
500 return false;
501 }
502 if (req.buffer == null || req.buffer.length < length) {
503 req.buffer = new byte[length];
504 }
505
506 byte[] blob = req.buffer;
507 req.length = length;
508
509 if (file.read(blob, 0, length) != length) {
510 Log.w(TAG, "cannot read blob data");
511 return false;
512 }
513 if (checkSum(blob, 0, length) != sum) {
514 Log.w(TAG, "blob checksum does not match: " + sum);
515 return false;
516 }
517 return true;
518 } catch (Throwable t) {
519 Log.e(TAG, "getBlob failed.", t);
520 return false;
521 } finally {
522 file.seek(oldPosition);
523 }
524 }
525
526 // Tries to look up a key in the specified hash region.
527 // Returns true if the lookup is successful.
528 // The slot offset in the index file is saved in mSlotOffset. If the lookup
529 // is successful, it's the slot found. Otherwise it's the slot suitable for
530 // insertion.
531 // If the lookup is successful, the file offset is also saved in
532 // mFileOffset.
533 private int mSlotOffset;
534 private int mFileOffset;
535 private boolean lookupInternal(long key, int hashStart) {
536 int slot = (int) (key % mMaxEntries);
537 if (slot < 0) slot += mMaxEntries;
538 int slotBegin = slot;
539 while (true) {
540 int offset = hashStart + slot * 12;
541 long candidateKey = mIndexBuffer.getLong(offset);
542 int candidateOffset = mIndexBuffer.getInt(offset + 8);
543 if (candidateOffset == 0) {
544 mSlotOffset = offset;
545 return false;
546 } else if (candidateKey == key) {
547 mSlotOffset = offset;
548 mFileOffset = candidateOffset;
549 return true;
550 } else {
551 if (++slot >= mMaxEntries) {
552 slot = 0;
553 }
554 if (slot == slotBegin) {
555 Log.w(TAG, "corrupted index: clear the slot.");
556 mIndexBuffer.putInt(hashStart + slot * 12 + 8, 0);
557 }
558 }
559 }
560 }
561
562 public void syncIndex() {
563 try {
564 mIndexBuffer.force();
565 } catch (Throwable t) {
566 Log.w(TAG, "sync index failed", t);
567 }
568 }
569
570 public void syncAll() {
571 syncIndex();
572 try {
573 mDataFile0.getFD().sync();
574 } catch (Throwable t) {
575 Log.w(TAG, "sync data file 0 failed", t);
576 }
577 try {
578 mDataFile1.getFD().sync();
579 } catch (Throwable t) {
580 Log.w(TAG, "sync data file 1 failed", t);
581 }
582 }
583
584 // This is for testing only.
585 //
586 // Returns the active count (mActiveEntries). This also verifies that
587 // the active count matches matches what's inside the hash region.
588 int getActiveCount() {
589 int count = 0;
590 for (int i = 0; i < mMaxEntries; i++) {
591 int offset = mActiveHashStart + i * 12;
592 long candidateKey = mIndexBuffer.getLong(offset);
593 int candidateOffset = mIndexBuffer.getInt(offset + 8);
594 if (candidateOffset != 0) ++count;
595 }
596 if (count == mActiveEntries) {
597 return count;
598 } else {
599 Log.e(TAG, "wrong active count: " + mActiveEntries + " vs " + count);
600 return -1; // signal failure.
601 }
602 }
603
604 int checkSum(byte[] data) {
605 mAdler32.reset();
606 mAdler32.update(data);
607 return (int) mAdler32.getValue();
608 }
609
610 int checkSum(byte[] data, int offset, int nbytes) {
611 mAdler32.reset();
612 mAdler32.update(data, offset, nbytes);
613 return (int) mAdler32.getValue();
614 }
615
616 static void closeSilently(Closeable c) {
617 if (c == null) return;
618 try {
619 c.close();
620 } catch (Throwable t) {
621 // do nothing
622 }
623 }
624
625 static int readInt(byte[] buf, int offset) {
626 return (buf[offset] & 0xff)
627 | ((buf[offset + 1] & 0xff) << 8)
628 | ((buf[offset + 2] & 0xff) << 16)
629 | ((buf[offset + 3] & 0xff) << 24);
630 }
631
632 static long readLong(byte[] buf, int offset) {
633 long result = buf[offset + 7] & 0xff;
634 for (int i = 6; i >= 0; i--) {
635 result = (result << 8) | (buf[offset + i] & 0xff);
636 }
637 return result;
638 }
639
640 static void writeInt(byte[] buf, int offset, int value) {
641 for (int i = 0; i < 4; i++) {
642 buf[offset + i] = (byte) (value & 0xff);
643 value >>= 8;
644 }
645 }
646
647 static void writeLong(byte[] buf, int offset, long value) {
648 for (int i = 0; i < 8; i++) {
649 buf[offset + i] = (byte) (value & 0xff);
650 value >>= 8;
651 }
652 }
653}