Bug 901097 - FileReader API in workers, r=sicking, r=nfroyd
authorAndrea Marchesini <amarchesini@mozilla.com>
Fri, 18 Dec 2015 08:44:00 +0000
changeset 276915 6848f6651cceda6757e1bee4bd1918394d18c5b0
parent 276914 289faf063a573614a824a1956299c5bacc8c2af7
child 276916 09fbc07f80c0226ef49e02e5e64a517420c8384f
push id29810
push usercbook@mozilla.com
push dateFri, 18 Dec 2015 14:24:54 +0000
treeherdermozilla-central@c5cb194cc9cb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssicking, nfroyd
bugs901097
milestone46.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 901097 - FileReader API in workers, r=sicking, r=nfroyd
dom/base/FileReader.cpp
dom/base/FileReader.h
dom/webidl/FileReader.webidl
dom/workers/test/fileapi_chromeScript.js
dom/workers/test/mochitest.ini
dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
dom/workers/test/test_fileReader.html
dom/workers/test/test_worker_interfaces.js
dom/workers/test/worker_fileReader.js
testing/web-platform/meta/FileAPI/idlharness.worker.js.ini
xpcom/io/nsStreamUtils.cpp
xpcom/threads/TimerThread.cpp
xpcom/threads/nsICancelableRunnable.idl
--- a/dom/base/FileReader.cpp
+++ b/dom/base/FileReader.cpp
@@ -1,44 +1,44 @@
 /* -*- 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 "FileReader.h"
 
-#include "nsContentCID.h"
-#include "nsContentUtils.h"
-#include "nsDOMClassInfoID.h"
-#include "nsError.h"
-#include "nsIFile.h"
-#include "nsNetCID.h"
-#include "nsNetUtil.h"
+#include "nsIEventTarget.h"
+#include "nsIGlobalObject.h"
+#include "nsITimer.h"
+#include "nsITransport.h"
+#include "nsIStreamTransportService.h"
 
-#include "nsXPCOM.h"
-#include "nsIDOMEventListener.h"
-#include "nsJSEnvironment.h"
-#include "nsCycleCollectionParticipant.h"
 #include "mozilla/Base64.h"
+#include "mozilla/dom/DOMError.h"
 #include "mozilla/dom/EncodingUtils.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/FileReaderBinding.h"
 #include "mozilla/dom/ProgressEvent.h"
+#include "nsContentUtils.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDOMJSUtils.h"
+#include "nsError.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
 #include "xpcpublic.h"
-#include "nsDOMJSUtils.h"
 
-#include "jsfriendapi.h"
-
-#include "nsITransport.h"
-#include "nsIStreamTransportService.h"
+#include "WorkerPrivate.h"
+#include "WorkerScope.h"
 
 namespace mozilla {
 namespace dom {
 
+using namespace workers;
+
 #define ABORT_STR "abort"
 #define LOAD_STR "load"
 #define LOADSTART_STR "loadstart"
 #define LOADEND_STR "loadend"
 #define ERROR_STR "error"
 #define PROGRESS_STR "progress"
 
 const uint64_t kUnknownSize = uint64_t(-1);
@@ -51,17 +51,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
                                                   DOMEventTargetHelper)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBlob)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mProgressNotifier)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mError)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(FileReader,
                                                 DOMEventTargetHelper)
-  tmp->mResultArrayBuffer = nullptr;
+  tmp->Shutdown();
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mBlob)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mProgressNotifier)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mError)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(FileReader,
                                                DOMEventTargetHelper)
   NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mResultArrayBuffer)
@@ -71,54 +71,81 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_
   NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
   NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
   NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
 NS_IMPL_ADDREF_INHERITED(FileReader, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(FileReader, DOMEventTargetHelper)
 
+class MOZ_RAII FileReaderDecreaseBusyCounter
+{
+  RefPtr<FileReader> mFileReader;
+public:
+  explicit FileReaderDecreaseBusyCounter(FileReader* aFileReader)
+    : mFileReader(aFileReader)
+  {}
+
+  ~FileReaderDecreaseBusyCounter()
+  {
+    mFileReader->DecreaseBusyCounter();
+  }
+};
+
 void
 FileReader::RootResultArrayBuffer()
 {
   mozilla::HoldJSObjects(this);
 }
 
 //FileReader constructors/initializers
 
-FileReader::FileReader(nsPIDOMWindow* aWindow)
+FileReader::FileReader(nsPIDOMWindow* aWindow,
+                       WorkerPrivate* aWorkerPrivate)
   : DOMEventTargetHelper(aWindow)
   , mFileData(nullptr)
   , mDataLen(0)
   , mDataFormat(FILE_AS_BINARY)
   , mResultArrayBuffer(nullptr)
   , mProgressEventWasDelayed(false)
   , mTimerIsActive(false)
   , mReadyState(EMPTY)
   , mTotal(0)
   , mTransferred(0)
+  , mTarget(do_GetCurrentThread())
+  , mBusyCount(0)
+  , mWorkerPrivate(aWorkerPrivate)
 {
+  MOZ_ASSERT_IF(!NS_IsMainThread(), mWorkerPrivate && !aWindow);
+  MOZ_ASSERT_IF(NS_IsMainThread(), !mWorkerPrivate);
   SetDOMStringToNull(mResult);
 }
 
 FileReader::~FileReader()
 {
-  FreeFileData();
-  mResultArrayBuffer = nullptr;
+  Shutdown();
   DropJSObjects(this);
 }
 
 /* static */ already_AddRefed<FileReader>
 FileReader::Constructor(const GlobalObject& aGlobal, ErrorResult& aRv)
 {
   // The owner can be null when this object is used by chrome code.
   nsCOMPtr<nsPIDOMWindow> owner = do_QueryInterface(aGlobal.GetAsSupports());
-  RefPtr<FileReader> fileReader = new FileReader(owner);
+  WorkerPrivate* workerPrivate = nullptr;
 
-  if (!owner && nsContentUtils::IsCallerChrome()) {
+  if (!NS_IsMainThread()) {
+    JSContext* cx = aGlobal.Context();
+    workerPrivate = GetWorkerPrivateFromContext(cx);
+    MOZ_ASSERT(workerPrivate);
+  }
+
+  RefPtr<FileReader> fileReader = new FileReader(owner, workerPrivate);
+
+  if (!owner && nsContentUtils::ThreadsafeIsCallerChrome()) {
     // Instead of grabbing some random global from the context stack,
     // let's use the default one (junk scope) for now.
     // We should move away from this Init...
     fileReader->BindToOwner(xpc::NativeGlobal(xpc::PrivilegedJunkScope()));
   }
 
   return fileReader.forget();
 }
@@ -210,17 +237,27 @@ FileReader::DoOnLoadEnd(nsresult aStatus
 
   aSuccessEvent = NS_LITERAL_STRING(LOAD_STR);
   aTerminationEvent = NS_LITERAL_STRING(LOADEND_STR);
 
   nsresult rv = NS_OK;
   switch (mDataFormat) {
     case FILE_AS_ARRAYBUFFER: {
       AutoJSAPI jsapi;
-      if (NS_WARN_IF(!jsapi.Init(DOMEventTargetHelper::GetParentObject()))) {
+      nsCOMPtr<nsIGlobalObject> globalObject;
+
+      if (NS_IsMainThread()) {
+        globalObject = do_QueryInterface(GetParentObject());
+      } else {
+        MOZ_ASSERT(mWorkerPrivate);
+        MOZ_ASSERT(mBusyCount);
+        globalObject = mWorkerPrivate->GlobalScope();
+      }
+
+      if (!globalObject || !jsapi.Init(globalObject)) {
         FreeFileData();
         return NS_ERROR_FAILURE;
       }
 
       RootResultArrayBuffer();
       mResultArrayBuffer = JS_NewArrayBufferWithContents(jsapi.cx(), mDataLen, mFileData);
       if (!mResultArrayBuffer) {
         JS_ClearPendingException(jsapi.cx());
@@ -251,48 +288,68 @@ FileReader::DoOnLoadEnd(nsresult aStatus
   mResult.SetIsVoid(false);
 
   FreeFileData();
 
   return rv;
 }
 
 nsresult
-FileReader::DoReadData(nsIAsyncInputStream* aStream, uint64_t aCount)
+FileReader::DoAsyncWait()
 {
-  MOZ_ASSERT(aStream);
+  nsresult rv = IncreaseBusyCounter();
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mAsyncStream->AsyncWait(this,
+                               /* aFlags*/ 0,
+                               /* aRequestedCount */ 0,
+                               mTarget);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    DecreaseBusyCounter();
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+FileReader::DoReadData(uint64_t aCount)
+{
+  MOZ_ASSERT(mAsyncStream);
 
   if (mDataFormat == FILE_AS_BINARY) {
     //Continuously update our binary string as data comes in
     uint32_t oldLen = mResult.Length();
     NS_ASSERTION(mResult.Length() == mDataLen, "unexpected mResult length");
     if (uint64_t(oldLen) + aCount > UINT32_MAX)
       return NS_ERROR_OUT_OF_MEMORY;
     char16_t *buf = nullptr;
     mResult.GetMutableData(&buf, oldLen + aCount, fallible);
     NS_ENSURE_TRUE(buf, NS_ERROR_OUT_OF_MEMORY);
 
     uint32_t bytesRead = 0;
-    aStream->ReadSegments(ReadFuncBinaryString, buf + oldLen, aCount,
-                          &bytesRead);
+    mAsyncStream->ReadSegments(ReadFuncBinaryString, buf + oldLen, aCount,
+                               &bytesRead);
     NS_ASSERTION(bytesRead == aCount, "failed to read data");
   }
   else {
     //Update memory buffer to reflect the contents of the file
     if (mDataLen + aCount > UINT32_MAX) {
       // PR_Realloc doesn't support over 4GB memory size even if 64-bit OS
       return NS_ERROR_OUT_OF_MEMORY;
     }
     if (mDataFormat != FILE_AS_ARRAYBUFFER) {
       mFileData = (char *) realloc(mFileData, mDataLen + aCount);
       NS_ENSURE_TRUE(mFileData, NS_ERROR_OUT_OF_MEMORY);
     }
 
     uint32_t bytesRead = 0;
-    aStream->Read(mFileData + mDataLen, aCount, &bytesRead);
+    mAsyncStream->Read(mFileData + mDataLen, aCount, &bytesRead);
     NS_ASSERTION(bytesRead == aCount, "failed to read data");
   }
 
   mDataLen += aCount;
   return NS_OK;
 }
 
 // Helper methods
@@ -356,20 +413,17 @@ FileReader::ReadFileContent(Blob& aBlob,
   mAsyncStream = do_QueryInterface(wrapper);
   MOZ_ASSERT(mAsyncStream);
 
   mTotal = mBlob->GetSize(aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
   }
 
-  aRv = mAsyncStream->AsyncWait(this,
-                                /* aFlags*/ 0,
-                                /* aRequestedCount */ 0,
-                                NS_GetCurrentThread());
+  aRv = DoAsyncWait();
   if (NS_WARN_IF(aRv.Failed())) {
     return;
   }
 
   //FileReader should be in loading state here
   mReadyState = LOADING;
   DispatchProgressEvent(NS_LITERAL_STRING(LOADSTART_STR));
 
@@ -462,16 +516,17 @@ FileReader::StartProgressEventTimer()
   if (!mProgressNotifier) {
     mProgressNotifier = do_CreateInstance(NS_TIMER_CONTRACTID);
   }
 
   if (mProgressNotifier) {
     mProgressEventWasDelayed = false;
     mTimerIsActive = true;
     mProgressNotifier->Cancel();
+    mProgressNotifier->SetTarget(mTarget);
     mProgressNotifier->InitWithCallback(this, NS_PROGRESS_EVENT_INTERVAL,
                                         nsITimer::TYPE_ONE_SHOT);
   }
 }
 
 void
 FileReader::ClearProgressEventTimer()
 {
@@ -545,28 +600,30 @@ FileReader::Notify(nsITimer* aTimer)
 // InputStreamCallback
 NS_IMETHODIMP
 FileReader::OnInputStreamReady(nsIAsyncInputStream* aStream)
 {
   if (mReadyState != LOADING || aStream != mAsyncStream) {
     return NS_OK;
   }
 
+  // We use this class to decrease the busy counter at the end of this method.
+  // In theory we can do it immediatelly but, for debugging reasons, we want to
+  // be 100% sure we have a feature when OnLoadEnd() is called.
+  FileReaderDecreaseBusyCounter RAII(this);
+
   uint64_t aCount;
   nsresult rv = aStream->Available(&aCount);
 
   if (NS_SUCCEEDED(rv) && aCount) {
-    rv = DoReadData(aStream, aCount);
+    rv = DoReadData(aCount);
   }
 
   if (NS_SUCCEEDED(rv)) {
-    rv = aStream->AsyncWait(this,
-                            /* aFlags*/ 0,
-                            /* aRequestedCount */ 0,
-                            NS_GetCurrentThread());
+    rv = DoAsyncWait();
   }
 
   if (NS_FAILED(rv) || !aCount) {
     if (rv == NS_BASE_STREAM_CLOSED) {
       rv = NS_OK;
     }
     return OnLoadEnd(rv);
   }
@@ -638,10 +695,61 @@ FileReader::Abort(ErrorResult& aRv)
   //Clean up memory buffer
   FreeFileData();
 
   // Dispatch the events
   DispatchProgressEvent(NS_LITERAL_STRING(ABORT_STR));
   DispatchProgressEvent(NS_LITERAL_STRING(LOADEND_STR));
 }
 
+nsresult
+FileReader::IncreaseBusyCounter()
+{
+  if (mWorkerPrivate && mBusyCount++ == 0 &&
+      !mWorkerPrivate->AddFeature(mWorkerPrivate->GetJSContext(), this)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+void
+FileReader::DecreaseBusyCounter()
+{
+  MOZ_ASSERT_IF(mWorkerPrivate, mBusyCount);
+  if (mWorkerPrivate && --mBusyCount == 0) {
+    mWorkerPrivate->RemoveFeature(mWorkerPrivate->GetJSContext(), this);
+  }
+}
+
+bool
+FileReader::Notify(JSContext* aCx, Status aStatus)
+{
+  MOZ_ASSERT(mWorkerPrivate);
+  mWorkerPrivate->AssertIsOnWorkerThread();
+
+  if (aStatus > Running) {
+    Shutdown();
+  }
+
+  return true;
+}
+
+void
+FileReader::Shutdown()
+{
+  FreeFileData();
+  mResultArrayBuffer = nullptr;
+
+  if (mAsyncStream) {
+    mAsyncStream->Close();
+    mAsyncStream = nullptr;
+  }
+
+  if (mWorkerPrivate && mBusyCount != 0) {
+    mWorkerPrivate->RemoveFeature(mWorkerPrivate->GetJSContext(), this);
+    mWorkerPrivate = nullptr;
+    mBusyCount = 0;
+  }
+}
+
 } // dom namespace
 } // mozilla namespace
--- a/dom/base/FileReader.h
+++ b/dom/base/FileReader.h
@@ -4,57 +4,67 @@
  * 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_FileReader_h
 #define mozilla_dom_FileReader_h
 
 #include "mozilla/Attributes.h"
 #include "mozilla/DOMEventTargetHelper.h"
-#include "mozilla/dom/DOMError.h"
 
-#include "nsCOMPtr.h"
 #include "nsIAsyncInputStream.h"
-#include "nsIStreamListener.h"
-#include "nsISupportsUtils.h"
 #include "nsIInterfaceRequestor.h"
-#include "nsITimer.h"
-#include "nsJSUtils.h"
+#include "nsCOMPtr.h"
 #include "nsString.h"
-#include "nsTArray.h"
 #include "nsWeakReference.h"
-#include "prtime.h"
+#include "WorkerFeature.h"
 
 #define NS_PROGRESS_EVENT_INTERVAL 50
 
+class nsITimer;
+class nsIEventTarget;
+
 namespace mozilla {
 namespace dom {
 
 class Blob;
+class DOMError;
+
+namespace workers {
+class WorkerPrivate;
+}
 
 extern const uint64_t kUnknownSize;
 
+class FileReaderDecreaseBusyCounter;
+
 class FileReader final : public DOMEventTargetHelper,
                          public nsIInterfaceRequestor,
                          public nsSupportsWeakReference,
                          public nsIInputStreamCallback,
-                         public nsITimerCallback
+                         public nsITimerCallback,
+                         public workers::WorkerFeature
 {
+  friend class FileReaderDecreaseBusyCounter;
+
 public:
-  explicit FileReader(nsPIDOMWindow* aWindow);
+  FileReader(nsPIDOMWindow* aWindow,
+             workers::WorkerPrivate* aWorkerPrivate);
 
   NS_DECL_ISUPPORTS_INHERITED
 
   NS_DECL_NSITIMERCALLBACK
   NS_DECL_NSIINPUTSTREAMCALLBACK
   NS_DECL_NSIINTERFACEREQUESTOR
 
-  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(FileReader, DOMEventTargetHelper)
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(FileReader,
+                                                         DOMEventTargetHelper)
 
-  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+  virtual JSObject* WrapObject(JSContext* aCx,
+                               JS::Handle<JSObject*> aGivenProto) override;
 
   // WebIDL
   static already_AddRefed<FileReader>
   Constructor(const GlobalObject& aGlobal, ErrorResult& aRv);
   void ReadAsArrayBuffer(JSContext* aCx, Blob& aBlob, ErrorResult& aRv)
   {
     ReadFileContent(aBlob, EmptyString(), FILE_AS_ARRAYBUFFER, aRv);
   }
@@ -91,16 +101,19 @@ public:
   IMPL_EVENT_HANDLER(error)
   IMPL_EVENT_HANDLER(loadend)
 
   void ReadAsBinaryString(Blob& aBlob, ErrorResult& aRv)
   {
     ReadFileContent(aBlob, EmptyString(), FILE_AS_BINARY, aRv);
   }
 
+  // WorkerFeature
+  bool Notify(JSContext* aCx, workers::Status) override;
+
 private:
   virtual ~FileReader();
 
   // This must be in sync with dom/webidl/FileReader.webidl
   enum eReadyState {
     EMPTY = 0,
     LOADING = 1,
     DONE = 2
@@ -126,28 +139,34 @@ private:
 
   nsresult OnLoadEnd(nsresult aStatus);
 
   void StartProgressEventTimer();
   void ClearProgressEventTimer();
   void DispatchError(nsresult rv, nsAString& finalEvent);
   nsresult DispatchProgressEvent(const nsAString& aType);
 
-  nsresult DoReadData(nsIAsyncInputStream* aStream, uint64_t aCount);
+  nsresult DoAsyncWait();
+  nsresult DoReadData(uint64_t aCount);
 
   nsresult DoOnLoadEnd(nsresult aStatus, nsAString& aSuccessEvent,
                        nsAString& aTerminationEvent);
 
   void FreeFileData()
   {
     free(mFileData);
     mFileData = nullptr;
     mDataLen = 0;
   }
 
+  nsresult IncreaseBusyCounter();
+  void DecreaseBusyCounter();
+
+  void Shutdown();
+
   char *mFileData;
   RefPtr<Blob> mBlob;
   nsCString mCharset;
   uint32_t mDataLen;
 
   eDataFormat mDataFormat;
 
   nsString mResult;
@@ -161,14 +180,21 @@ private:
   nsCOMPtr<nsIAsyncInputStream> mAsyncStream;
 
   RefPtr<DOMError> mError;
 
   eReadyState mReadyState;
 
   uint64_t mTotal;
   uint64_t mTransferred;
+
+  nsCOMPtr<nsIEventTarget> mTarget;
+
+  uint64_t mBusyCount;
+
+  // Kept alive with a WorkerFeature.
+  workers::WorkerPrivate* mWorkerPrivate;
 };
 
 } // dom namespace
 } // mozilla namespace
 
 #endif // mozilla_dom_FileReader_h
--- a/dom/webidl/FileReader.webidl
+++ b/dom/webidl/FileReader.webidl
@@ -6,17 +6,17 @@
  * The origin of this IDL file is
  * http://dev.w3.org/2006/webapi/FileAPI/#dfn-filereader
  *
  * Copyright © 2013 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. W3C
  * liability, trademark and document use rules apply.
  */
 
 [Constructor,
- Exposed=(Window,System)]
+ Exposed=(Window,Worker,System)]
 interface FileReader : EventTarget {
   // async read methods
   [Throws]
   void readAsArrayBuffer(Blob blob);
   [Throws]
   void readAsText(Blob blob, optional DOMString label = "");
   [Throws]
   void readAsDataURL(Blob blob);
copy from dom/base/test/fileapi_chromeScript.js
copy to dom/workers/test/fileapi_chromeScript.js
--- a/dom/workers/test/mochitest.ini
+++ b/dom/workers/test/mochitest.ini
@@ -114,16 +114,18 @@ support-files =
   sharedworker_performance_user_timing.js
   referrer.sjs
   performance_observer.html
   sharedWorker_ports.js
   sharedWorker_lifetime.js
   worker_referrer.js
   websocket_https.html
   websocket_https_worker.js
+  worker_fileReader.js
+  fileapi_chromeScript.js
 
 [test_404.html]
 [test_atob.html]
 [test_blobConstructor.html]
 [test_blobWorkers.html]
 [test_bug949946.html]
 [test_bug978260.html]
 [test_bug998474.html]
@@ -231,8 +233,9 @@ skip-if = buildapp == 'b2g'
 [test_xhr_system.html]
 skip-if = buildapp == 'b2g'
 [test_xhr_timeout.html]
 skip-if = (os == "win") || (os == "mac") || toolkit == 'android' #bug 798220
 [test_xhrAbort.html]
 [test_referrer.html]
 [test_sharedWorker_ports.html]
 [test_sharedWorker_lifetime.html]
+[test_fileReader.html]
--- a/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
+++ b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
@@ -110,16 +110,18 @@ var interfaceNamesInGlobalScope =
     "ExtendableEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ExtendableMessageEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "FetchEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "File",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "FileReader",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "FileReaderSync",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "FormData",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Headers",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "IDBCursor",
 // IMPORTANT: Do not change this list without review from a DOM peer!
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/test_fileReader.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for FileReader in workers</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+<script type="text/javascript;version=1.7">
+
+const minFileSize = 20000;
+SimpleTest.waitForExplicitFinish();
+
+// Create strings containing data we'll test with. We'll want long
+// strings to ensure they span multiple buffers while loading
+var testTextData = "asd b\tlah\u1234w\u00a0r";
+while (testTextData.length < minFileSize) {
+  testTextData = testTextData + testTextData;
+}
+
+var testASCIIData = "abcdef 123456\n";
+while (testASCIIData.length < minFileSize) {
+  testASCIIData = testASCIIData + testASCIIData;
+}
+
+var testBinaryData = "";
+for (var i = 0; i < 256; i++) {
+  testBinaryData += String.fromCharCode(i);
+}
+while (testBinaryData.length < minFileSize) {
+  testBinaryData = testBinaryData + testBinaryData;
+}
+
+var dataurldata0 = testBinaryData.substr(0, testBinaryData.length -
+					 testBinaryData.length % 3);
+var dataurldata1 = testBinaryData.substr(0, testBinaryData.length - 2 -
+					 testBinaryData.length % 3);
+var dataurldata2 = testBinaryData.substr(0, testBinaryData.length - 1 -
+					 testBinaryData.length % 3);
+
+
+//Set up files for testing
+var openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js");
+var opener = SpecialPowers.loadChromeScript(openerURL);
+opener.addMessageListener("files.opened", onFilesOpened);
+opener.sendAsyncMessage("files.open", [
+  testASCIIData,
+  testBinaryData,
+  null,
+  convertToUTF8(testTextData),
+  convertToUTF16(testTextData),
+  "",
+  dataurldata0,
+  dataurldata1,
+  dataurldata2,
+]);
+
+function onFilesOpened(message) {
+  var worker = new Worker('worker_fileReader.js');
+  worker.postMessage({ blobs: message,
+                       testTextData: testTextData,
+                       testASCIIData: testASCIIData,
+                       testBinaryData: testBinaryData,
+                       dataurldata0: dataurldata0,
+                       dataurldata1: dataurldata1,
+                       dataurldata2: dataurldata2 });
+
+  worker.onmessage = function(e) {
+    var msg = e.data;
+    if (msg.type == 'finish') {
+      SimpleTest.finish();
+      return;
+    }
+
+    if (msg.type == 'check') {
+      ok(msg.status, msg.msg);
+      return;
+    }
+
+    ok(false, "Unknown message.");
+  }
+}
+
+function convertToUTF16(s) {
+  res = "";
+  for (var i = 0; i < s.length; ++i) {
+    c = s.charCodeAt(i);
+    res += String.fromCharCode(c & 255, c >>> 8);
+  }
+  return res;
+}
+
+function convertToUTF8(s) {
+  return unescape(encodeURIComponent(s));
+}
+
+</script>
+</body>
+</html>
--- a/dom/workers/test/test_worker_interfaces.js
+++ b/dom/workers/test/test_worker_interfaces.js
@@ -102,16 +102,18 @@ var interfaceNamesInGlobalScope =
     "DOMStringList",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Event",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "EventTarget",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "File",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "FileReader",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "FileReaderSync",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "FormData",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Headers",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "IDBCursor",
 // IMPORTANT: Do not change this list without review from a DOM peer!
new file mode 100644
--- /dev/null
+++ b/dom/workers/test/worker_fileReader.js
@@ -0,0 +1,417 @@
+var testRanCounter = 0;
+var expectedTestCount = 0;
+var testSetupFinished = false;
+
+function ok(a, msg) {
+  postMessage({type: 'check', status: !!a, msg: msg });
+}
+
+function is(a, b, msg) {
+  ok(a === b, msg);
+}
+
+function finish() {
+  postMessage({type: 'finish'});
+}
+
+function convertToUTF16(s) {
+  res = "";
+  for (var i = 0; i < s.length; ++i) {
+    c = s.charCodeAt(i);
+    res += String.fromCharCode(c & 255, c >>> 8);
+  }
+  return res;
+}
+
+function convertToUTF8(s) {
+  return unescape(encodeURIComponent(s));
+}
+
+function convertToDataURL(s) {
+  return "data:application/octet-stream;base64," + btoa(s);
+}
+
+onmessage = function(message) {
+  is(FileReader.EMPTY, 0, "correct EMPTY value");
+  is(FileReader.LOADING, 1, "correct LOADING value");
+  is(FileReader.DONE, 2, "correct DONE value");
+
+  // List of blobs.
+  var asciiFile = message.data.blobs.shift();
+  var binaryFile = message.data.blobs.shift();
+  var nonExistingFile = message.data.blobs.shift();
+  var utf8TextFile = message.data.blobs.shift();
+  var utf16TextFile = message.data.blobs.shift();
+  var emptyFile = message.data.blobs.shift();
+  var dataUrlFile0 = message.data.blobs.shift();
+  var dataUrlFile1 = message.data.blobs.shift();
+  var dataUrlFile2 = message.data.blobs.shift();
+
+  // List of buffers for testing.
+  var testTextData = message.data.testTextData;
+  var testASCIIData = message.data.testASCIIData;
+  var testBinaryData = message.data.testBinaryData;
+  var dataurldata0 = message.data.dataurldata0;
+  var dataurldata1 = message.data.dataurldata1;
+  var dataurldata2 = message.data.dataurldata2;
+
+  // Test that plain reading works and fires events as expected, both
+  // for text and binary reading
+
+  var onloadHasRunText = false;
+  var onloadStartHasRunText = false;
+  r = new FileReader();
+  is(r.readyState, FileReader.EMPTY, "correct initial text readyState");
+  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "plain reading");
+  r.addEventListener("load", function() { onloadHasRunText = true }, false);
+  r.addEventListener("loadstart", function() { onloadStartHasRunText = true }, false);
+  r.readAsText(asciiFile);
+  is(r.readyState, FileReader.LOADING, "correct loading text readyState");
+  is(onloadHasRunText, false, "text loading must be async");
+  is(onloadStartHasRunText, true, "text loadstart should fire sync");
+  expectedTestCount++;
+
+  var onloadHasRunBinary = false;
+  var onloadStartHasRunBinary = false;
+  r = new FileReader();
+  is(r.readyState, FileReader.EMPTY, "correct initial binary readyState");
+  r.addEventListener("load", function() { onloadHasRunBinary = true }, false);
+  r.addEventListener("loadstart", function() { onloadStartHasRunBinary = true }, false);
+  r.readAsBinaryString(binaryFile);
+  r.onload = getLoadHandler(testBinaryData, testBinaryData.length, "binary reading");
+  is(r.readyState, FileReader.LOADING, "correct loading binary readyState");
+  is(onloadHasRunBinary, false, "binary loading must be async");
+  is(onloadStartHasRunBinary, true, "binary loadstart should fire sync");
+  expectedTestCount++;
+
+  var onloadHasRunArrayBuffer = false;
+  var onloadStartHasRunArrayBuffer = false;
+  r = new FileReader();
+  is(r.readyState, FileReader.EMPTY, "correct initial arrayBuffer readyState");
+  r.addEventListener("load", function() { onloadHasRunArrayBuffer = true }, false);
+  r.addEventListener("loadstart", function() { onloadStartHasRunArrayBuffer = true }, false);
+  r.readAsArrayBuffer(binaryFile);
+  r.onload = getLoadHandlerForArrayBuffer(testBinaryData, testBinaryData.length, "array buffer reading");
+  is(r.readyState, FileReader.LOADING, "correct loading arrayBuffer readyState");
+  is(onloadHasRunArrayBuffer, false, "arrayBuffer loading must be async");
+  is(onloadStartHasRunArrayBuffer, true, "arrayBuffer loadstart should fire sync");
+  expectedTestCount++;
+
+  // Test a variety of encodings, and make sure they work properly
+  r = new FileReader();
+  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "no encoding reading");
+  r.readAsText(asciiFile, "");
+  expectedTestCount++;
+
+  r = new FileReader();
+  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "iso8859 reading");
+  r.readAsText(asciiFile, "iso-8859-1");
+  expectedTestCount++;
+
+  r = new FileReader();
+  r.onload = getLoadHandler(testTextData,
+                            convertToUTF8(testTextData).length,
+                            "utf8 reading");
+  r.readAsText(utf8TextFile, "utf8");
+  expectedTestCount++;
+
+  r = new FileReader();
+  r.readAsText(utf16TextFile, "utf-16");
+  r.onload = getLoadHandler(testTextData,
+                            convertToUTF16(testTextData).length,
+                            "utf16 reading");
+  expectedTestCount++;
+
+  // Test get result without reading
+  r = new FileReader();
+  is(r.readyState, FileReader.EMPTY,
+     "readyState in test reader get result without reading");
+  is(r.error, null,
+     "no error in test reader get result without reading");
+  is(r.result, null,
+     "result in test reader get result without reading");
+
+  // Test loading an empty file works (and doesn't crash!)
+  r = new FileReader();
+  r.onload = getLoadHandler("", 0, "empty no encoding reading");
+  r.readAsText(emptyFile, "");
+  expectedTestCount++;
+
+  r = new FileReader();
+  r.onload = getLoadHandler("", 0, "empty utf8 reading");
+  r.readAsText(emptyFile, "utf8");
+  expectedTestCount++;
+
+  r = new FileReader();
+  r.onload = getLoadHandler("", 0, "empty utf16 reading");
+  r.readAsText(emptyFile, "utf-16");
+  expectedTestCount++;
+
+  r = new FileReader();
+  r.onload = getLoadHandler("", 0, "empty binary string reading");
+  r.readAsBinaryString(emptyFile);
+  expectedTestCount++;
+
+  r = new FileReader();
+  r.onload = getLoadHandlerForArrayBuffer("", 0, "empty array buffer reading");
+  r.readAsArrayBuffer(emptyFile);
+  expectedTestCount++;
+
+  r = new FileReader();
+  r.onload = getLoadHandler(convertToDataURL(""), 0, "empt binary string reading");
+  r.readAsDataURL(emptyFile);
+  expectedTestCount++;
+
+  // Test reusing a FileReader to read multiple times
+  r = new FileReader();
+  r.onload = getLoadHandler(testASCIIData,
+                            testASCIIData.length,
+                            "to-be-reused reading text")
+  var makeAnotherReadListener = function(event) {
+    r = event.target;
+    r.removeEventListener("load", makeAnotherReadListener, false);
+    r.onload = getLoadHandler(testASCIIData,
+                              testASCIIData.length,
+                              "reused reading text");
+    r.readAsText(asciiFile);
+  };
+  r.addEventListener("load", makeAnotherReadListener, false);
+  r.readAsText(asciiFile);
+  expectedTestCount += 2;
+
+  r = new FileReader();
+  r.onload = getLoadHandler(testBinaryData,
+                            testBinaryData.length,
+                            "to-be-reused reading binary")
+  var makeAnotherReadListener2 = function(event) {
+    r = event.target;
+    r.removeEventListener("load", makeAnotherReadListener2, false);
+    r.onload = getLoadHandler(testBinaryData,
+                              testBinaryData.length,
+                              "reused reading binary");
+    r.readAsBinaryString(binaryFile);
+  };
+  r.addEventListener("load", makeAnotherReadListener2, false);
+  r.readAsBinaryString(binaryFile);
+  expectedTestCount += 2;
+
+  r = new FileReader();
+  r.onload = getLoadHandler(convertToDataURL(testBinaryData),
+                            testBinaryData.length,
+                            "to-be-reused reading data url")
+  var makeAnotherReadListener3 = function(event) {
+    r = event.target;
+    r.removeEventListener("load", makeAnotherReadListener3, false);
+    r.onload = getLoadHandler(convertToDataURL(testBinaryData),
+                              testBinaryData.length,
+                              "reused reading data url");
+    r.readAsDataURL(binaryFile);
+  };
+  r.addEventListener("load", makeAnotherReadListener3, false);
+  r.readAsDataURL(binaryFile);
+  expectedTestCount += 2;
+
+  r = new FileReader();
+  r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
+                                          testBinaryData.length,
+                                          "to-be-reused reading arrayBuffer")
+  var makeAnotherReadListener4 = function(event) {
+    r = event.target;
+    r.removeEventListener("load", makeAnotherReadListener4, false);
+    r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
+                                            testBinaryData.length,
+                                            "reused reading arrayBuffer");
+    r.readAsArrayBuffer(binaryFile);
+  };
+  r.addEventListener("load", makeAnotherReadListener4, false);
+  r.readAsArrayBuffer(binaryFile);
+  expectedTestCount += 2;
+
+  // Test first reading as ArrayBuffer then read as something else
+  // (BinaryString) and doesn't crash
+  r = new FileReader();
+  r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
+                                          testBinaryData.length,
+                                          "to-be-reused reading arrayBuffer")
+  var makeAnotherReadListener5 = function(event) {
+    r = event.target;
+    r.removeEventListener("load", makeAnotherReadListener5, false);
+    r.onload = getLoadHandler(testBinaryData,
+                              testBinaryData.length,
+                              "reused reading binary string");
+    r.readAsBinaryString(binaryFile);
+  };
+  r.addEventListener("load", makeAnotherReadListener5, false);
+  r.readAsArrayBuffer(binaryFile);
+  expectedTestCount += 2;
+
+  //Test data-URI encoding on differing file sizes
+  is(dataurldata0.length % 3, 0, "Want to test data with length % 3 == 0");
+  r = new FileReader();
+  r.onload = getLoadHandler(convertToDataURL(dataurldata0),
+                            dataurldata0.length,
+                            "dataurl reading, %3 = 0");
+  r.readAsDataURL(dataUrlFile0);
+  expectedTestCount++;
+
+  is(dataurldata1.length % 3, 1, "Want to test data with length % 3 == 1");
+  r = new FileReader();
+  r.onload = getLoadHandler(convertToDataURL(dataurldata1),
+                            dataurldata1.length,
+                            "dataurl reading, %3 = 1");
+  r.readAsDataURL(dataUrlFile1);
+  expectedTestCount++;
+
+  is(dataurldata2.length % 3, 2, "Want to test data with length % 3 == 2");
+  r = new FileReader();
+  r.onload = getLoadHandler(convertToDataURL(dataurldata2),
+                            dataurldata2.length,
+                            "dataurl reading, %3 = 2");
+  r.readAsDataURL(dataUrlFile2),
+  expectedTestCount++;
+
+
+  // Test abort()
+  var abortHasRun = false;
+  var loadEndHasRun = false;
+  r = new FileReader();
+  r.onabort = function (event) {
+    is(abortHasRun, false, "abort should only fire once");
+    is(loadEndHasRun, false, "loadend shouldn't have fired yet");
+    abortHasRun = true;
+    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
+    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
+    is(event.target.result, null, "file data should be null on aborted reads");
+  }
+  r.onloadend = function (event) {
+    is(abortHasRun, true, "abort should fire before loadend");
+    is(loadEndHasRun, false, "loadend should only fire once");
+    loadEndHasRun = true;
+    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
+    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
+    is(event.target.result, null, "file data should be null on aborted reads");
+  }
+  r.onload = function() { ok(false, "load should not fire for aborted reads") };
+  r.onerror = function() { ok(false, "error should not fire for aborted reads") };
+  r.onprogress = function() { ok(false, "progress should not fire for aborted reads") };
+  var abortThrew = false;
+  try {
+    r.abort();
+  } catch(e) {
+    abortThrew = true;
+  }
+  is(abortThrew, true, "abort() must throw if not loading");
+  is(abortHasRun, false, "abort() is a no-op unless loading");
+  r.readAsText(asciiFile);
+  r.abort();
+  is(abortHasRun, true, "abort should fire sync");
+  is(loadEndHasRun, true, "loadend should fire sync");
+
+  // Test calling readAsX to cause abort()
+  var reuseAbortHasRun = false;
+  r = new FileReader();
+  r.onabort = function (event) {
+    is(reuseAbortHasRun, false, "abort should only fire once");
+    reuseAbortHasRun = true;
+    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
+    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
+    is(event.target.result, null, "file data should be null on aborted reads");
+  }
+  r.onload = function() { ok(false, "load should not fire for aborted reads") };
+  var abortThrew = false;
+  try {
+    r.abort();
+  } catch(e) {
+    abortThrew = true;
+  }
+  is(abortThrew, true, "abort() must throw if not loading");
+  is(reuseAbortHasRun, false, "abort() is a no-op unless loading");
+  r.readAsText(asciiFile);
+  r.readAsText(asciiFile);
+  is(reuseAbortHasRun, true, "abort should fire sync");
+  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "reuse-as-abort reading");
+  expectedTestCount++;
+
+
+  // Test reading from nonexistent files
+  r = new FileReader();
+  var didThrow = false;
+  r.onerror = function (event) {
+    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onerror");
+    is(event.target.error.name, "NotFoundError", "error set to NotFoundError for nonexistent files");
+    is(event.target.result, null, "file data should be null on aborted reads");
+    testHasRun();
+  };
+  r.onload = function (event) {
+    is(false, "nonexistent file shouldn't load! (FIXME: bug 1122788)");
+    testHasRun();
+  };
+  try {
+    r.readAsDataURL(nonExistingFile);
+    expectedTestCount++;
+  } catch(ex) {
+    didThrow = true;
+  }
+  // Once this test passes, we should test that onerror gets called and
+  // that the FileReader object is in the right state during that call.
+  is(didThrow, false, "shouldn't throw when opening nonexistent file, should fire error instead");
+
+
+  function getLoadHandler(expectedResult, expectedLength, testName) {
+    return function (event) {
+      is(event.target.readyState, FileReader.DONE,
+         "readyState in test " + testName);
+      is(event.target.error, null,
+         "no error in test " + testName);
+      is(event.target.result, expectedResult,
+         "result in test " + testName);
+      is(event.lengthComputable, true,
+         "lengthComputable in test " + testName);
+      is(event.loaded, expectedLength,
+         "loaded in test " + testName);
+      is(event.total, expectedLength,
+         "total in test " + testName);
+      testHasRun();
+    }
+  }
+
+  function getLoadHandlerForArrayBuffer(expectedResult, expectedLength, testName) {
+    return function (event) {
+      is(event.target.readyState, FileReader.DONE,
+         "readyState in test " + testName);
+      is(event.target.error, null,
+         "no error in test " +  testName);
+      is(event.lengthComputable, true,
+         "lengthComputable in test " + testName);
+      is(event.loaded, expectedLength,
+         "loaded in test " + testName);
+      is(event.total, expectedLength,
+         "total in test " + testName);
+      is(event.target.result.byteLength, expectedLength,
+         "array buffer size in test " + testName);
+      var u8v = new Uint8Array(event.target.result);
+      is(String.fromCharCode.apply(String, u8v), expectedResult,
+         "array buffer contents in test " + testName);
+      u8v = null;
+      is(event.target.result.byteLength, expectedLength,
+         "array buffer size after gc in test " + testName);
+      u8v = new Uint8Array(event.target.result);
+      is(String.fromCharCode.apply(String, u8v), expectedResult,
+         "array buffer contents after gc in test " + testName);
+      testHasRun();
+    }
+  }
+
+  function testHasRun() {
+    //alert(testRanCounter);
+    ++testRanCounter;
+    if (testRanCounter == expectedTestCount) {
+      is(testSetupFinished, true, "test setup should have finished; check for exceptions");
+      is(onloadHasRunText, true, "onload text should have fired by now");
+      is(onloadHasRunBinary, true, "onload binary should have fired by now");
+      finish();
+    }
+  }
+
+  testSetupFinished = true;
+}
--- a/testing/web-platform/meta/FileAPI/idlharness.worker.js.ini
+++ b/testing/web-platform/meta/FileAPI/idlharness.worker.js.ini
@@ -36,141 +36,8 @@
   [FileList interface: existence and properties of interface prototype object's "constructor" property]
     expected: FAIL
 
   [FileList interface: operation item(unsigned long)]
     expected: FAIL
 
   [FileList interface: attribute length]
     expected: FAIL
-
-  [FileReader interface: existence and properties of interface object]
-    expected: FAIL
-
-  [FileReader interface object length]
-    expected: FAIL
-
-  [FileReader interface: existence and properties of interface prototype object]
-    expected: FAIL
-
-  [FileReader interface: existence and properties of interface prototype object's "constructor" property]
-    expected: FAIL
-
-  [FileReader interface: operation readAsArrayBuffer(Blob)]
-    expected: FAIL
-
-  [FileReader interface: operation readAsText(Blob,DOMString)]
-    expected: FAIL
-
-  [FileReader interface: operation readAsDataURL(Blob)]
-    expected: FAIL
-
-  [FileReader interface: operation abort()]
-    expected: FAIL
-
-  [FileReader interface: constant EMPTY on interface object]
-    expected: FAIL
-
-  [FileReader interface: constant EMPTY on interface prototype object]
-    expected: FAIL
-
-  [FileReader interface: constant LOADING on interface object]
-    expected: FAIL
-
-  [FileReader interface: constant LOADING on interface prototype object]
-    expected: FAIL
-
-  [FileReader interface: constant DONE on interface object]
-    expected: FAIL
-
-  [FileReader interface: constant DONE on interface prototype object]
-    expected: FAIL
-
-  [FileReader interface: attribute readyState]
-    expected: FAIL
-
-  [FileReader interface: attribute result]
-    expected: FAIL
-
-  [FileReader interface: attribute error]
-    expected: FAIL
-
-  [FileReader interface: attribute onloadstart]
-    expected: FAIL
-
-  [FileReader interface: attribute onprogress]
-    expected: FAIL
-
-  [FileReader interface: attribute onload]
-    expected: FAIL
-
-  [FileReader interface: attribute onabort]
-    expected: FAIL
-
-  [FileReader interface: attribute onerror]
-    expected: FAIL
-
-  [FileReader interface: attribute onloadend]
-    expected: FAIL
-
-  [FileReader must be primary interface of new FileReader()]
-    expected: FAIL
-
-  [Stringification of new FileReader()]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "readAsArrayBuffer" with the proper type (0)]
-    expected: FAIL
-
-  [FileReader interface: calling readAsArrayBuffer(Blob) on new FileReader() with too few arguments must throw TypeError]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "readAsText" with the proper type (1)]
-    expected: FAIL
-
-  [FileReader interface: calling readAsText(Blob,DOMString) on new FileReader() with too few arguments must throw TypeError]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "readAsDataURL" with the proper type (2)]
-    expected: FAIL
-
-  [FileReader interface: calling readAsDataURL(Blob) on new FileReader() with too few arguments must throw TypeError]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "abort" with the proper type (3)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "EMPTY" with the proper type (4)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "LOADING" with the proper type (5)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "DONE" with the proper type (6)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "readyState" with the proper type (7)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "result" with the proper type (8)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "error" with the proper type (9)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "onloadstart" with the proper type (10)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "onprogress" with the proper type (11)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "onload" with the proper type (12)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "onabort" with the proper type (13)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "onerror" with the proper type (14)]
-    expected: FAIL
-
-  [FileReader interface: new FileReader() must inherit property "onloadend" with the proper type (15)]
-    expected: FAIL
-
--- a/xpcom/io/nsStreamUtils.cpp
+++ b/xpcom/io/nsStreamUtils.cpp
@@ -7,31 +7,34 @@
 #include "mozilla/Mutex.h"
 #include "mozilla/Attributes.h"
 #include "nsStreamUtils.h"
 #include "nsAutoPtr.h"
 #include "nsCOMPtr.h"
 #include "nsIPipe.h"
 #include "nsICloneableInputStream.h"
 #include "nsIEventTarget.h"
-#include "nsIRunnable.h"
+#include "nsICancelableRunnable.h"
 #include "nsISafeOutputStream.h"
 #include "nsString.h"
 #include "nsIAsyncInputStream.h"
 #include "nsIAsyncOutputStream.h"
 #include "nsIBufferedStreams.h"
 #include "nsNetCID.h"
 #include "nsServiceManagerUtils.h"
 
 using namespace mozilla;
 
 //-----------------------------------------------------------------------------
 
+// This is a nsICancelableRunnable because we can dispatch it to Workers and
+// those can be shut down at any time, and in these cases, Cancel() is called
+// instead of Run().
 class nsInputStreamReadyEvent final
-  : public nsIRunnable
+  : public nsICancelableRunnable
   , public nsIInputStreamCallback
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
 
   nsInputStreamReadyEvent(nsIInputStreamCallback* aCallback,
                           nsIEventTarget* aTarget)
     : mCallback(aCallback)
@@ -90,29 +93,38 @@ public:
       if (mStream) {
         mCallback->OnInputStreamReady(mStream);
       }
       mCallback = nullptr;
     }
     return NS_OK;
   }
 
+  NS_IMETHOD Cancel() override
+  {
+    mCallback = nullptr;
+    return NS_OK;
+  }
+
 private:
   nsCOMPtr<nsIAsyncInputStream>    mStream;
   nsCOMPtr<nsIInputStreamCallback> mCallback;
   nsCOMPtr<nsIEventTarget>         mTarget;
 };
 
-NS_IMPL_ISUPPORTS(nsInputStreamReadyEvent, nsIRunnable,
-                  nsIInputStreamCallback)
+NS_IMPL_ISUPPORTS(nsInputStreamReadyEvent, nsICancelableRunnable,
+                  nsIRunnable, nsIInputStreamCallback)
 
 //-----------------------------------------------------------------------------
 
+// This is a nsICancelableRunnable because we can dispatch it to Workers and
+// those can be shut down at any time, and in these cases, Cancel() is called
+// instead of Run().
 class nsOutputStreamReadyEvent final
-  : public nsIRunnable
+  : public nsICancelableRunnable
   , public nsIOutputStreamCallback
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
 
   nsOutputStreamReadyEvent(nsIOutputStreamCallback* aCallback,
                            nsIEventTarget* aTarget)
     : mCallback(aCallback)
@@ -171,24 +183,30 @@ public:
       if (mStream) {
         mCallback->OnOutputStreamReady(mStream);
       }
       mCallback = nullptr;
     }
     return NS_OK;
   }
 
+  NS_IMETHOD Cancel() override
+  {
+    mCallback = nullptr;
+    return NS_OK;
+  }
+
 private:
   nsCOMPtr<nsIAsyncOutputStream>    mStream;
   nsCOMPtr<nsIOutputStreamCallback> mCallback;
   nsCOMPtr<nsIEventTarget>          mTarget;
 };
 
-NS_IMPL_ISUPPORTS(nsOutputStreamReadyEvent, nsIRunnable,
-                  nsIOutputStreamCallback)
+NS_IMPL_ISUPPORTS(nsOutputStreamReadyEvent, nsICancelableRunnable,
+                  nsIRunnable, nsIOutputStreamCallback)
 
 //-----------------------------------------------------------------------------
 
 already_AddRefed<nsIInputStreamCallback>
 NS_NewInputStreamReadyEvent(nsIInputStreamCallback* aCallback,
                             nsIEventTarget* aTarget)
 {
   NS_ASSERTION(aCallback, "null callback");
@@ -211,17 +229,17 @@ NS_NewOutputStreamReadyEvent(nsIOutputSt
 
 //-----------------------------------------------------------------------------
 // NS_AsyncCopy implementation
 
 // abstract stream copier...
 class nsAStreamCopier
   : public nsIInputStreamCallback
   , public nsIOutputStreamCallback
-  , public nsIRunnable
+  , public nsICancelableRunnable
 {
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
 
   nsAStreamCopier()
     : mLock("nsAStreamCopier.mLock")
     , mCallback(nullptr)
     , mProgressCallback(nullptr)
@@ -428,16 +446,18 @@ public:
     if (mEventIsPending) {
       mEventIsPending = false;
       PostContinuationEvent_Locked();
     }
 
     return NS_OK;
   }
 
+  NS_IMETHOD Cancel() MOZ_MUST_OVERRIDE override = 0;
+
   nsresult PostContinuationEvent()
   {
     // we cannot post a continuation event if there is currently
     // an event in process.  doing so could result in Process being
     // run simultaneously on multiple threads, so we mark the event
     // as pending, and if an event is already in process then we
     // just let that existing event take care of posting the real
     // continuation event.
@@ -484,16 +504,17 @@ protected:
   virtual ~nsAStreamCopier()
   {
   }
 };
 
 NS_IMPL_ISUPPORTS(nsAStreamCopier,
                   nsIInputStreamCallback,
                   nsIOutputStreamCallback,
+                  nsICancelableRunnable,
                   nsIRunnable)
 
 class nsStreamCopierIB final : public nsAStreamCopier
 {
 public:
   nsStreamCopierIB() : nsAStreamCopier()
   {
   }
@@ -522,28 +543,34 @@ public:
       state->mSinkCondition = rv;
     } else if (*aCountWritten == 0) {
       state->mSinkCondition = NS_BASE_STREAM_CLOSED;
     }
 
     return state->mSinkCondition;
   }
 
-  uint32_t DoCopy(nsresult* aSourceCondition, nsresult* aSinkCondition)
+  uint32_t DoCopy(nsresult* aSourceCondition,
+                  nsresult* aSinkCondition) override
   {
     ReadSegmentsState state;
     state.mSink = mSink;
     state.mSinkCondition = NS_OK;
 
     uint32_t n;
     *aSourceCondition =
       mSource->ReadSegments(ConsumeInputBuffer, &state, mChunkSize, &n);
     *aSinkCondition = state.mSinkCondition;
     return n;
   }
+
+  NS_IMETHOD Cancel() override
+  {
+    return NS_OK;
+  }
 };
 
 class nsStreamCopierOB final : public nsAStreamCopier
 {
 public:
   nsStreamCopierOB() : nsAStreamCopier()
   {
   }
@@ -572,28 +599,34 @@ public:
       state->mSourceCondition = rv;
     } else if (*aCountRead == 0) {
       state->mSourceCondition = NS_BASE_STREAM_CLOSED;
     }
 
     return state->mSourceCondition;
   }
 
-  uint32_t DoCopy(nsresult* aSourceCondition, nsresult* aSinkCondition)
+  uint32_t DoCopy(nsresult* aSourceCondition,
+                  nsresult* aSinkCondition) override
   {
     WriteSegmentsState state;
     state.mSource = mSource;
     state.mSourceCondition = NS_OK;
 
     uint32_t n;
     *aSinkCondition =
       mSink->WriteSegments(FillOutputBuffer, &state, mChunkSize, &n);
     *aSourceCondition = state.mSourceCondition;
     return n;
   }
+
+  NS_IMETHOD Cancel() override
+  {
+    return NS_OK;
+  }
 };
 
 //-----------------------------------------------------------------------------
 
 nsresult
 NS_AsyncCopy(nsIInputStream*         aSource,
              nsIOutputStream*        aSink,
              nsIEventTarget*         aTarget,
--- a/xpcom/threads/TimerThread.cpp
+++ b/xpcom/threads/TimerThread.cpp
@@ -122,20 +122,33 @@ public:
   }
 
   void* Alloc(size_t aSize);
   void Free(void* aPtr);
 };
 
 } // namespace
 
-class nsTimerEvent : public nsRunnable
+// This is a nsICancelableRunnable because we can dispatch it to Workers and
+// those can be shut down at any time, and in these cases, Cancel() is called
+// instead of Run().
+class nsTimerEvent : public nsCancelableRunnable
 {
 public:
-  NS_IMETHOD Run();
+  NS_IMETHOD Run() override;
+
+  NS_IMETHOD Cancel() override
+  {
+    // Since nsTimerImpl is not thread-safe, we should release |mTimer|
+    // here in the target thread to avoid race condition. Otherwise,
+    // ~nsTimerEvent() which calls nsTimerImpl::Release() could run in the
+    // timer thread and result in race condition.
+    mTimer = nullptr;
+    return NS_OK;
+  }
 
   nsTimerEvent()
     : mTimer()
     , mGeneration(0)
   {
     MOZ_COUNT_CTOR(nsTimerEvent);
 
     // Note: We override operator new for this class, and the override is
@@ -248,35 +261,34 @@ nsTimerEvent::DeleteAllocatorIfNeeded()
     delete sAllocator;
     sAllocator = nullptr;
   }
 }
 
 NS_IMETHODIMP
 nsTimerEvent::Run()
 {
+  MOZ_ASSERT(mTimer);
+
   if (mGeneration != mTimer->GetGeneration()) {
     return NS_OK;
   }
 
   if (MOZ_LOG_TEST(GetTimerLog(), LogLevel::Debug)) {
     TimeStamp now = TimeStamp::Now();
     MOZ_LOG(GetTimerLog(), LogLevel::Debug,
            ("[this=%p] time between PostTimerEvent() and Fire(): %fms\n",
             this, (now - mInitTime).ToMilliseconds()));
   }
 
   mTimer->Fire();
-  // Since nsTimerImpl is not thread-safe, we should release |mTimer|
-  // here in the target thread to avoid race condition. Otherwise,
-  // ~nsTimerEvent() which calls nsTimerImpl::Release() could run in the
-  // timer thread and result in race condition.
-  mTimer = nullptr;
 
-  return NS_OK;
+  // We call Cancel() to correctly release mTimer.
+  // Read more in the Cancel() implementation.
+  return Cancel();
 }
 
 nsresult
 TimerThread::Init()
 {
   MOZ_LOG(GetTimerLog(), LogLevel::Debug,
          ("TimerThread::Init [%d]\n", mInitialized));
 
--- a/xpcom/threads/nsICancelableRunnable.idl
+++ b/xpcom/threads/nsICancelableRunnable.idl
@@ -11,14 +11,15 @@
  */
 
 [scriptable, uuid(de93dc4c-5eea-4eb7-b6d1-dbf1e0cef65c)]
 interface nsICancelableRunnable : nsIRunnable
 {
     /**
      * Cancels a pending task.  If the task has already been executed this will
      * be a no-op.  Calling this method twice is considered an error.
+     * If cancel() is called, run() will not be called.
      *
      * @throws NS_ERROR_UNEXPECTED
      *   Indicates that the runnable has already been canceled.
      */
     void cancel();
 };