Bug 1020416 - Add forceValidFor() and isForcedValid attribute to nsICacheEntry. r=honzab, r=michal
☠☠ backed out by 0dc711216018 ☠ ☠
authorJeremy Poulin <jpoulin@cs.uml.edu>
Fri, 18 Jul 2014 18:11:34 -0700
changeset 216003 a232fd36c82701cee208ce2a869d87e1400cb1ea
parent 216002 917fbaf92fa280cbb1011d924175d23e4dff772a
child 216004 c809edd9b1c37352484596c86aa950b60ce1fce1
push id3857
push userraliiev@mozilla.com
push dateTue, 02 Sep 2014 16:39:23 +0000
treeherdermozilla-beta@5638b907b505 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewershonzab, michal
bugs1020416
milestone33.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 1020416 - Add forceValidFor() and isForcedValid attribute to nsICacheEntry. r=honzab, r=michal
netwerk/cache2/CacheEntry.cpp
netwerk/cache2/CacheIndex.cpp
netwerk/cache2/CacheIndex.h
netwerk/cache2/CacheStorageService.cpp
netwerk/cache2/CacheStorageService.h
netwerk/cache2/OldWrappers.cpp
netwerk/cache2/OldWrappers.h
netwerk/cache2/nsICacheEntry.idl
netwerk/test/unit/test_cache2-27-force-valid-for.js
netwerk/test/unit/xpcshell.ini
--- a/netwerk/cache2/CacheEntry.cpp
+++ b/netwerk/cache2/CacheEntry.cpp
@@ -955,16 +955,48 @@ NS_IMETHODIMP CacheEntry::GetLastModifie
 
 NS_IMETHODIMP CacheEntry::GetExpirationTime(uint32_t *aExpirationTime)
 {
   NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE);
 
   return mFile->GetExpirationTime(aExpirationTime);
 }
 
+NS_IMETHODIMP CacheEntry::GetIsForcedValid(bool *aIsForcedValid)
+{
+  NS_ENSURE_ARG(aIsForcedValid);
+
+  nsAutoCString key;
+
+  nsresult rv = HashingKeyWithStorage(key);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  *aIsForcedValid = CacheStorageService::Self()->IsForcedValidEntry(key);
+  LOG(("CacheEntry::GetIsForcedValid [this=%p, IsForcedValid=%d]", this, *aIsForcedValid));
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP CacheEntry::ForceValidFor(uint32_t aSecondsToTheFuture)
+{
+  LOG(("CacheEntry::ForceValidFor [this=%p, aSecondsToTheFuture=%d]", this, aSecondsToTheFuture));
+
+  nsAutoCString key;
+  nsresult rv = HashingKeyWithStorage(key);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  CacheStorageService::Self()->ForceEntryValidFor(key, aSecondsToTheFuture);
+
+  return NS_OK;
+}
+
 NS_IMETHODIMP CacheEntry::SetExpirationTime(uint32_t aExpirationTime)
 {
   NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE);
 
   nsresult rv = mFile->SetExpirationTime(aExpirationTime);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Aligned assignment, thus atomic.
--- a/netwerk/cache2/CacheIndex.cpp
+++ b/netwerk/cache2/CacheIndex.cpp
@@ -1143,51 +1143,107 @@ CacheIndex::GetEntryForEviction(SHA1Sum:
 {
   LOG(("CacheIndex::GetEntryForEviction()"));
 
   nsRefPtr<CacheIndex> index = gInstance;
 
   if (!index)
     return NS_ERROR_NOT_INITIALIZED;
 
+  MOZ_ASSERT(CacheFileIOManager::IsOnIOThread());
+
   CacheIndexAutoLock lock(index);
 
   if (!index->IsIndexUsable()) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   MOZ_ASSERT(index->mFrecencyArray.Length() ==
              index->mExpirationArray.Length());
 
   if (index->mExpirationArray.Length() == 0)
     return NS_ERROR_NOT_AVAILABLE;
 
+  SHA1Sum::Hash hash;
+  bool foundEntry = false;
+  uint32_t i = 0, j = 0;
   uint32_t now = PR_Now() / PR_USEC_PER_SEC;
-  if (index->mExpirationArray[0]->mExpirationTime < now) {
-    memcpy(aHash, &index->mExpirationArray[0]->mHash, sizeof(SHA1Sum::Hash));
-    *aCnt = index->mExpirationArray.Length();
+
+  // find the first expired, non-forced valid entry
+  for (i = 0; i < index->mExpirationArray.Length(); i++) {
+    if (index->mExpirationArray[i]->mExpirationTime < now) {
+      memcpy(&hash, &index->mExpirationArray[i]->mHash, sizeof(SHA1Sum::Hash));
+
+      if (!IsForcedValidEntry(&hash)) {
+        foundEntry = true;
+        break;
+      }
+
+    } else {
+      // all further entries have not expired yet
+      break;
+    }
+  }
+
+  if (foundEntry) {
+    *aCnt = index->mExpirationArray.Length() - i;
+
     LOG(("CacheIndex::GetEntryForEviction() - returning entry from expiration "
          "array [hash=%08x%08x%08x%08x%08x, cnt=%u, expTime=%u, now=%u, "
-         "frecency=%u]", LOGSHA1(aHash), *aCnt,
-         index->mExpirationArray[0]->mExpirationTime, now,
-         index->mExpirationArray[0]->mFrecency));
+         "frecency=%u]", LOGSHA1(&hash), *aCnt,
+         index->mExpirationArray[i]->mExpirationTime, now,
+         index->mExpirationArray[i]->mFrecency));
   }
   else {
-    memcpy(aHash, &index->mFrecencyArray[0]->mHash, sizeof(SHA1Sum::Hash));
-    *aCnt = index->mFrecencyArray.Length();
+    // check if we've already tried all the entries
+    if (i == index->mExpirationArray.Length())
+      return NS_ERROR_NOT_AVAILABLE;
+
+    // find first non-forced valid entry with the lowest frecency
+    for (j = 0; j < index->mFrecencyArray.Length(); j++) {
+      memcpy(&hash, &index->mFrecencyArray[j]->mHash, sizeof(SHA1Sum::Hash));
+
+      if (!IsForcedValidEntry(&hash)) {
+        foundEntry = true;
+        break;
+      }
+    }
+
+    if (!foundEntry)
+      return NS_ERROR_NOT_AVAILABLE;
+
+    // forced valid entries skipped in both arrays could overlap, just use max
+    *aCnt = index->mFrecencyArray.Length() - std::max(i, j);
+
     LOG(("CacheIndex::GetEntryForEviction() - returning entry from frecency "
          "array [hash=%08x%08x%08x%08x%08x, cnt=%u, expTime=%u, now=%u, "
-         "frecency=%u]", LOGSHA1(aHash), *aCnt,
-         index->mExpirationArray[0]->mExpirationTime, now,
-         index->mExpirationArray[0]->mFrecency));
+         "frecency=%u]", LOGSHA1(&hash), *aCnt,
+         index->mFrecencyArray[j]->mExpirationTime, now,
+         index->mFrecencyArray[j]->mFrecency));
   }
 
+  memcpy(aHash, &hash, sizeof(SHA1Sum::Hash));
+
   return NS_OK;
 }
 
+
+// static
+bool CacheIndex::IsForcedValidEntry(const SHA1Sum::Hash *aHash)
+{
+  nsRefPtr<CacheFileHandle> handle;
+
+  CacheFileIOManager::gInstance->mHandles.GetHandle(
+    aHash, false, getter_AddRefs(handle));
+
+  nsCString hashKey = handle->Key();
+  return CacheStorageService::Self()->IsForcedValidEntry(hashKey);
+}
+
+
 // static
 nsresult
 CacheIndex::GetCacheSize(uint32_t *_retval)
 {
   LOG(("CacheIndex::GetCacheSize()"));
 
   nsRefPtr<CacheIndex> index = gInstance;
 
--- a/netwerk/cache2/CacheIndex.h
+++ b/netwerk/cache2/CacheIndex.h
@@ -556,19 +556,25 @@ public:
   };
 
   // Returns status of the entry in index for the given key. It can be called
   // on any thread.
   static nsresult HasEntry(const nsACString &aKey, EntryStatus *_retval);
 
   // Returns a hash of the least important entry that should be evicted if the
   // cache size is over limit and also returns a total number of all entries in
-  // the index.
+  // the index minus the number of forced valid entries that we encounter
+  // when searching (see below)
   static nsresult GetEntryForEviction(SHA1Sum::Hash *aHash, uint32_t *aCnt);
 
+  // Checks if a cache entry is currently forced valid. Used to prevent an entry
+  // (that has been forced valid) from being evicted when the cache size reaches
+  // its limit.
+  static bool IsForcedValidEntry(const SHA1Sum::Hash *aHash);
+
   // Returns cache size in kB.
   static nsresult GetCacheSize(uint32_t *_retval);
 
   // Synchronously returns the disk occupation and number of entries per-context.
   // Callable on any thread.
   static nsresult GetCacheStats(nsILoadContextInfo *aInfo, uint32_t *aSize, uint32_t *aCount);
 
   // Asynchronously gets the disk cache size, used for display in the UI.
--- a/netwerk/cache2/CacheStorageService.cpp
+++ b/netwerk/cache2/CacheStorageService.cpp
@@ -952,19 +952,26 @@ CacheStorageService::RemoveEntry(CacheEn
 
   mozilla::MutexAutoLock lock(mLock);
 
   if (mShutdown) {
     LOG(("  after shutdown"));
     return false;
   }
 
-  if (aOnlyUnreferenced && aEntry->IsReferenced()) {
-    LOG(("  still referenced, not removing"));
-    return false;
+  if (aOnlyUnreferenced) {
+    if (aEntry->IsReferenced()) {
+      LOG(("  still referenced, not removing"));
+      return false;
+    }
+
+    if (!aEntry->IsUsingDisk() && IsForcedValidEntry(entryKey)) {
+      LOG(("  forced valid, not removing"));
+      return false;
+    }
   }
 
   CacheEntryTable* entries;
   if (sGlobalEntryTables->Get(aEntry->GetStorageID(), &entries))
     RemoveExactEntry(entries, entryKey, aEntry, false /* don't overwrite */);
 
   nsAutoCString memoryStorageID(aEntry->GetStorageID());
   AppendMemoryStorageID(memoryStorageID);
@@ -1022,16 +1029,56 @@ CacheStorageService::RecordMemoryOnlyEnt
   if (aOnlyInMemory) {
     AddExactEntry(entries, entryKey, aEntry, aOverwrite);
   }
   else {
     RemoveExactEntry(entries, entryKey, aEntry, aOverwrite);
   }
 }
 
+// Checks if a cache entry is forced valid (will be loaded directly from cache
+// without further validation) - see nsICacheEntry.idl for further details
+bool CacheStorageService::IsForcedValidEntry(nsACString &aCacheEntryKey)
+{
+  TimeStamp validUntil;
+
+  mozilla::MutexAutoLock lock(mLock);
+
+  if (!mForcedValidEntries.Get(aCacheEntryKey, &validUntil)) {
+    return false;
+  }
+
+  if (validUntil.IsNull()) {
+    return false;
+  }
+
+  // Entry timeout not reached yet
+  if (TimeStamp::NowLoRes() <= validUntil) {
+    return true;
+  }
+
+  // Entry timeout has been reached
+  mForcedValidEntries.Remove(aCacheEntryKey);
+  return false;
+}
+
+// Allows a cache entry to be loaded directly from cache without further
+// validation - see nsICacheEntry.idl for further details
+void CacheStorageService::ForceEntryValidFor(nsACString &aCacheEntryKey,
+                                             uint32_t aSecondsToTheFuture)
+{
+  mozilla::MutexAutoLock lock(mLock);
+
+  // This will be the timeout
+  TimeStamp validUntil = TimeStamp::NowLoRes() +
+    TimeDuration::FromSeconds(aSecondsToTheFuture);
+
+  mForcedValidEntries.Put(aCacheEntryKey, validUntil);
+}
+
 void
 CacheStorageService::OnMemoryConsumptionChange(CacheMemoryConsumer* aConsumer,
                                                uint32_t aCurrentMemoryConsumption)
 {
   LOG(("CacheStorageService::OnMemoryConsumptionChange [consumer=%p, size=%u]",
     aConsumer, aCurrentMemoryConsumption));
 
   uint32_t savedMemorySize = aConsumer->mReportedMemoryConsumption;
--- a/netwerk/cache2/CacheStorageService.h
+++ b/netwerk/cache2/CacheStorageService.h
@@ -141,16 +141,36 @@ private:
   /**
    * Tells the storage service whether this entry is only to be stored in
    * memory.
    */
   void RecordMemoryOnlyEntry(CacheEntry* aEntry,
                              bool aOnlyInMemory,
                              bool aOverwrite);
 
+  /**
+   * Sets a cache entry valid (overrides the default loading behavior by loading
+   * directly from cache) for the given number of seconds
+   * See nsICacheEntry.idl for more details
+   */
+  void ForceEntryValidFor(nsACString &aCacheEntryKey,
+                          uint32_t aSecondsToTheFuture);
+
+private:
+  friend class CacheIndex;
+
+  /**
+   * Retrieves the status of the cache entry to see if it has been forced valid
+   * (so it will loaded directly from cache without further validation)
+   * CacheIndex uses this to prevent a cache entry from being prememptively
+   * thrown away when forced valid
+   * See nsICacheEntry.idl for more details
+   */
+  bool IsForcedValidEntry(nsACString &aCacheEntryKey);
+
 private:
   // These are helpers for telemetry monitorying of the memory pools.
   void TelemetryPrune(TimeStamp &now);
   void TelemetryRecordEntryCreation(CacheEntry const* entry);
   void TelemetryRecordEntryRemoval(CacheEntry const* entry);
 
 private:
   // Following methods are thread safe to call.
@@ -259,16 +279,19 @@ private:
                            bool aCreateIfNotExist,
                            bool aReplace,
                            CacheEntryHandle** aResult);
 
   static CacheStorageService* sSelf;
 
   mozilla::Mutex mLock;
 
+  // Tracks entries that may be forced valid.
+  nsDataHashtable<nsCStringHashKey, TimeStamp> mForcedValidEntries;
+
   bool mShutdown;
 
   // Accessible only on the service thread
   class MemoryPool
   {
   public:
     enum EType
     {
--- a/netwerk/cache2/OldWrappers.cpp
+++ b/netwerk/cache2/OldWrappers.cpp
@@ -361,16 +361,28 @@ NS_IMETHODIMP
 }
 
 _OldCacheEntryWrapper::~_OldCacheEntryWrapper()
 {
   MOZ_COUNT_DTOR(_OldCacheEntryWrapper);
   LOG(("Destroying _OldCacheEntryWrapper %p for descriptor %p", this, mOldInfo.get()));
 }
 
+NS_IMETHODIMP _OldCacheEntryWrapper::GetIsForcedValid(bool *aIsForcedValid)
+{
+  // Unused stub
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP _OldCacheEntryWrapper::ForceValidFor(uint32_t aSecondsToTheFuture)
+{
+  // Unused stub
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
 NS_IMPL_ISUPPORTS(_OldCacheEntryWrapper, nsICacheEntry)
 
 NS_IMETHODIMP _OldCacheEntryWrapper::AsyncDoom(nsICacheEntryDoomCallback* listener)
 {
   nsRefPtr<DoomCallbackWrapper> cb = listener
     ? new DoomCallbackWrapper(listener)
     : nullptr;
   return AsyncDoom(cb);
--- a/netwerk/cache2/OldWrappers.h
+++ b/netwerk/cache2/OldWrappers.h
@@ -28,16 +28,18 @@ class _OldCacheEntryWrapper : public nsI
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_FORWARD_SAFE_NSICACHEENTRYDESCRIPTOR(mOldDesc)
   NS_FORWARD_NSICACHEENTRYINFO(mOldInfo->)
 
   NS_IMETHOD AsyncDoom(nsICacheEntryDoomCallback* listener);
   NS_IMETHOD GetPersistent(bool *aPersistToDisk);
+  NS_IMETHOD GetIsForcedValid(bool *aIsForcedValid);
+  NS_IMETHOD ForceValidFor(uint32_t aSecondsToTheFuture);
   NS_IMETHOD SetValid() { return NS_OK; }
   NS_IMETHOD MetaDataReady() { return NS_OK; }
   NS_IMETHOD Recreate(bool, nsICacheEntry**);
   NS_IMETHOD GetDataSize(int64_t *size);
   NS_IMETHOD OpenInputStream(int64_t offset, nsIInputStream * *_retval);
   NS_IMETHOD OpenOutputStream(int64_t offset, nsIOutputStream * *_retval);
   NS_IMETHOD MaybeMarkValid();
   NS_IMETHOD HasWriteAccess(bool aWriteOnly, bool *aWriteAccess);
--- a/netwerk/cache2/nsICacheEntry.idl
+++ b/netwerk/cache2/nsICacheEntry.idl
@@ -11,17 +11,17 @@ interface nsICacheEntryDoomCallback;
 // ************************ REMOVE **********************
 typedef long nsCacheAccessMode;
 typedef long nsCacheStoragePolicy;
 
 interface nsICacheListener;
 interface nsIFile;
 interface nsICacheEntryMetaDataVisitor;
 
-[scriptable, uuid(972dc51d-df01-4b1e-b7f3-76dbcc603b1e)]
+[scriptable, uuid(607c2a2c-0a48-40b9-a956-8cf2bb9857cf)]
 interface nsICacheEntry : nsISupports
 {
   /**
    * Placeholder for the initial value of expiration time.
    */
   const unsigned long NO_EXPIRATION_TIME = 0xFFFFFFFF;
 
   /**
@@ -58,16 +58,38 @@ interface nsICacheEntry : nsISupports
 
   /**
    * Set the time at which the cache entry should be considered invalid (in
    * seconds since the Epoch).
    */
   void setExpirationTime(in uint32_t expirationTime);
 
   /**
+   * This method is intended to override the per-spec cache validation
+   * decisions for a duration specified in seconds. The current state can
+   * be examined with isForcedValid (see below). This value is not persisted,
+   * so it will not survive session restart. Cache entries that are forced valid
+   * will not be evicted from the cache for the duration of forced validity.
+   * This means that there is a potential problem if the number of forced valid
+   * entries grows to take up more space than the cache size allows.
+   *
+   * @param aSecondsToTheFuture
+   *        the number of seconds the default cache validation behavior will be
+   *        overridden before it returns to normal
+   */
+  void forceValidFor(in unsigned long aSecondsToTheFuture);
+
+  /**
+   * The state variable for whether this entry is currently forced valid.
+   * Defaults to false for normal cache validation behavior, and will return
+   * true if the number of seconds set by forceValidFor() has yet to be reached.
+   */
+  readonly attribute boolean isForcedValid;
+
+  /**
    * Open blocking input stream to cache data.  Use the stream transport
    * service to asynchronously read this stream on a background thread.
    * The returned stream MAY implement nsISeekableStream.
    *
    * @param offset
    *        read starting from this offset into the cached data.  an offset
    *        beyond the end of the stream has undefined consequences.
    *
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_cache2-27-force-valid-for.js
@@ -0,0 +1,37 @@
+Components.utils.import('resource://gre/modules/LoadContextInfo.jsm');
+
+function run_test()
+{
+  do_get_profile();
+
+  if (!newCacheBackEndUsed()) {
+    do_check_true(true, "This test checks only cache2 specific behavior.");
+    return;
+  }
+
+  var mc = new MultipleCallbacks(2, function() {
+    finish_cache2_test();
+  });
+
+  asyncOpenCacheEntry("http://m1/", "memory", Ci.nsICacheStorage.OPEN_NORMALLY, LoadContextInfo.default,
+    new OpenCallback(NEW, "meta", "data", function(entry) {
+      // Check the default
+      equal(entry.isForcedValid, false);
+
+      // Forced valid and confirm
+      entry.forceValidFor(2);
+      do_timeout(1000, function() {
+        equal(entry.isForcedValid, true);
+        mc.fired();
+      });
+
+      // Confirm the timeout occurs
+      do_timeout(3000, function() {
+        equal(entry.isForcedValid, false);
+        mc.fired();
+      });
+    })
+  );
+
+  do_test_pending();
+}
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -62,17 +62,20 @@ skip-if = os == "android"
 [test_cache2-21-anon-storage.js]
 [test_cache2-22-anon-visit.js]
 [test_cache2-23-read-over-chunk.js]
 [test_cache2-24-exists.js]
 # Bug 675039, comment 6: "The difference is that the memory cache is disabled in Armv6 builds."
 skip-if = os == "android"
 [test_cache2-25-chunk-memory-limit.js]
 [test_cache2-26-no-outputstream-open.js]
-# GC, that this patch is depenedent on, doesn't work well on Android."
+# GC, that this patch is dependent on, doesn't work well on Android.
+skip-if = os == "android"
+[test_cache2-27-force-valid-for.js]
+# Bug 675039, comment 6: "The difference is that the memory cache is disabled in Armv6 builds."
 skip-if = os == "android"
 [test_304_responses.js]
 # Bug 675039: test hangs on Android-armv6 
 skip-if = os == "android"
 [test_cacheForOfflineUse_no-store.js]
 [test_307_redirect.js]
 [test_NetUtil.js]
 [test_URIs.js]