Bug 901097 - FileReader API in workers, r=sicking, r=nfroyd
☠☠ backed out by 15880ddc54b9 ☠ ☠
authorAndrea Marchesini <amarchesini@mozilla.com>
Thu, 17 Dec 2015 12:00:35 +0000
changeset 316164 30839ee209e8c215ad700ac15d2c75162da9df08
parent 316163 565d7ae436baa8b3f3fd679bcc9aec74d76533a8
child 316165 15880ddc54b9258e4dfce412ec5806ed53f69c85
push id8516
push userjlund@mozilla.com
push dateFri, 18 Dec 2015 00:52:34 +0000
reviewerssicking, nfroyd
bugs901097
milestone46.0a1
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()
 {
+  Shutdown();
   FreeFileData();
-  mResultArrayBuffer = nullptr;
-  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()
+{
+  mResultArrayBuffer = nullptr;
+  DropJSObjects(this);
+
+  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,36 @@ 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;
+    if (mFileData) {
+      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 +182,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();
 };