Bug 774146 - Get more detail on reason for DISK_CACHE_CORRUPT failures. r=michal
authorBrian R. Bondy <netzen@gmail.com>
Fri, 20 Jul 2012 19:47:52 -0400
changeset 100029 4c632ce7134408a1c0ab644184b10a0f8210af7b
parent 100028 c6e2e6537d701f4ffe6b1d56099c89160ed016f3
child 100030 5bd9db1381a62e26aec0b1f9c294d3b34f5e9c0a
push id12305
push userbbondy@mozilla.com
push dateSat, 21 Jul 2012 17:43:59 +0000
treeherdermozilla-inbound@4c632ce71344 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmichal
bugs774146
milestone17.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 774146 - Get more detail on reason for DISK_CACHE_CORRUPT failures. r=michal
netwerk/cache/nsDiskCache.h
netwerk/cache/nsDiskCacheBlockFile.cpp
netwerk/cache/nsDiskCacheBlockFile.h
netwerk/cache/nsDiskCacheDevice.cpp
netwerk/cache/nsDiskCacheMap.cpp
netwerk/cache/nsDiskCacheMap.h
toolkit/components/telemetry/TelemetryHistograms.h
--- a/netwerk/cache/nsDiskCache.h
+++ b/netwerk/cache/nsDiskCache.h
@@ -18,16 +18,45 @@
 class nsDiskCache {
 public:
     enum {
             kCurrentVersion = 0x00010013      // format = 16 bits major version/16 bits minor version
     };
 
     enum { kData, kMetaData };
 
+    // Stores the reason why the cache is corrupt.
+    // Note: I'm only listing the enum values explicitly for easy mapping when
+    // looking at telemetry data.
+    enum CorruptCacheInfo {
+      kNotCorrupt = 0,
+      kInvalidArgPointer = 1,
+      kUnexpectedError = 2,
+      kOpenCacheMapError = 3,
+      kBlockFilesShouldNotExist = 4,
+      kOutOfMemory = 5,
+      kCreateCacheSubdirectories = 6,
+      kBlockFilesShouldExist = 7,
+      kHeaderSizeNotRead = 8,
+      kHeaderIsDirty = 9,
+      kVersionMismatch = 10,
+      kRecordsIncomplete = 11,
+      kHeaderIncomplete = 12,
+      kNotEnoughToRead = 13,
+      kEntryCountIncorrect = 14,
+      kCouldNotGetBlockFileForIndex = 15,
+      kCouldNotCreateBlockFile = 16,
+      kBlockFileSizeError = 17,
+      kBlockFileBitMapWriteError = 18,
+      kBlockFileSizeLessThanBitMap = 19,
+      kBlockFileBitMapReadError = 20,
+      kBlockFileEstimatedSizeError = 21,
+      kFlushHeaderError = 22
+    };
+
     // Parameter initval initializes internal state of hash function. Hash values are different
     // for the same text when different initval is used. It can be any random number.
     // 
     // It can be used for generating 64-bit hash value:
     //   (PRUint64(Hash(key, initval1)) << 32) | Hash(key, initval2)
     //
     // It can be also used to hash multiple strings:
     //   h = Hash(string1, 0);
--- a/netwerk/cache/nsDiskCacheBlockFile.cpp
+++ b/netwerk/cache/nsDiskCacheBlockFile.cpp
@@ -16,72 +16,85 @@ using namespace mozilla;
  *****************************************************************************/
 
 /******************************************************************************
  *  Open
  *****************************************************************************/
 nsresult
 nsDiskCacheBlockFile::Open(nsIFile * blockFile,
                            PRUint32  blockSize,
-                           PRUint32  bitMapSize)
+                           PRUint32  bitMapSize,
+                           nsDiskCache::CorruptCacheInfo *  corruptInfo)
 {
-    if (bitMapSize % 32)
+    NS_ENSURE_ARG_POINTER(corruptInfo);
+    *corruptInfo = nsDiskCache::kUnexpectedError;
+
+    if (bitMapSize % 32) {
+        *corruptInfo = nsDiskCache::kInvalidArgPointer;
         return NS_ERROR_INVALID_ARG;
+    }
 
     mBlockSize = blockSize;
     mBitMapWords = bitMapSize / 32;
     PRUint32 bitMapBytes = mBitMapWords * 4;
     
     // open the file - restricted to user, the data could be confidential
     nsresult rv = blockFile->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 00600, &mFD);
     if (NS_FAILED(rv)) {
+        *corruptInfo = nsDiskCache::kCouldNotCreateBlockFile;
         CACHE_LOG_DEBUG(("CACHE: nsDiskCacheBlockFile::Open "
                          "[this=%p] unable to open or create file: %d",
                          this, rv));
         return rv;  // unable to open or create file
     }
     
     // allocate bit map buffer
     mBitMap = new PRUint32[mBitMapWords];
     
     // check if we just creating the file
     mFileSize = PR_Available(mFD);
     if (mFileSize < 0) {
         // XXX an error occurred. We could call PR_GetError(), but how would that help?
+        *corruptInfo = nsDiskCache::kBlockFileSizeError;
         rv = NS_ERROR_UNEXPECTED;
         goto error_exit;
     }
     if (mFileSize == 0) {
         // initialize bit map and write it
         memset(mBitMap, 0, bitMapBytes);
-        if (!Write(0, mBitMap, bitMapBytes))
+        if (!Write(0, mBitMap, bitMapBytes)) {
+            *corruptInfo = nsDiskCache::kBlockFileBitMapWriteError;
             goto error_exit;
+        }
         
     } else if ((PRUint32)mFileSize < bitMapBytes) {
+        *corruptInfo = nsDiskCache::kBlockFileSizeLessThanBitMap;
         rv = NS_ERROR_UNEXPECTED;  // XXX NS_ERROR_CACHE_INVALID;
         goto error_exit;
         
     } else {
         // read the bit map
         const PRInt32 bytesRead = PR_Read(mFD, mBitMap, bitMapBytes);
         if ((bytesRead < 0) || ((PRUint32)bytesRead < bitMapBytes)) {
+            *corruptInfo = nsDiskCache::kBlockFileBitMapReadError;
             rv = NS_ERROR_UNEXPECTED;
             goto error_exit;
         }
 #if defined(IS_LITTLE_ENDIAN)
         // Swap from network format
         for (unsigned int i = 0; i < mBitMapWords; ++i)
             mBitMap[i] = ntohl(mBitMap[i]);
 #endif
         // validate block file size
         // Because not whole blocks are written, the size may be a 
         // little bit smaller than used blocks times blocksize,
         // because the last block will generally not be 'whole'.
         const PRUint32  estimatedSize = CalcBlockFileSize();
         if ((PRUint32)mFileSize + blockSize < estimatedSize) {
+            *corruptInfo = nsDiskCache::kBlockFileEstimatedSizeError;
             rv = NS_ERROR_UNEXPECTED;
             goto error_exit;
         }
     }
     CACHE_LOG_DEBUG(("CACHE: nsDiskCacheBlockFile::Open [this=%p] succeeded",
                       this));
     return NS_OK;
 
--- a/netwerk/cache/nsDiskCacheBlockFile.h
+++ b/netwerk/cache/nsDiskCacheBlockFile.h
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef _nsDiskCacheBlockFile_h_
 #define _nsDiskCacheBlockFile_h_
 
 #include "nsIFile.h"
+#include "nsDiskCache.h"
 
 /******************************************************************************
  *  nsDiskCacheBlockFile
  *
  *  The structure of a cache block file is a 4096 bytes bit map, followed by
  *  some number of blocks of mBlockSize.  The creator of a
  *  nsDiskCacheBlockFile object must provide the block size for a given file.
  *
@@ -25,19 +26,19 @@ public:
            , mBlockSize(0)
            , mBitMapWords(0)
            , mFileSize(0)
            , mBitMapDirty(false)
             {}
     ~nsDiskCacheBlockFile() { (void) Close(true); }
     
     nsresult  Open( nsIFile *  blockFile, PRUint32  blockSize,
-                    PRUint32  bitMapSize);
+                    PRUint32  bitMapSize, nsDiskCache::CorruptCacheInfo *  corruptInfo);
     nsresult  Close(bool flush);
-    
+
     /*
      * Trim
      * Truncates the block file to the end of the last allocated block.
      */
     nsresult  Trim() { return nsDiskCache::Truncate(mFD, CalcBlockFileSize()); }
     nsresult  DeallocateBlocks( PRInt32  startBlock, PRInt32  numBlocks);
     nsresult  WriteBlocks( void * buffer, PRUint32 size, PRInt32  numBlocks, 
                            PRInt32 * startBlock);
--- a/netwerk/cache/nsDiskCacheDevice.cpp
+++ b/netwerk/cache/nsDiskCacheDevice.cpp
@@ -981,22 +981,26 @@ nsDiskCacheDevice::OpenDiskCache()
     // if we don't have a cache directory, create one and open it
     bool exists;
     nsresult rv = mCacheDirectory->Exists(&exists);
     if (NS_FAILED(rv))
         return rv;
 
     if (exists) {
         // Try opening cache map file.
-        rv = mCacheMap.Open(mCacheDirectory);        
+        nsDiskCache::CorruptCacheInfo corruptInfo;
+        rv = mCacheMap.Open(mCacheDirectory, &corruptInfo);
+
         // move "corrupt" caches to trash
         if (NS_SUCCEEDED(rv)) {
-            Telemetry::Accumulate(Telemetry::DISK_CACHE_CORRUPT, 0);
+            Telemetry::Accumulate(Telemetry::DISK_CACHE_CORRUPT_DETAILS,
+                                  corruptInfo);
         } else if (rv == NS_ERROR_FILE_CORRUPTED) {
-            Telemetry::Accumulate(Telemetry::DISK_CACHE_CORRUPT, 1);
+            Telemetry::Accumulate(Telemetry::DISK_CACHE_CORRUPT_DETAILS,
+                                  corruptInfo);
             // delay delete by 1 minute to avoid IO thrash at startup
             rv = nsDeleteDir::DeleteDir(mCacheDirectory, true, 60000);
             if (NS_FAILED(rv))
                 return rv;
             exists = false;
         } else {
             // don't gather telemetry for "corrupt cache" for new profile
             // where cache doesn't exist (most likely case if we're here).
@@ -1008,17 +1012,18 @@ nsDiskCacheDevice::OpenDiskCache()
     if (!exists) {
         rv = mCacheDirectory->Create(nsIFile::DIRECTORY_TYPE, 0777);
         CACHE_LOG_PATH(PR_LOG_ALWAYS, "\ncreate cache directory: %s\n", mCacheDirectory);
         CACHE_LOG_ALWAYS(("mCacheDirectory->Create() = %x\n", rv));
         if (NS_FAILED(rv))
             return rv;
     
         // reopen the cache map     
-        rv = mCacheMap.Open(mCacheDirectory);
+        nsDiskCache::CorruptCacheInfo corruptInfo;
+        rv = mCacheMap.Open(mCacheDirectory, &corruptInfo);
         if (NS_FAILED(rv))
             return rv;
     }
 
     return NS_OK;
 }
 
 
--- a/netwerk/cache/nsDiskCacheMap.cpp
+++ b/netwerk/cache/nsDiskCacheMap.cpp
@@ -22,126 +22,167 @@
  *  nsDiskCacheMap
  *****************************************************************************/
 
 /**
  *  File operations
  */
 
 nsresult
-nsDiskCacheMap::Open(nsIFile *  cacheDirectory)
+nsDiskCacheMap::Open(nsIFile *  cacheDirectory,
+                     nsDiskCache::CorruptCacheInfo *  corruptInfo)
 {
+    NS_ENSURE_ARG_POINTER(corruptInfo);
+
+    // Assume we have an unexpected error until we find otherwise.
+    *corruptInfo = nsDiskCache::kUnexpectedError;
     NS_ENSURE_ARG_POINTER(cacheDirectory);
     if (mMapFD)  return NS_ERROR_ALREADY_INITIALIZED;
 
     mCacheDirectory = cacheDirectory;   // save a reference for ourselves
     
     // create nsIFile for _CACHE_MAP_
     nsresult rv;
     nsCOMPtr<nsIFile> file;
     rv = cacheDirectory->Clone(getter_AddRefs(file));
     rv = file->AppendNative(NS_LITERAL_CSTRING("_CACHE_MAP_"));
     NS_ENSURE_SUCCESS(rv, rv);
 
     // open the file - restricted to user, the data could be confidential
     rv = file->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 00600, &mMapFD);
-    NS_ENSURE_SUCCESS(rv, NS_ERROR_FILE_CORRUPTED);
+    if (NS_FAILED(rv)) {
+        *corruptInfo = nsDiskCache::kOpenCacheMapError;
+        NS_WARNING("Could not open cache map file");
+        return NS_ERROR_FILE_CORRUPTED;
+    }
 
     bool cacheFilesExist = CacheFilesExist();
     rv = NS_ERROR_FILE_CORRUPTED;  // presume the worst
 
     // check size of map file
     PRUint32 mapSize = PR_Available(mMapFD);    
     if (mapSize == 0) {  // creating a new _CACHE_MAP_
 
         // block files shouldn't exist if we're creating the _CACHE_MAP_
-        if (cacheFilesExist)
-            goto error_exit;
+        if (cacheFilesExist) {
+            *corruptInfo = nsDiskCache::kBlockFilesShouldNotExist;
+            goto error_exit; 
+        }
 
-        if (NS_FAILED(CreateCacheSubDirectories()))
+        if (NS_FAILED(CreateCacheSubDirectories())) {
+            *corruptInfo = nsDiskCache::kCreateCacheSubdirectories;
             goto error_exit;
+        }
 
         // create the file - initialize in memory
         memset(&mHeader, 0, sizeof(nsDiskCacheHeader));
         mHeader.mVersion = nsDiskCache::kCurrentVersion;
         mHeader.mRecordCount = kMinRecordCount;
         mRecordArray = (nsDiskCacheRecord *)
             PR_CALLOC(mHeader.mRecordCount * sizeof(nsDiskCacheRecord));
         if (!mRecordArray) {
+            *corruptInfo = nsDiskCache::kOutOfMemory;
             rv = NS_ERROR_OUT_OF_MEMORY;
             goto error_exit;
         }
     } else if (mapSize >= sizeof(nsDiskCacheHeader)) {  // read existing _CACHE_MAP_
         
         // if _CACHE_MAP_ exists, so should the block files
-        if (!cacheFilesExist)
+        if (!cacheFilesExist) {
+            *corruptInfo = nsDiskCache::kBlockFilesShouldExist;
             goto error_exit;
+        }
 
         CACHE_LOG_DEBUG(("CACHE: nsDiskCacheMap::Open [this=%p] reading map", this));
 
         // read the header
         PRUint32 bytesRead = PR_Read(mMapFD, &mHeader, sizeof(nsDiskCacheHeader));
-        if (sizeof(nsDiskCacheHeader) != bytesRead)  goto error_exit;
+        if (sizeof(nsDiskCacheHeader) != bytesRead) {
+            *corruptInfo = nsDiskCache::kHeaderSizeNotRead;
+            goto error_exit;
+        }
         mHeader.Unswap();
 
-        if (mHeader.mIsDirty || (mHeader.mVersion != nsDiskCache::kCurrentVersion))
+        if (mHeader.mIsDirty) {
+            *corruptInfo = nsDiskCache::kHeaderIsDirty;
             goto error_exit;
+        }
+        
+        if (mHeader.mVersion != nsDiskCache::kCurrentVersion) {
+            *corruptInfo = nsDiskCache::kVersionMismatch;
+            goto error_exit;
+        }
 
         PRUint32 recordArraySize =
                 mHeader.mRecordCount * sizeof(nsDiskCacheRecord);
-        if (mapSize < recordArraySize + sizeof(nsDiskCacheHeader))
+        if (mapSize < recordArraySize + sizeof(nsDiskCacheHeader)) {
+            *corruptInfo = nsDiskCache::kRecordsIncomplete;
             goto error_exit;
+        }
 
         // Get the space for the records
         mRecordArray = (nsDiskCacheRecord *) PR_MALLOC(recordArraySize);
         if (!mRecordArray) {
+            *corruptInfo = nsDiskCache::kOutOfMemory;
             rv = NS_ERROR_OUT_OF_MEMORY;
             goto error_exit;
         }
 
         // Read the records
         bytesRead = PR_Read(mMapFD, mRecordArray, recordArraySize);
-        if (bytesRead < recordArraySize)
+        if (bytesRead < recordArraySize) {
+            *corruptInfo = nsDiskCache::kNotEnoughToRead;
             goto error_exit;
+        }
 
         // Unswap each record
         PRInt32 total = 0;
         for (PRInt32 i = 0; i < mHeader.mRecordCount; ++i) {
             if (mRecordArray[i].HashNumber()) {
 #if defined(IS_LITTLE_ENDIAN)
                 mRecordArray[i].Unswap();
 #endif
                 total ++;
             }
         }
         
         // verify entry count
-        if (total != mHeader.mEntryCount)
+        if (total != mHeader.mEntryCount) {
+            *corruptInfo = nsDiskCache::kEntryCountIncorrect;
             goto error_exit;
+        }
 
     } else {
+        *corruptInfo = nsDiskCache::kHeaderIncomplete;
         goto error_exit;
     }
 
-    rv = OpenBlockFiles();
-    if (NS_FAILED(rv))  goto error_exit;
+    rv = OpenBlockFiles(corruptInfo);
+    if (NS_FAILED(rv)) {
+        // corruptInfo is set in the call to OpenBlockFiles
+        goto error_exit;
+    }
 
     // set dirty bit and flush header
     mHeader.mIsDirty    = true;
     rv = FlushHeader();
-    if (NS_FAILED(rv))  goto error_exit;
+    if (NS_FAILED(rv)) {
+        *corruptInfo = nsDiskCache::kFlushHeaderError;
+        goto error_exit;
+    }
     
     {
         // extra scope so the compiler doesn't barf on the above gotos jumping
         // past this declaration down here
         PRUint32 overhead = moz_malloc_size_of(mRecordArray);
         mozilla::Telemetry::Accumulate(mozilla::Telemetry::HTTP_DISK_CACHE_OVERHEAD,
                 overhead);
     }
 
+    *corruptInfo = nsDiskCache::kNotCorrupt;
     return NS_OK;
     
 error_exit:
     (void) Close(false);
        
     return rv;
 }
 
@@ -580,33 +621,42 @@ nsDiskCacheMap::EvictRecords( nsDiskCach
         tempRank[bucketIndex] = GetBucketRank(bucketIndex, rank);
     }
     return NS_OK;
 }
 
 
 
 nsresult
-nsDiskCacheMap::OpenBlockFiles()
+nsDiskCacheMap::OpenBlockFiles(nsDiskCache::CorruptCacheInfo *  corruptInfo)
 {
+    NS_ENSURE_ARG_POINTER(corruptInfo);
+
     // create nsIFile for block file
     nsCOMPtr<nsIFile> blockFile;
     nsresult rv = NS_OK;
+    *corruptInfo = nsDiskCache::kUnexpectedError;
     
     for (int i = 0; i < kNumBlockFiles; ++i) {
         rv = GetBlockFileForIndex(i, getter_AddRefs(blockFile));
-        if (NS_FAILED(rv)) break;
+        if (NS_FAILED(rv)) {
+            *corruptInfo = nsDiskCache::kCouldNotGetBlockFileForIndex;
+            break;
+        }
     
         PRUint32 blockSize = GetBlockSizeForIndex(i+1); // +1 to match file selectors 1,2,3
         PRUint32 bitMapSize = GetBitMapSizeForIndex(i+1);
-        rv = mBlockFile[i].Open(blockFile, blockSize, bitMapSize);
-        if (NS_FAILED(rv)) break;
+        rv = mBlockFile[i].Open(blockFile, blockSize, bitMapSize, corruptInfo);
+        if (NS_FAILED(rv)) {
+            // corruptInfo was set inside the call to mBlockFile[i].Open
+            break;
+        }
     }
     // close all files in case of any error
-    if (NS_FAILED(rv)) 
+    if (NS_FAILED(rv))
         (void)CloseBlockFiles(false); // we already have an error to report
 
     return rv;
 }
 
 
 nsresult
 nsDiskCacheMap::CloseBlockFiles(bool flush)
--- a/netwerk/cache/nsDiskCacheMap.h
+++ b/netwerk/cache/nsDiskCacheMap.h
@@ -391,17 +391,18 @@ public:
 /**
  *  File Operations
  *
  *  Open
  *
  *  Creates a new cache map file if one doesn't exist.
  *  Returns error if it detects change in format or cache wasn't closed.
  */
-    nsresult  Open( nsIFile *  cacheDirectory);
+    nsresult  Open( nsIFile *  cacheDirectory,
+                    nsDiskCache::CorruptCacheInfo *  corruptInfo);
     nsresult  Close(bool flush);
     nsresult  Trim();
 
     nsresult  FlushHeader();
     nsresult  FlushRecords( bool unswap);
 
     void      NotifyCapacityChange(PRUint32 capacity);
 
@@ -473,17 +474,17 @@ public:
     PRInt32  EntryCount()  { return mHeader.mEntryCount; }
 
 
 private:
 
     /**
      *  Private methods
      */
-    nsresult    OpenBlockFiles();
+    nsresult    OpenBlockFiles(nsDiskCache::CorruptCacheInfo *  corruptInfo);
     nsresult    CloseBlockFiles(bool flush);
     bool        CacheFilesExist();
 
     nsresult    CreateCacheSubDirectories();
 
     PRUint32    CalculateFileIndex(PRUint32 size);
 
     nsresult    GetBlockFileForIndex( PRUint32 index, nsIFile ** result);
--- a/toolkit/components/telemetry/TelemetryHistograms.h
+++ b/toolkit/components/telemetry/TelemetryHistograms.h
@@ -190,17 +190,17 @@ HISTOGRAM(SPDY_SETTINGS_RTT, 1, 1000, 10
 HISTOGRAM(SPDY_SETTINGS_MAX_STREAMS, 1, 5000, 100, EXPONENTIAL,  "SPDY: Settings Max Streams parameter")
 HISTOGRAM(SPDY_SETTINGS_CWND, 1, 500, 50, EXPONENTIAL,  "SPDY: Settings CWND (packets)")
 HISTOGRAM(SPDY_SETTINGS_RETRANS, 1, 100, 50, EXPONENTIAL,  "SPDY: Retransmission Rate")
 HISTOGRAM(SPDY_SETTINGS_IW, 1, 1000, 50, EXPONENTIAL,  "SPDY: Settings IW (rounded to KB)")
 
 #undef _HTTP_HIST
 #undef HTTP_HISTOGRAMS
 
-HISTOGRAM(DISK_CACHE_CORRUPT, 0, 1, 2, BOOLEAN, "Was the HTTP disk cache corrupt at startup?")
+HISTOGRAM(DISK_CACHE_CORRUPT_DETAILS, 1, 50, 51, LINEAR, "Why the HTTP disk cache was corrupted at startup")
 HISTOGRAM_ENUMERATED_VALUES(HTTP_CACHE_DISPOSITION_2,         5, "HTTP Cache Hit, Reval, Failed-Reval, Miss")
 HISTOGRAM_ENUMERATED_VALUES(HTTP_DISK_CACHE_DISPOSITION_2,    5, "HTTP Disk Cache Hit, Reval, Failed-Reval, Miss")
 HISTOGRAM_ENUMERATED_VALUES(HTTP_MEMORY_CACHE_DISPOSITION_2,  5, "HTTP Memory Cache Hit, Reval, Failed-Reval, Miss")
 HISTOGRAM_ENUMERATED_VALUES(HTTP_OFFLINE_CACHE_DISPOSITION_2, 5, "HTTP Offline Cache Hit, Reval, Failed-Reval, Miss")
 HISTOGRAM(CACHE_DEVICE_SEARCH, 1, 100, 100, LINEAR, "Time to search cache (ms)")
 HISTOGRAM(CACHE_MEMORY_SEARCH, 1, 100, 100, LINEAR, "Time to search memory cache (ms)")
 HISTOGRAM(CACHE_DISK_SEARCH, 1, 100, 100, LINEAR, "Time to search disk cache (ms)")
 HISTOGRAM(CACHE_OFFLINE_SEARCH, 1, 100, 100, LINEAR, "Time to search offline cache (ms)")