Bug 1267941 - Implement Storage API estimate() feature, r=janv,baku
authorShawn Huang <shuang@mozilla.com>
Tue, 14 Jun 2016 13:57:36 +0100
changeset 312966 e753af61eb0221f850b557127ad99768d9c0e0a9
parent 312965 c5ed16e1dbc645100a0245a82d83995d52207f41
child 312967 95e68b473e91d27a6c469c4a4765aa04e784c800
push id30665
push usercbook@mozilla.com
push dateWed, 07 Sep 2016 15:20:43 +0000
treeherdermozilla-central@95acb9299faf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanv, baku
bugs1267941
milestone51.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 1267941 - Implement Storage API estimate() feature, r=janv,baku
dom/base/Navigator.cpp
dom/base/Navigator.h
dom/cache/test/mochitest/test_cache_orphaned_body.html
dom/indexedDB/test/mochitest.ini
dom/indexedDB/test/test_storage_manager_estimate.html
dom/indexedDB/test/unit/test_storage_manager_estimate.js
dom/quota/QuotaManagerService.cpp
dom/quota/StorageManager.cpp
dom/quota/StorageManager.h
dom/quota/moz.build
dom/tests/mochitest/general/test_interfaces.html
dom/webidl/Navigator.webidl
dom/webidl/StorageManager.webidl
dom/webidl/WorkerNavigator.webidl
dom/webidl/moz.build
dom/workers/WorkerNavigator.cpp
dom/workers/WorkerNavigator.h
dom/workers/test/navigator_worker.js
dom/workers/test/serviceworkers/serviceworker_wrapper.js
dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
dom/workers/test/test_navigator.html
dom/workers/test/test_worker_interfaces.js
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -42,16 +42,17 @@
 #include "mozilla/dom/FlyWebPublishedServer.h"
 #include "mozilla/dom/FlyWebService.h"
 #include "mozilla/dom/IccManager.h"
 #include "mozilla/dom/InputPortManager.h"
 #include "mozilla/dom/MobileMessageManager.h"
 #include "mozilla/dom/Permissions.h"
 #include "mozilla/dom/Presentation.h"
 #include "mozilla/dom/ServiceWorkerContainer.h"
+#include "mozilla/dom/StorageManager.h"
 #include "mozilla/dom/TCPSocket.h"
 #include "mozilla/dom/Telephony.h"
 #include "mozilla/dom/Voicemail.h"
 #include "mozilla/dom/TVManager.h"
 #include "mozilla/dom/VRDisplay.h"
 #include "mozilla/dom/workers/RuntimeService.h"
 #include "mozilla/Hal.h"
 #include "nsISiteSpecificUserAgent.h"
@@ -227,16 +228,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCellBroadcast)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIccManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMobileMessageManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTelephony)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVoicemail)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTVManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputPortManager)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConnection)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStorageManager)
 #ifdef MOZ_B2G_RIL
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMobileConnections)
 #endif
 #ifdef MOZ_B2G_BT
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBluetooth)
 #endif
 #ifdef MOZ_AUDIO_CHANNEL_MANAGER
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelManager)
@@ -271,16 +273,18 @@ Navigator::Invalidate()
 
   if (mPlugins) {
     mPlugins->Invalidate();
     mPlugins = nullptr;
   }
 
   mPermissions = nullptr;
 
+  mStorageManager = nullptr;
+
   // If there is a page transition, make sure delete the geolocation object.
   if (mGeolocation) {
     mGeolocation->Shutdown();
     mGeolocation = nullptr;
   }
 
   if (mNotification) {
     mNotification->Shutdown();
@@ -653,16 +657,31 @@ Navigator::GetPermissions(ErrorResult& a
 
   if (!mPermissions) {
     mPermissions = new Permissions(mWindow);
   }
 
   return mPermissions;
 }
 
+StorageManager*
+Navigator::Storage()
+{
+  MOZ_ASSERT(mWindow);
+
+  if(!mStorageManager) {
+    nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);
+    MOZ_ASSERT(global);
+
+    mStorageManager = new StorageManager(global);
+  }
+
+  return mStorageManager;
+}
+
 // Values for the network.cookie.cookieBehavior pref are documented in
 // nsCookieService.cpp.
 #define COOKIE_BEHAVIOR_REJECT 2
 
 bool
 Navigator::CookieEnabled()
 {
   bool cookieEnabled =
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -96,16 +96,17 @@ class IccManager;
 class Telephony;
 class Voicemail;
 class TVManager;
 class InputPortManager;
 class DeviceStorageAreaListener;
 class Presentation;
 class LegacyMozTCPSocket;
 class VRDisplay;
+class StorageManager;
 
 namespace time {
 class TimeManager;
 } // namespace time
 
 namespace system {
 #ifdef MOZ_AUDIO_CHANNEL_MANAGER
 class AudioChannelManager;
@@ -282,16 +283,18 @@ public:
                               ErrorResult& aRv);
 
   already_AddRefed<ServiceWorkerContainer> ServiceWorker();
 
   void GetLanguages(nsTArray<nsString>& aLanguages);
 
   bool MozE10sEnabled();
 
+  StorageManager* Storage();
+
   static void GetAcceptLanguages(nsTArray<nsString>& aLanguages);
 
   // WebIDL helper methods
   static bool HasWakeLockSupport(JSContext* /* unused*/, JSObject* /*unused */);
   static bool HasCameraSupport(JSContext* /* unused */,
                                JSObject* aGlobal);
   static bool HasWifiManagerSupport(JSContext* /* unused */,
                                   JSObject* aGlobal);
@@ -372,14 +375,15 @@ private:
   nsCOMPtr<nsPIDOMWindowInner> mWindow;
   RefPtr<DeviceStorageAreaListener> mDeviceStorageAreaListener;
   RefPtr<Presentation> mPresentation;
 #ifdef MOZ_GAMEPAD
   RefPtr<GamepadServiceTest> mGamepadServiceTest;
 #endif
   nsTArray<RefPtr<Promise> > mVRGetDisplaysPromises;
   nsTArray<uint32_t> mRequestedVibrationPattern;
+  RefPtr<StorageManager> mStorageManager;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_Navigator_h
--- a/dom/cache/test/mochitest/test_cache_orphaned_body.html
+++ b/dom/cache/test/mochitest/test_cache_orphaned_body.html
@@ -38,16 +38,42 @@ function storageUsage() {
     var principal = SpecialPowers.wrap(document).nodePrincipal;
     var cb = SpecialPowers.wrapCallback(function(request) {
       resolve(request.usage, request.fileUsage);
     });
     qms.getUsageForPrincipal(principal, cb);
   });
 }
 
+function groupUsage() {
+  return new Promise(function(resolve, reject) {
+   navigator.storage.estimate().then(storageEstimation => {
+     resolve(storageEstimation.usage, 0);
+   });
+  });
+}
+
+function workerGroupUsage() {
+  return new Promise(function(resolve, reject) {
+    function workerScript() {
+      navigator.storage.estimate().then(storageEstimation => {
+        postMessage(storageEstimation.usage);
+      });
+    }
+
+    let url =
+      URL.createObjectURL(new Blob(["(", workerScript.toSource(), ")()"]));
+
+    let worker = new Worker(url);
+    worker.onmessage = function (e) {
+      resolve(e.data, 0);
+    };
+  });
+}
+
 function resetStorage() {
   return new Promise(function(resolve, reject) {
     var qms = SpecialPowers.Services.qms;
     var request = qms.reset();
     var cb = SpecialPowers.wrapCallback(resolve);
     request.callback = cb;
   });
 }
@@ -137,16 +163,36 @@ SpecialPowers.pushPrefEnv({
     return resetStorage();
   }).then(function() {
     return storageUsage();
   }).then(function(usage) {
     fullUsage = usage;
     ok(fullUsage > initialUsage, 'disk usage should have grown');
   })
 
+  // Test groupUsage()
+  .then(function() {
+    return resetStorage();
+  }).then(function() {
+    return groupUsage();
+  }).then(function(usage) {
+    fullUsage = usage;
+    ok(fullUsage > initialUsage, 'disk group usage should have grown');
+  })
+
+  // Test workerGroupUsage()
+  .then(function() {
+    return resetStorage();
+  }).then(function() {
+    return workerGroupUsage();
+  }).then(function(usage) {
+    fullUsage = usage;
+    ok(fullUsage > initialUsage, 'disk group usage on worker should have grown');
+  })
+
   // Now perform a new Cache operation that will reopen the origin.  This
   // should clean up the orphaned body.
   .then(function() {
     return caches.match(url);
   }).then(function(r) {
     ok(!r, 'response should not exist in storage');
   })
 
--- a/dom/indexedDB/test/mochitest.ini
+++ b/dom/indexedDB/test/mochitest.ini
@@ -91,16 +91,17 @@ support-files =
   unit/test_rename_objectStore.js
   unit/test_rename_objectStore_errors.js
   unit/test_request_readyState.js
   unit/test_setVersion.js
   unit/test_setVersion_abort.js
   unit/test_setVersion_events.js
   unit/test_setVersion_exclusion.js
   unit/test_setVersion_throw.js
+  unit/test_storage_manager_estimate.js
   unit/test_success_events_after_abort.js
   unit/test_table_locks.js
   unit/test_table_rollback.js
   unit/test_temporary_storage.js
   unit/test_traffic_jam.js
   unit/test_transaction_abort.js
   unit/test_transaction_abort_hang.js
   unit/test_transaction_duplicate_store_names.js
@@ -350,16 +351,18 @@ skip-if = (buildapp == 'b2g' && toolkit 
 [test_setVersion_abort.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_setVersion_events.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_setVersion_exclusion.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_setVersion_throw.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
+[test_storage_manager_estimate.html]
+skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_success_events_after_abort.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_table_locks.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_table_rollback.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
 [test_third_party.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') # Bug 931116
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/test_storage_manager_estimate.html
@@ -0,0 +1,19 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>Indexed Database Test for StorageManager</title>
+
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+  <script type="text/javascript;version=1.7" src="unit/test_storage_manager_estimate.js"></script>
+  <script type="text/javascript;version=1.7" src="helpers.js"></script>
+
+</head>
+
+<body onload="runTest();"></body>
+
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/unit/test_storage_manager_estimate.js
@@ -0,0 +1,56 @@
+var testGenerator = testSteps();
+
+function testSteps()
+{
+  const name = this.window ? window.location.pathname :
+	       "test_storage_manager_estimate.js";
+  const objectStoreName = "storagesManager";
+  const arraySize = 1e6;
+
+  ok('estimate' in navigator.storage, 'Has estimate function');
+  is(typeof navigator.storage.estimate, 'function', 'estimate is function');
+  ok(navigator.storage.estimate() instanceof Promise,
+     'estimate() method exists and returns a Promise');
+
+  navigator.storage.estimate().then(estimation => {
+    testGenerator.send(estimation.usage);
+  });
+
+  let before = yield undefined;
+
+  let request = indexedDB.open(name, 1);
+  request.onerror = errorHandler;
+  request.onupgradeneeded = grabEventAndContinueHandler;
+  request.onsuccess = continueToNextStep;
+  let event = yield undefined;
+
+  let db = event.target.result;
+  db.onerror = errorHandler;
+
+  let objectStore = db.createObjectStore(objectStoreName, { });
+  yield undefined;
+
+  navigator.storage.estimate().then(estimation => {
+    testGenerator.send(estimation.usage);
+  });
+  let usageAfterCreate = yield undefined;
+  ok(usageAfterCreate > before, 'estimated usage must increase after createObjectStore');
+
+  let txn = db.transaction(objectStoreName, "readwrite");
+  objectStore = txn.objectStore(objectStoreName);
+  objectStore.put(new Uint8Array(arraySize), 'k');
+  txn.oncomplete = continueToNextStep;
+  txn.onabort = errorHandler;
+  txn.onerror = errorHandler;
+  event = yield undefined;
+
+  navigator.storage.estimate().then(estimation => {
+    testGenerator.send(estimation.usage);
+  });
+  let usageAfterPut = yield undefined;
+  ok(usageAfterPut > usageAfterCreate, 'estimated usage must increase after putting large object');
+  db.close();
+
+  finishTest();
+  yield undefined;
+}
--- a/dom/quota/QuotaManagerService.cpp
+++ b/dom/quota/QuotaManagerService.cpp
@@ -498,17 +498,16 @@ NS_IMETHODIMP
 QuotaManagerService::GetUsageForPrincipal(nsIPrincipal* aPrincipal,
                                           nsIQuotaUsageCallback* aCallback,
                                           bool aGetGroupUsage,
                                           nsIQuotaUsageRequest** _retval)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aPrincipal);
   MOZ_ASSERT(aCallback);
-  MOZ_ASSERT(nsContentUtils::IsCallerChrome());
 
   RefPtr<UsageRequest> request = new UsageRequest(aPrincipal, aCallback);
 
   UsageParams params;
 
   PrincipalInfo& principalInfo = params.principalInfo();
   nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo);
   if (NS_WARN_IF(NS_FAILED(rv))) {
new file mode 100644
--- /dev/null
+++ b/dom/quota/StorageManager.cpp
@@ -0,0 +1,353 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "StorageManager.h"
+
+#include "mozilla/dom/PromiseWorkerProxy.h"
+#include "mozilla/dom/quota/QuotaManagerService.h"
+#include "mozilla/dom/StorageManagerBinding.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/ErrorResult.h"
+#include "nsIQuotaCallbacks.h"
+#include "nsIQuotaRequests.h"
+#include "nsPIDOMWindow.h"
+
+using namespace mozilla::dom::workers;
+
+namespace mozilla {
+namespace dom {
+
+namespace {
+
+// This class is used to get quota usage callback.
+class EstimateResolver final
+  : public nsIQuotaUsageCallback
+{
+  class FinishWorkerRunnable;
+
+  // If this resolver was created for a window then mPromise must be non-null.
+  // Otherwise mProxy must be non-null.
+  RefPtr<Promise> mPromise;
+  RefPtr<PromiseWorkerProxy> mProxy;
+
+  nsresult mResultCode;
+  StorageEstimate mStorageEstimate;
+
+public:
+  explicit EstimateResolver(Promise* aPromise)
+    : mPromise(aPromise)
+    , mResultCode(NS_OK)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(aPromise);
+  }
+
+  explicit EstimateResolver(PromiseWorkerProxy* aProxy)
+    : mProxy(aProxy)
+    , mResultCode(NS_OK)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(aProxy);
+  }
+
+  void
+  ResolveOrReject(Promise* aPromise);
+
+  NS_DECL_THREADSAFE_ISUPPORTS
+
+  NS_DECL_NSIQUOTAUSAGECALLBACK
+
+private:
+  ~EstimateResolver()
+  { }
+};
+
+// This class is used to return promise on worker thread.
+class EstimateResolver::FinishWorkerRunnable final
+  : public WorkerRunnable
+{
+  RefPtr<EstimateResolver> mResolver;
+
+public:
+  explicit FinishWorkerRunnable(EstimateResolver* aResolver)
+    : WorkerRunnable(aResolver->mProxy->GetWorkerPrivate())
+    , mResolver(aResolver)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(aResolver);
+  }
+
+  virtual bool
+  WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override;
+};
+
+class EstimateWorkerMainThreadRunnable
+  : public WorkerMainThreadRunnable
+{
+  RefPtr<PromiseWorkerProxy> mProxy;
+
+public:
+  EstimateWorkerMainThreadRunnable(WorkerPrivate* aWorkerPrivate,
+                                   PromiseWorkerProxy* aProxy)
+    : WorkerMainThreadRunnable(aWorkerPrivate,
+                               NS_LITERAL_CSTRING("StorageManager :: Estimate"))
+    , mProxy(aProxy)
+  {
+    MOZ_ASSERT(aWorkerPrivate);
+    aWorkerPrivate->AssertIsOnWorkerThread();
+    MOZ_ASSERT(aProxy);
+  }
+
+  virtual bool
+  MainThreadRun() override;
+};
+
+nsresult
+GetUsageForPrincipal(nsIPrincipal* aPrincipal,
+                     nsIQuotaUsageCallback* aCallback,
+                     nsIQuotaUsageRequest** aRequest)
+{
+  MOZ_ASSERT(aPrincipal);
+  MOZ_ASSERT(aCallback);
+  MOZ_ASSERT(aRequest);
+
+  nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate();
+  if (NS_WARN_IF(!qms)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsresult rv = qms->GetUsageForPrincipal(aPrincipal, aCallback, true, aRequest);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+};
+
+nsresult
+GetStorageEstimate(nsIQuotaUsageRequest* aRequest,
+                   StorageEstimate& aStorageEstimate)
+{
+  MOZ_ASSERT(aRequest);
+
+  uint64_t usage;
+  nsresult rv = aRequest->GetUsage(&usage);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  uint64_t limit;
+  rv = aRequest->GetLimit(&limit);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  aStorageEstimate.mUsage.Construct() = usage;
+  aStorageEstimate.mQuota.Construct() = limit;
+  return NS_OK;
+}
+
+} // namespace
+
+/*******************************************************************************
+ * Local class implementations
+ ******************************************************************************/
+
+void
+EstimateResolver::ResolveOrReject(Promise* aPromise)
+{
+  MOZ_ASSERT(aPromise);
+
+  if (NS_SUCCEEDED(mResultCode)) {
+    aPromise->MaybeResolve(mStorageEstimate);
+  } else {
+    aPromise->MaybeReject(mResultCode);
+  }
+}
+
+NS_IMPL_ISUPPORTS(EstimateResolver, nsIQuotaUsageCallback)
+
+NS_IMETHODIMP
+EstimateResolver::OnUsageResult(nsIQuotaUsageRequest *aRequest)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aRequest);
+
+  nsresult rv = aRequest->GetResultCode(&mResultCode);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    mResultCode = rv;
+  } else if (NS_SUCCEEDED(mResultCode)) {
+    rv = GetStorageEstimate(aRequest, mStorageEstimate);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      mResultCode = rv;
+    }
+  }
+
+  // In a main thread request.
+  if (!mProxy) {
+    MOZ_ASSERT(mPromise);
+
+    ResolveOrReject(mPromise);
+    return NS_OK;
+  }
+
+  // In a worker thread request.
+  MutexAutoLock lock(mProxy->Lock());
+
+  if (NS_WARN_IF(mProxy->CleanedUp())) {
+    return NS_ERROR_FAILURE;
+  }
+
+  RefPtr<FinishWorkerRunnable> runnable = new FinishWorkerRunnable(this);
+  if (NS_WARN_IF(!runnable->Dispatch())) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+bool
+EstimateResolver::
+FinishWorkerRunnable::WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate)
+{
+  MOZ_ASSERT(aWorkerPrivate);
+  aWorkerPrivate->AssertIsOnWorkerThread();
+
+  RefPtr<PromiseWorkerProxy> proxy = mResolver->mProxy;
+  MOZ_ASSERT(proxy);
+
+  RefPtr<Promise> promise = proxy->WorkerPromise();
+  MOZ_ASSERT(promise);
+
+  mResolver->ResolveOrReject(promise);
+
+  proxy->CleanUp();
+
+  return true;
+}
+
+bool
+EstimateWorkerMainThreadRunnable::MainThreadRun()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  nsCOMPtr<nsIPrincipal> principal;
+
+  {
+    MutexAutoLock lock(mProxy->Lock());
+    if (mProxy->CleanedUp()) {
+      return true;
+    }
+    principal = mProxy->GetWorkerPrivate()->GetPrincipal();
+  }
+
+  MOZ_ASSERT(principal);
+
+  RefPtr<EstimateResolver> resolver = new EstimateResolver(mProxy);
+
+  RefPtr<nsIQuotaUsageRequest> request;
+  nsresult rv =
+    GetUsageForPrincipal(principal, resolver, getter_AddRefs(request));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return false;
+  }
+
+  return true;
+}
+
+/*******************************************************************************
+ * StorageManager
+ ******************************************************************************/
+
+StorageManager::StorageManager(nsIGlobalObject* aGlobal)
+  : mOwner(aGlobal)
+{
+  MOZ_ASSERT(aGlobal);
+}
+
+StorageManager::~StorageManager()
+{
+}
+
+already_AddRefed<Promise>
+StorageManager::Estimate(ErrorResult& aRv)
+{
+  MOZ_ASSERT(mOwner);
+
+  RefPtr<Promise> promise = Promise::Create(mOwner, aRv);
+  if (NS_WARN_IF(!promise)) {
+    return nullptr;
+  }
+
+  if (NS_IsMainThread()) {
+    nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mOwner);
+    if (NS_WARN_IF(!window)) {
+      aRv.Throw(NS_ERROR_FAILURE);
+      return nullptr;
+    }
+
+    nsCOMPtr<nsIDocument> doc = window->GetExtantDoc();
+    if (NS_WARN_IF(!doc)) {
+      aRv.Throw(NS_ERROR_FAILURE);
+      return nullptr;
+    }
+
+    nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
+    MOZ_ASSERT(principal);
+
+    RefPtr<EstimateResolver> resolver = new EstimateResolver(promise);
+
+    RefPtr<nsIQuotaUsageRequest> request;
+    nsresult rv =
+      GetUsageForPrincipal(principal, resolver, getter_AddRefs(request));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      aRv.Throw(rv);
+      return nullptr;
+    }
+
+    return promise.forget();
+  }
+
+  WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+  MOZ_ASSERT(workerPrivate);
+
+  RefPtr<PromiseWorkerProxy> promiseProxy =
+    PromiseWorkerProxy::Create(workerPrivate, promise);
+  if (NS_WARN_IF(!promiseProxy)) {
+    return nullptr;
+  }
+
+  RefPtr<EstimateWorkerMainThreadRunnable> runnnable =
+    new EstimateWorkerMainThreadRunnable(promiseProxy->GetWorkerPrivate(),
+                                         promiseProxy);
+
+  runnnable->Dispatch(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
+    return nullptr;
+  }
+
+  return promise.forget();
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(StorageManager, mOwner)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(StorageManager)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(StorageManager)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StorageManager)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+JSObject*
+StorageManager::WrapObject(JSContext* aCx,
+                           JS::Handle<JSObject*> aGivenProto)
+{
+  return StorageManagerBinding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/quota/StorageManager.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 mozilla_dom_StorageManager_h
+#define mozilla_dom_StorageManager_h
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+namespace dom {
+
+class Promise;
+struct StorageEstimate;
+
+class StorageManager final
+  : public nsISupports
+  , public nsWrapperCache
+{
+  nsCOMPtr<nsIGlobalObject> mOwner;
+
+public:
+  explicit
+  StorageManager(nsIGlobalObject* aGlobal);
+
+  nsIGlobalObject*
+  GetParentObject() const
+  {
+    return mOwner;
+  }
+
+  // WebIDL
+  already_AddRefed<Promise>
+  Estimate(ErrorResult& aRv);
+
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(StorageManager)
+
+  // nsWrapperCache
+  virtual JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+private:
+  ~StorageManager();
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_StorageManager_h
--- a/dom/quota/moz.build
+++ b/dom/quota/moz.build
@@ -7,16 +7,20 @@
 XPIDL_SOURCES += [
     'nsIQuotaCallbacks.idl',
     'nsIQuotaManagerService.idl',
     'nsIQuotaRequests.idl',
 ]
 
 XPIDL_MODULE = 'dom_quota'
 
+EXPORTS.mozilla.dom += [
+  'StorageManager.h',
+]
+
 EXPORTS.mozilla.dom.quota += [
     'ActorsParent.h',
     'Client.h',
     'FileStreams.h',
     'OriginScope.h',
     'PersistenceType.h',
     'QuotaCommon.h',
     'QuotaManager.h',
@@ -27,25 +31,27 @@ EXPORTS.mozilla.dom.quota += [
 ]
 
 UNIFIED_SOURCES += [
     'ActorsChild.cpp',
     'ActorsParent.cpp',
     'FileStreams.cpp',
     'QuotaManagerService.cpp',
     'QuotaRequests.cpp',
+    'StorageManager.cpp',
 ]
 
 IPDL_SOURCES += [
     'PQuota.ipdl',
     'PQuotaRequest.ipdl',
     'PQuotaUsageRequest.ipdl',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 FINAL_LIBRARY = 'xul'
 LOCAL_INCLUDES += [
+    '../workers',
     '/caps',
 ]
 
 if CONFIG['GNU_CXX']:
     CXXFLAGS += ['-Wno-error=shadow']
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -1065,16 +1065,18 @@ var interfaceNamesInGlobalScope =
     {name: "SpecialPowers", xbl: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "StereoPannerNode",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Storage",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "StorageEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "StorageManager",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "StyleSheet",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "StyleSheetList",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "SubtleCrypto",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "SVGAElement",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/webidl/Navigator.webidl
+++ b/dom/webidl/Navigator.webidl
@@ -25,16 +25,17 @@ interface Navigator {
   // objects implementing this interface also implement the interfaces given below
 };
 Navigator implements NavigatorID;
 Navigator implements NavigatorLanguage;
 Navigator implements NavigatorOnLine;
 Navigator implements NavigatorContentUtils;
 Navigator implements NavigatorStorageUtils;
 Navigator implements NavigatorConcurrentHardware;
+Navigator implements NavigatorStorage;
 
 [NoInterfaceObject, Exposed=(Window,Worker)]
 interface NavigatorID {
   // WebKit/Blink/Trident/Presto support this (hardcoded "Mozilla").
   [Constant, Cached]
   readonly attribute DOMString appCodeName; // constant "Mozilla"
   [Constant, Cached]
   readonly attribute DOMString appName;
@@ -80,16 +81,21 @@ interface NavigatorContentUtils {
   void registerContentHandler(DOMString mimeType, DOMString url, DOMString title);
   // NOT IMPLEMENTED
   //DOMString isProtocolHandlerRegistered(DOMString scheme, DOMString url);
   //DOMString isContentHandlerRegistered(DOMString mimeType, DOMString url);
   //void unregisterProtocolHandler(DOMString scheme, DOMString url);
   //void unregisterContentHandler(DOMString mimeType, DOMString url);
 };
 
+[NoInterfaceObject, Exposed=(Window,Worker)]
+interface NavigatorStorage {
+  readonly attribute StorageManager storage;
+};
+
 [NoInterfaceObject]
 interface NavigatorStorageUtils {
   // NOT IMPLEMENTED
   //void yieldForStorageUpdates();
 };
 
 partial interface Navigator {
   [Throws]
new file mode 100644
--- /dev/null
+++ b/dom/webidl/StorageManager.webidl
@@ -0,0 +1,24 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ *
+ * The origin of this IDL file is
+ * https://storage.spec.whatwg.org/#storagemanager
+ *
+ */
+
+[Exposed=(Window,Worker)]
+interface StorageManager {
+  // [Throws]
+  // Promise<boolean> persisted();
+  // [Throws]
+  // [Exposed=Window] Promise<boolean> persist();
+  [Throws]
+  Promise<StorageEstimate> estimate();
+};
+
+dictionary StorageEstimate {
+  unsigned long long usage;
+  unsigned long long quota;
+};
--- a/dom/webidl/WorkerNavigator.webidl
+++ b/dom/webidl/WorkerNavigator.webidl
@@ -6,8 +6,9 @@
 [Exposed=Worker]
 interface WorkerNavigator {
 };
 
 WorkerNavigator implements NavigatorID;
 WorkerNavigator implements NavigatorLanguage;
 WorkerNavigator implements NavigatorOnLine;
 WorkerNavigator implements NavigatorConcurrentHardware;
+WorkerNavigator implements NavigatorStorage;
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -428,16 +428,17 @@ WEBIDL_FILES = [
     'SimpleGestureEvent.webidl',
     'SmsMessage.webidl',
     'SocketCommon.webidl',
     'SourceBuffer.webidl',
     'SourceBufferList.webidl',
     'StereoPannerNode.webidl',
     'Storage.webidl',
     'StorageEvent.webidl',
+    'StorageManager.webidl',
     'StorageType.webidl',
     'StyleSheet.webidl',
     'StyleSheetList.webidl',
     'SubtleCrypto.webidl',
     'SVGAElement.webidl',
     'SVGAngle.webidl',
     'SVGAnimatedAngle.webidl',
     'SVGAnimatedBoolean.webidl',
--- a/dom/workers/WorkerNavigator.cpp
+++ b/dom/workers/WorkerNavigator.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseWorkerProxy.h"
+#include "mozilla/dom/StorageManager.h"
 #include "mozilla/dom/WorkerNavigator.h"
 #include "mozilla/dom/WorkerNavigatorBinding.h"
 
 #include "nsProxyRelease.h"
 #include "RuntimeService.h"
 
 #include "nsIDocument.h"
 
@@ -21,18 +22,17 @@
 
 #include "mozilla/dom/Navigator.h"
 
 namespace mozilla {
 namespace dom {
 
 using namespace mozilla::dom::workers;
 
-NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(WorkerNavigator)
-
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WorkerNavigator, mStorageManager);
 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(WorkerNavigator, AddRef)
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(WorkerNavigator, Release)
 
 /* static */ already_AddRefed<WorkerNavigator>
 WorkerNavigator::Create(bool aOnLine)
 {
   RuntimeService* rts = RuntimeService::GetService();
   MOZ_ASSERT(rts);
@@ -159,10 +159,26 @@ uint64_t
 WorkerNavigator::HardwareConcurrency() const
 {
   RuntimeService* rts = RuntimeService::GetService();
   MOZ_ASSERT(rts);
 
   return rts->ClampedHardwareConcurrency();
 }
 
+StorageManager*
+WorkerNavigator::Storage()
+{
+  if (!mStorageManager) {
+    WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+    MOZ_ASSERT(workerPrivate);
+
+    RefPtr<nsIGlobalObject> global = workerPrivate->GlobalScope();
+    MOZ_ASSERT(global);
+
+    mStorageManager = new StorageManager(global);
+  }
+
+  return mStorageManager;
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/workers/WorkerNavigator.h
+++ b/dom/workers/WorkerNavigator.h
@@ -10,22 +10,24 @@
 #include "Workers.h"
 #include "RuntimeService.h"
 #include "nsString.h"
 #include "nsWrapperCache.h"
 
 namespace mozilla {
 namespace dom {
 class Promise;
+class StorageManager;
 
 class WorkerNavigator final : public nsWrapperCache
 {
   typedef struct workers::RuntimeService::NavigatorProperties NavigatorProperties;
 
   NavigatorProperties mProperties;
+  RefPtr<StorageManager> mStorageManager;
   bool mOnline;
 
   WorkerNavigator(const NavigatorProperties& aProperties,
                   bool aOnline)
     : mProperties(aProperties)
     , mOnline(aOnline)
   {
     MOZ_COUNT_CTOR(WorkerNavigator);
@@ -96,14 +98,16 @@ public:
   void SetOnLine(bool aOnline)
   {
     mOnline = aOnline;
   }
 
   void SetLanguages(const nsTArray<nsString>& aLanguages);
 
   uint64_t HardwareConcurrency() const;
+
+  StorageManager* Storage();
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_workernavigator_h__
--- a/dom/workers/test/navigator_worker.js
+++ b/dom/workers/test/navigator_worker.js
@@ -10,16 +10,17 @@ var supportedProps = [
   "appVersion",
   "platform",
   "product",
   "userAgent",
   "onLine",
   "language",
   "languages",
   "hardwareConcurrency",
+  "storage"
 ];
 
 self.onmessage = function(event) {
   if (!event || !event.data) {
     return;
   }
 
   startTest(event.data.isB2G);
--- a/dom/workers/test/serviceworkers/serviceworker_wrapper.js
+++ b/dom/workers/test/serviceworkers/serviceworker_wrapper.js
@@ -93,16 +93,29 @@ function workerTestGetIsB2G(cb) {
     removeEventListener('message', workerTestGetIsB2GCB);
     cb(e.data.result);
   });
   client.postMessage({
     type: 'getIsB2G'
   });
 }
 
+function workerTestGetStorageManager(cb) {
+  addEventListener('message', function workerTestGetStorageManagerCB(e) {
+    if (e.data.type !== 'returnStorageManager') {
+      return;
+    }
+    removeEventListener('message', workerTestGetStorageManagerCB);
+    cb(e.data.result);
+  });
+  client.postMessage({
+    type: 'getStorageManager'
+  });
+}
+
 addEventListener('message', function workerWrapperOnMessage(e) {
   removeEventListener('message', workerWrapperOnMessage);
   var data = e.data;
   self.clients.matchAll().then(function(clients) {
     client = clients[0];
     try {
       importScripts(data.script);
     } catch(e) {
--- a/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
+++ b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
@@ -191,16 +191,18 @@ var interfaceNamesInGlobalScope =
     "Response",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ServiceWorker",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ServiceWorkerGlobalScope",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ServiceWorkerRegistration",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "StorageManager",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "SubtleCrypto",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "TextDecoder",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "TextEncoder",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "URL",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/workers/test/test_navigator.html
+++ b/dom/workers/test/test_navigator.html
@@ -35,16 +35,21 @@ Tests of DOM Worker Navigator
       return;
     }
 
     if (args.name === "languages") {
       is(navigator.languages.toString(), args.value.toString(), "languages matches");
       return;
     }
 
+    if (args.name === "storage") {
+      is(typeof navigator.storage, typeof args.value, "storage type matches");
+      return;
+    }
+
     is(navigator[args.name], args.value,
        "Mismatched navigator string for " + args.name + "!");
   };
 
   worker.onerror = function(event) {
     ok(false, "Worker had an error: " + event.message);
     SimpleTest.finish();
   }
--- a/dom/workers/test/test_worker_interfaces.js
+++ b/dom/workers/test/test_worker_interfaces.js
@@ -175,16 +175,18 @@ var interfaceNamesInGlobalScope =
     { name: "PushSubscriptionOptions", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Request",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Response",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "ServiceWorkerRegistration", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "StorageManager",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "SubtleCrypto",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "TextDecoder",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "TextEncoder",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "XMLHttpRequest",
 // IMPORTANT: Do not change this list without review from a DOM peer!