Bug 497003 - Support delivery of OnDataAvailable off the main thread r=bz r=jduell
authorSteve Workman <sworkman@mozilla.com>
Thu, 13 Jun 2013 10:42:48 -0700
changeset 142091 f70770fc6dcea68dc75bf2c652e0dbbbdccd42ca
parent 142090 75575b5f073b0107a39db50386ecf4f20a547434
child 142092 841ed173ba2b5ef36e68850f5c60a3943e5dac7c
push id3911
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 20:17:26 +0000
treeherdermozilla-aurora@7e26ca8db92b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, jduell
bugs497003
milestone24.0a1
Bug 497003 - Support delivery of OnDataAvailable off the main thread r=bz r=jduell
netwerk/base/public/moz.build
netwerk/base/public/nsIInputStreamPump.idl
netwerk/base/public/nsIThreadRetargetableRequest.idl
netwerk/base/public/nsIThreadRetargetableStreamListener.idl
netwerk/base/public/nsStreamListenerWrapper.h
netwerk/base/src/nsInputStreamPump.cpp
netwerk/base/src/nsInputStreamPump.h
netwerk/base/src/nsStreamListenerTee.cpp
netwerk/base/src/nsStreamListenerTee.h
netwerk/base/src/nsStreamListenerWrapper.cpp
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpChannel.h
netwerk/test/httpserver/httpd.js
netwerk/test/mochitests/Makefile.in
netwerk/test/mochitests/moz.build
netwerk/test/mochitests/partial_content.sjs
netwerk/test/mochitests/test_partially_cached_content.html
netwerk/test/moz.build
--- a/netwerk/base/public/moz.build
+++ b/netwerk/base/public/moz.build
@@ -90,16 +90,18 @@ XPIDL_SOURCES += [
     'nsIStandardURL.idl',
     'nsIStreamListener.idl',
     'nsIStreamListenerTee.idl',
     'nsIStreamLoader.idl',
     'nsIStreamTransportService.idl',
     'nsIStrictTransportSecurityService.idl',
     'nsISyncStreamListener.idl',
     'nsISystemProxySettings.idl',
+    'nsIThreadRetargetableRequest.idl',
+    'nsIThreadRetargetableStreamListener.idl',
     'nsITimedChannel.idl',
     'nsITraceableChannel.idl',
     'nsITransport.idl',
     'nsIUDPServerSocket.idl',
     'nsIURI.idl',
     'nsIURIChecker.idl',
     'nsIURIClassifier.idl',
     'nsIURIWithPrincipal.idl',
--- a/netwerk/base/public/nsIInputStreamPump.idl
+++ b/netwerk/base/public/nsIInputStreamPump.idl
@@ -6,20 +6,21 @@
 
 interface nsIInputStream;
 interface nsIStreamListener;
 
 /**
  * nsIInputStreamPump
  *
  * This interface provides a means to configure and use a input stream pump
- * instance.  The input stream pump will asynchronously read from a input
- * stream, and push data to a nsIStreamListener instance.  It utilizes the
+ * instance.  The input stream pump will asynchronously read from an input
+ * stream, and push data to an nsIStreamListener instance.  It utilizes the
  * current thread's nsIEventTarget in order to make reading from the stream
- * asynchronous.
+ * asynchronous. A different thread can be used if the pump also implements
+ * nsIThreadRetargetableRequest.
  *
  * If the given stream supports nsIAsyncInputStream, then the stream pump will
  * call the stream's AsyncWait method to drive the stream listener.  Otherwise,
  * the stream will be read on a background thread utilizing the stream
  * transport service.  More details are provided below.
  */
 [scriptable, uuid(400F5468-97E7-4d2b-9C65-A82AECC7AE82)]
 interface nsIInputStreamPump : nsIRequest
new file mode 100644
--- /dev/null
+++ b/netwerk/base/public/nsIThreadRetargetableRequest.idl
@@ -0,0 +1,34 @@
+/* -*- 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 "nsISupports.idl"
+#include "nsIEventTarget.idl"
+
+/**
+ * nsIThreadRetargetableRequest
+ *
+ * Should be implemented by requests that support retargeting delivery of
+ * OnDataAvailable and OnStopRequest off the main thread.
+ */
+[uuid(27b84c48-5a73-4ba4-a8a4-8b5e649a145e)]
+interface nsIThreadRetargetableRequest : nsISupports
+{
+  /**
+   * Called to retarget delivery of OnDataAvailable and OnStopRequest to
+   * another thread. Should only be called within the context of OnStartRequest
+   * on the main thread.
+   *
+   * @param aNewTarget New event target, e.g. thread or threadpool.
+   *
+   * Note: no return value is given. If the retargeting cannot be handled,
+   * normal delivery to the main thread will continue. As such, listeners
+   * should be ready to deal with OnDataAvailable and OnStopRequest on
+   * either the main thread or the new target thread.
+   */
+  void retargetDeliveryTo(in nsIEventTarget aNewTarget);
+};
+
+
new file mode 100644
--- /dev/null
+++ b/netwerk/base/public/nsIThreadRetargetableStreamListener.idl
@@ -0,0 +1,34 @@
+/* -*- 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 "nsISupports.idl"
+
+/**
+ * nsIThreadRetargetableListener
+ *
+ * To be used by classes which implement nsIStreamListener and whose
+ * OnDataAvailable and OnStopRequest may be retargeted for delivery off the
+ * main thread.
+ */
+[uuid(fb2304b8-f82f-4433-af68-d874a2ebbdc1)]
+interface nsIThreadRetargetableStreamListener : nsISupports
+{
+  /**
+   * Checks this listener and any next listeners it may have to verify that
+   * they can receive OnDataAvailable and OnStopRequest off the main thread.
+   * It is the responsibility of the implementing class to decide on the
+   * criteria to determine if retargeted delivery of these methods is
+   * possible, but it must check any and all nsIStreamListener objects that
+   * might be called in the listener chain.
+   *
+   * An exception should be thrown if a listener in the chain does not
+   * support retargeted delivery, i.e. if the next listener does not implement
+   * nsIThreadRetargetableStreamListener, or a call to its checkListenerChain()
+   * fails.
+   */
+  void checkListenerChain();
+};
+
--- a/netwerk/base/public/nsStreamListenerWrapper.h
+++ b/netwerk/base/public/nsStreamListenerWrapper.h
@@ -3,32 +3,35 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef nsStreamListenerWrapper_h__
 #define nsStreamListenerWrapper_h__
 
 #include "nsCOMPtr.h"
 #include "nsIStreamListener.h"
 #include "nsIRequestObserver.h"
+#include "nsIThreadRetargetableStreamListener.h"
 #include "mozilla/Attributes.h"
 
 // Wrapper class to make replacement of nsHttpChannel's listener
 // from JavaScript possible. It is workaround for bug 433711 and 682305.
 class nsStreamListenerWrapper MOZ_FINAL : public nsIStreamListener
+                                        , public nsIThreadRetargetableStreamListener
 {
 public:
   nsStreamListenerWrapper(nsIStreamListener *listener)
     : mListener(listener)
   {
     NS_ASSERTION(mListener, "no stream listener specified");
   }
 
   NS_DECL_ISUPPORTS
   NS_FORWARD_NSIREQUESTOBSERVER(mListener->)
   NS_FORWARD_NSISTREAMLISTENER(mListener->)
+  NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
 
 private:
   ~nsStreamListenerWrapper() {}
   nsCOMPtr<nsIStreamListener> mListener;
 };
 
 #endif // nsStreamListenerWrapper_h__
 
--- a/netwerk/base/src/nsInputStreamPump.cpp
+++ b/netwerk/base/src/nsInputStreamPump.cpp
@@ -6,20 +6,23 @@
 
 #include "nsIOService.h"
 #include "nsInputStreamPump.h"
 #include "nsIServiceManager.h"
 #include "nsIStreamTransportService.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsISeekableStream.h"
 #include "nsITransport.h"
+#include "nsIThreadRetargetableStreamListener.h"
+#include "nsStreamUtils.h"
 #include "nsNetUtil.h"
 #include "nsThreadUtils.h"
 #include "nsCOMPtr.h"
 #include "prlog.h"
+#include "nsPrintfCString.h"
 #include "GeckoProfiler.h"
 #include <algorithm>
 
 static NS_DEFINE_CID(kStreamTransportServiceCID, NS_STREAMTRANSPORTSERVICE_CID);
 
 #if defined(PR_LOGGING)
 //
 // NSPR_LOG_MODULES=nsStreamPump:5
@@ -36,16 +39,17 @@ nsInputStreamPump::nsInputStreamPump()
     : mState(STATE_IDLE)
     , mStreamOffset(0)
     , mStreamLength(UINT64_MAX)
     , mStatus(NS_OK)
     , mSuspendCount(0)
     , mLoadFlags(LOAD_NORMAL)
     , mWaiting(false)
     , mCloseWhenDone(false)
+    , mRetargeting(false)
 {
 #if defined(PR_LOGGING)
     if (!gStreamPumpLog)
         gStreamPumpLog = PR_NewLogModule("nsStreamPump");
 #endif
 }
 
 nsInputStreamPump::~nsInputStreamPump()
@@ -114,38 +118,43 @@ nsInputStreamPump::PeekStream(PeekSegmen
                                     nsIOService::gDefaultSegmentSize,
                                     &dummy);
 }
 
 nsresult
 nsInputStreamPump::EnsureWaiting()
 {
     // no need to worry about multiple threads... an input stream pump lives
-    // on only one thread.
-
+    // on only one thread at a time.
+    MOZ_ASSERT(mAsyncStream);
     if (!mWaiting) {
+        MOZ_ASSERT(mTargetThread);
         nsresult rv = mAsyncStream->AsyncWait(this, 0, 0, mTargetThread);
         if (NS_FAILED(rv)) {
             NS_ERROR("AsyncWait failed");
             return rv;
         }
+        // Any retargeting during STATE_START or START_TRANSFER is complete
+        // after the call to AsyncWait; next callback wil be on mTargetThread.
+        mRetargeting = false;
         mWaiting = true;
     }
     return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
 // nsInputStreamPump::nsISupports
 //-----------------------------------------------------------------------------
 
 // although this class can only be accessed from one thread at a time, we do
 // allow its ownership to move from thread to thread, assuming the consumer
 // understands the limitations of this.
-NS_IMPL_THREADSAFE_ISUPPORTS3(nsInputStreamPump,
+NS_IMPL_THREADSAFE_ISUPPORTS4(nsInputStreamPump,
                               nsIRequest,
+                              nsIThreadRetargetableRequest,
                               nsIInputStreamCallback,
                               nsIInputStreamPump)
 
 //-----------------------------------------------------------------------------
 // nsInputStreamPump::nsIRequest
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
@@ -375,25 +384,40 @@ nsInputStreamPump::OnInputStreamReady(ns
             nextState = OnStateStop();
             break;
         default:
             nextState = 0;
             NS_NOTREACHED("Unknown enum value.");
             return NS_ERROR_UNEXPECTED;
         }
 
-        if (mState == nextState && !mSuspendCount) {
-            NS_ASSERTION(mState == STATE_TRANSFER, "unexpected state");
-            NS_ASSERTION(NS_SUCCEEDED(mStatus), "unexpected status");
+        bool stillTransferring = (mState == STATE_TRANSFER &&
+                                  nextState == STATE_TRANSFER);
+        if (stillTransferring) {
+            NS_ASSERTION(NS_SUCCEEDED(mStatus),
+                         "Should not have failed status for ongoing transfer");
+        } else {
+            NS_ASSERTION(mState != nextState,
+                         "Only OnStateTransfer can be called more than once.");
+        }
+        if (mRetargeting) {
+            NS_ASSERTION(mState != STATE_STOP,
+                         "Retargeting should not happen during OnStateStop.");
+        }
 
+        // Wait asynchronously if there is still data to transfer, or if
+        // delivery of data has been requested on another thread.
+        if (!mSuspendCount && (stillTransferring || mRetargeting)) {
+            mState = nextState;
             mWaiting = false;
             mStatus = EnsureWaiting();
             if (NS_SUCCEEDED(mStatus))
                 break;
             
+            // Failure to start asynchronous wait: stop transfer.
             nextState = STATE_STOP;
         }
 
         mState = nextState;
     }
     return NS_OK;
 }
 
@@ -546,18 +570,53 @@ nsInputStreamPump::OnStateStop()
     if (NS_FAILED(mStatus))
         mAsyncStream->CloseWithStatus(mStatus);
     else if (mCloseWhenDone)
         mAsyncStream->Close();
 
     mAsyncStream = 0;
     mTargetThread = 0;
     mIsPending = false;
+    mRetargeting = false;
 
     mListener->OnStopRequest(this, mListenerContext, mStatus);
     mListener = 0;
     mListenerContext = 0;
 
     if (mLoadGroup)
         mLoadGroup->RemoveRequest(this, nullptr, mStatus);
 
     return STATE_IDLE;
 }
+
+//-----------------------------------------------------------------------------
+// nsIThreadRetargetableRequest
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsInputStreamPump::RetargetDeliveryTo(nsIEventTarget* aNewTarget)
+{
+    NS_ENSURE_ARG(aNewTarget);
+    if (aNewTarget == mTargetThread) {
+        NS_WARNING("Retargeting delivery to same thread");
+        return NS_OK;
+    }
+    NS_ENSURE_TRUE(mState == STATE_START || mState == STATE_TRANSFER,
+                   NS_ERROR_UNEXPECTED);
+
+    // Ensure that |mListener| and any subsequent listeners can be retargeted
+    // to another thread.
+    nsresult rv = NS_OK;
+    nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener =
+        do_QueryInterface(mListener, &rv);
+    if (NS_SUCCEEDED(rv) && retargetableListener) {
+        rv = retargetableListener->CheckListenerChain();
+        if (NS_SUCCEEDED(rv)) {
+            mTargetThread = aNewTarget;
+            mRetargeting = true;
+        }
+    }
+    LOG(("nsInputStreamPump::RetargetDeliveryTo [this=%x aNewTarget=%p] "
+         "%s listener [%p] rv[%x]",
+         this, aNewTarget, (mTargetThread == aNewTarget ? "success" : "failure"),
+         (nsIStreamListener*)mListener, rv));
+    return rv;
+}
--- a/netwerk/base/src/nsInputStreamPump.h
+++ b/netwerk/base/src/nsInputStreamPump.h
@@ -10,27 +10,30 @@
 #include "nsIInputStream.h"
 #include "nsIURI.h"
 #include "nsILoadGroup.h"
 #include "nsIStreamListener.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIProgressEventSink.h"
 #include "nsIAsyncInputStream.h"
 #include "nsIThread.h"
+#include "nsIThreadRetargetableRequest.h"
 #include "nsCOMPtr.h"
 #include "mozilla/Attributes.h"
 
 class nsInputStreamPump MOZ_FINAL : public nsIInputStreamPump
                                   , public nsIInputStreamCallback
+                                  , public nsIThreadRetargetableRequest
 {
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIREQUEST
     NS_DECL_NSIINPUTSTREAMPUMP
     NS_DECL_NSIINPUTSTREAMCALLBACK
+    NS_DECL_NSITHREADRETARGETABLEREQUEST
 
     nsInputStreamPump(); 
     ~nsInputStreamPump();
 
     static NS_HIDDEN_(nsresult)
                       Create(nsInputStreamPump  **result,
                              nsIInputStream      *stream,
                              int64_t              streamPos = -1,
@@ -66,24 +69,25 @@ protected:
     uint32_t OnStateStart();
     uint32_t OnStateTransfer();
     uint32_t OnStateStop();
 
     uint32_t                      mState;
     nsCOMPtr<nsILoadGroup>        mLoadGroup;
     nsCOMPtr<nsIStreamListener>   mListener;
     nsCOMPtr<nsISupports>         mListenerContext;
-    nsCOMPtr<nsIThread>           mTargetThread;
+    nsCOMPtr<nsIEventTarget>      mTargetThread;
     nsCOMPtr<nsIInputStream>      mStream;
     nsCOMPtr<nsIAsyncInputStream> mAsyncStream;
     uint64_t                      mStreamOffset;
     uint64_t                      mStreamLength;
     uint32_t                      mSegSize;
     uint32_t                      mSegCount;
     nsresult                      mStatus;
     uint32_t                      mSuspendCount;
     uint32_t                      mLoadFlags;
     bool                          mIsPending;
     bool                          mWaiting; // true if waiting on async source
     bool                          mCloseWhenDone;
+    bool                          mRetargeting;
 };
 
 #endif // !nsInputStreamChannel_h__
--- a/netwerk/base/src/nsStreamListenerTee.cpp
+++ b/netwerk/base/src/nsStreamListenerTee.cpp
@@ -1,19 +1,20 @@
 /* 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 "nsStreamListenerTee.h"
 #include "nsProxyRelease.h"
 
-NS_IMPL_ISUPPORTS3(nsStreamListenerTee,
-                   nsIStreamListener,
-                   nsIRequestObserver,
-                   nsIStreamListenerTee)
+NS_IMPL_THREADSAFE_ISUPPORTS4(nsStreamListenerTee,
+                              nsIStreamListener,
+                              nsIRequestObserver,
+                              nsIStreamListenerTee,
+                              nsIThreadRetargetableStreamListener)
 
 NS_IMETHODIMP
 nsStreamListenerTee::OnStartRequest(nsIRequest *request,
                                     nsISupports *context)
 {
     NS_ENSURE_TRUE(mListener, NS_ERROR_NOT_INITIALIZED);
     nsresult rv1 = mListener->OnStartRequest(request, context);
     nsresult rv2 = NS_OK;
@@ -88,16 +89,29 @@ nsStreamListenerTee::OnDataAvailable(nsI
         tee = do_QueryInterface(mInputTee, &rv);
         if (NS_FAILED(rv)) return rv;
     }
 
     return mListener->OnDataAvailable(request, context, tee, offset, count);
 }
 
 NS_IMETHODIMP
+nsStreamListenerTee::CheckListenerChain()
+{
+    NS_ASSERTION(NS_IsMainThread(), "Should be on main thread!");
+    nsresult rv = NS_OK;
+    nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener =
+        do_QueryInterface(mListener, &rv);
+    if (retargetableListener) {
+        rv = retargetableListener->CheckListenerChain();
+    }
+    return rv;
+}
+
+NS_IMETHODIMP
 nsStreamListenerTee::Init(nsIStreamListener *listener,
                           nsIOutputStream *sink,
                           nsIRequestObserver *requestObserver)
 {
     NS_ENSURE_ARG_POINTER(listener);
     NS_ENSURE_ARG_POINTER(sink);
     mListener = listener;
     mSink = sink;
--- a/netwerk/base/src/nsStreamListenerTee.h
+++ b/netwerk/base/src/nsStreamListenerTee.h
@@ -1,27 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef nsStreamListenerTee_h__
 #define nsStreamListenerTee_h__
 
 #include "nsIStreamListenerTee.h"
+#include "nsIThreadRetargetableStreamListener.h"
 #include "nsIInputStreamTee.h"
 #include "nsIOutputStream.h"
 #include "nsCOMPtr.h"
 #include "nsIEventTarget.h"
 
 class nsStreamListenerTee : public nsIStreamListenerTee
+                          , public nsIThreadRetargetableStreamListener
 {
 public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIREQUESTOBSERVER
     NS_DECL_NSISTREAMLISTENER
+    NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
     NS_DECL_NSISTREAMLISTENERTEE
 
     nsStreamListenerTee() { }
     virtual ~nsStreamListenerTee() { }
 
 private:
     nsCOMPtr<nsIInputStreamTee>  mInputTee;
     nsCOMPtr<nsIOutputStream>    mSink;
--- a/netwerk/base/src/nsStreamListenerWrapper.cpp
+++ b/netwerk/base/src/nsStreamListenerWrapper.cpp
@@ -1,10 +1,24 @@
 /* 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 "nsStreamListenerWrapper.h"
+#include "nsThreadUtils.h"
 
-NS_IMPL_ISUPPORTS2(nsStreamListenerWrapper,
+NS_IMPL_ISUPPORTS3(nsStreamListenerWrapper,
                    nsIStreamListener,
-                   nsIRequestObserver)
+                   nsIRequestObserver,
+                   nsIThreadRetargetableStreamListener)
 
+NS_IMETHODIMP
+nsStreamListenerWrapper::CheckListenerChain()
+{
+    NS_ASSERTION(NS_IsMainThread(), "Should be on main thread!");
+    nsresult rv = NS_OK;
+    nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener =
+        do_QueryInterface(mListener, &rv);
+    if (retargetableListener) {
+        rv = retargetableListener->CheckListenerChain();
+    }
+    return rv;
+}
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -24,16 +24,17 @@
 #include "nsStreamUtils.h"
 #include "nsIOService.h"
 #include "nsICacheService.h"
 #include "nsDNSPrefetch.h"
 #include "nsChannelClassifier.h"
 #include "nsIRedirectResultListener.h"
 #include "mozilla/TimeStamp.h"
 #include "nsError.h"
+#include "nsPrintfCString.h"
 #include "nsAlgorithm.h"
 #include "GeckoProfiler.h"
 #include "nsIConsoleService.h"
 #include "base/compiler_specific.h"
 #include "NullHttpTransaction.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/VisualEventTracer.h"
 
@@ -4247,16 +4248,18 @@ NS_INTERFACE_MAP_BEGIN(nsHttpChannel)
     NS_INTERFACE_MAP_ENTRY(nsISupportsPriority)
     NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback)
     NS_INTERFACE_MAP_ENTRY(nsIProxiedChannel)
     NS_INTERFACE_MAP_ENTRY(nsIHttpAuthenticableChannel)
     NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheContainer)
     NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheChannel)
     NS_INTERFACE_MAP_ENTRY(nsIAsyncVerifyRedirectCallback)
     NS_INTERFACE_MAP_ENTRY(nsITimedChannel)
+    NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableRequest)
+    NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener)
 NS_INTERFACE_MAP_END_INHERITING(HttpBaseChannel)
 
 //-----------------------------------------------------------------------------
 // nsHttpChannel::nsIRequest
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsHttpChannel::Cancel(nsresult status)
@@ -4938,16 +4941,52 @@ nsresult
 nsHttpChannel::ContinueOnStartRequest3(nsresult result)
 {
     if (mFallingBack)
         return NS_OK;
 
     return CallOnStartRequest();
 }
 
+class OnStopRequestCleanupEvent : public nsRunnable
+{
+public:
+    OnStopRequestCleanupEvent(nsHttpChannel *aHttpChannel,
+                              nsresult aStatus)
+    : mHttpChannel(aHttpChannel)
+    , mStatus(aStatus)
+    {
+        MOZ_ASSERT(!NS_IsMainThread(), "Shouldn't be created on main thread");
+        NS_ASSERTION(aHttpChannel, "aHttpChannel should not be null");
+    }
+
+    NS_IMETHOD Run()
+    {
+        MOZ_ASSERT(NS_IsMainThread(), "Should run on main thread");
+        if (mHttpChannel) {
+            mHttpChannel->OnStopRequestCleanup(mStatus);
+        }
+        return NS_OK;
+    }
+private:
+    nsRefPtr<nsHttpChannel> mHttpChannel;
+    nsresult mStatus;
+};
+
+nsresult
+nsHttpChannel::OnStopRequestCleanup(nsresult aStatus)
+{
+    NS_ASSERTION(NS_IsMainThread(), "Should be on main thread");
+    if (mLoadGroup) {
+        mLoadGroup->RemoveRequest(this, nullptr, aStatus);
+    }
+    ReleaseListeners();
+    return NS_OK;
+}
+
 NS_IMETHODIMP
 nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt, nsresult status)
 {
     PROFILER_LABEL("network", "nsHttpChannel::OnStopRequest");
     LOG(("nsHttpChannel::OnStopRequest [this=%p request=%p status=%x]\n",
         this, request, status));
 
     if (mTimingEnabled && request == mCachePump) {
@@ -5067,31 +5106,65 @@ nsHttpChannel::OnStopRequest(nsIRequest 
 
     MOZ_EVENT_TRACER_DONE(this, "net::http::channel");
 
     CloseCacheEntry(!contentComplete);
 
     if (mOfflineCacheEntry)
         CloseOfflineCacheEntry();
 
-    if (mLoadGroup)
-        mLoadGroup->RemoveRequest(this, nullptr, status);
+    if (NS_IsMainThread()) {
+        OnStopRequestCleanup(status);
+    } else {
+        nsresult rv = NS_DispatchToMainThread(
+            new OnStopRequestCleanupEvent(this, status));
+        NS_ENSURE_SUCCESS(rv, rv);
+    }
 
     // We don't need this info anymore
     CleanRedirectCacheChainIfNecessary();
 
-    ReleaseListeners();
-
     return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
 // nsHttpChannel::nsIStreamListener
 //-----------------------------------------------------------------------------
 
+class OnTransportStatusAsyncEvent : public nsRunnable
+{
+public:
+    OnTransportStatusAsyncEvent(nsITransportEventSink* aEventSink,
+                                nsresult aTransportStatus,
+                                uint64_t aProgress,
+                                uint64_t aProgressMax)
+    : mEventSink(aEventSink)
+    , mTransportStatus(aTransportStatus)
+    , mProgress(aProgress)
+    , mProgressMax(aProgressMax)
+    {
+        MOZ_ASSERT(!NS_IsMainThread(), "Shouldn't be created on main thread");
+    }
+
+    NS_IMETHOD Run()
+    {
+        MOZ_ASSERT(NS_IsMainThread(), "Should run on main thread");
+        if (mEventSink) {
+            mEventSink->OnTransportStatus(nullptr, mTransportStatus,
+                                          mProgress, mProgressMax);
+        }
+        return NS_OK;
+    }
+private:
+    nsCOMPtr<nsITransportEventSink> mEventSink;
+    nsresult mTransportStatus;
+    uint64_t mProgress;
+    uint64_t mProgressMax;
+};
+
 NS_IMETHODIMP
 nsHttpChannel::OnDataAvailable(nsIRequest *request, nsISupports *ctxt,
                                nsIInputStream *input,
                                uint64_t offset, uint32_t count)
 {
     PROFILER_LABEL("network", "nsHttpChannel::OnDataAvailable");
     LOG(("nsHttpChannel::OnDataAvailable [this=%p request=%p offset=%llu count=%u]\n",
         this, request, offset, count));
@@ -5126,17 +5199,24 @@ nsHttpChannel::OnDataAvailable(nsIReques
         // holds our best estimate of the total content length.  Even in the case
         // of a byte range request, the content length stored in the cached
         // response headers is what we want to use here.
 
         uint64_t progressMax(uint64_t(mResponseHead->ContentLength()));
         uint64_t progress = mLogicalOffset + uint64_t(count);
         MOZ_ASSERT(progress <= progressMax, "unexpected progress values");
 
-        OnTransportStatus(nullptr, transportStatus, progress, progressMax);
+        if (NS_IsMainThread()) {
+            OnTransportStatus(nullptr, transportStatus, progress, progressMax);
+        } else {
+            nsresult rv = NS_DispatchToMainThread(
+                new OnTransportStatusAsyncEvent(this, transportStatus,
+                                                progress, progressMax));
+            NS_ENSURE_SUCCESS(rv, rv);
+        }
 
         //
         // we have to manually keep the logical offset of the stream up-to-date.
         // we cannot depend solely on the offset provided, since we may have
         // already streamed some data from another source (see, for example,
         // OnDoneReadingPartialCacheEntry).
         //
         if (!mLogicalOffset)
@@ -5151,16 +5231,81 @@ nsHttpChannel::OnDataAvailable(nsIReques
             mLogicalOffset = progress;
         return rv;
     }
 
     return NS_ERROR_ABORT;
 }
 
 //-----------------------------------------------------------------------------
+// nsHttpChannel::nsIThreadRetargetableRequest
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsHttpChannel::RetargetDeliveryTo(nsIEventTarget* aNewTarget)
+{
+    MOZ_ASSERT(NS_IsMainThread(), "Should be called on main thread only");
+
+    NS_ENSURE_ARG(aNewTarget);
+    if (aNewTarget == NS_GetCurrentThread()) {
+        NS_WARNING("Retargeting delivery to same thread");
+        return NS_OK;
+    }
+    NS_ENSURE_TRUE(mTransactionPump || mCachePump, NS_ERROR_NOT_AVAILABLE);
+
+    nsresult rv = NS_OK;
+    // If both cache pump and transaction pump exist, we're probably dealing
+    // with partially cached content. So, we must be able to retarget both.
+    nsCOMPtr<nsIThreadRetargetableRequest> retargetableCachePump;
+    nsCOMPtr<nsIThreadRetargetableRequest> retargetableTransactionPump;
+    if (mCachePump) {
+        // Direct call to QueryInterface to avoid multiple inheritance issues.
+        rv = mCachePump->QueryInterface(NS_GET_IID(nsIThreadRetargetableRequest),
+                                        getter_AddRefs(retargetableCachePump));
+        // nsInputStreamPump should implement this interface.
+        MOZ_ASSERT(retargetableCachePump);
+        rv = retargetableCachePump->RetargetDeliveryTo(aNewTarget);
+    }
+    if (NS_SUCCEEDED(rv) && mTransactionPump) {
+        // Direct call to QueryInterface to avoid multiple inheritance issues.
+        rv = mTransactionPump->QueryInterface(NS_GET_IID(nsIThreadRetargetableRequest),
+                                              getter_AddRefs(retargetableTransactionPump));
+        // nsInputStreamPump should implement this interface.
+        MOZ_ASSERT(retargetableTransactionPump);
+        rv = retargetableTransactionPump->RetargetDeliveryTo(aNewTarget);
+
+        // If retarget fails for transaction pump, we must restore mCachePump.
+        if (NS_FAILED(rv) && retargetableCachePump) {
+            nsCOMPtr<nsIThread> mainThread;
+            rv = NS_GetMainThread(getter_AddRefs(mainThread));
+            NS_ENSURE_SUCCESS(rv, rv);
+            rv = retargetableCachePump->RetargetDeliveryTo(mainThread);
+        }
+    }
+    return rv;
+}
+
+//-----------------------------------------------------------------------------
+// nsHttpChannel::nsThreadRetargetableStreamListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsHttpChannel::CheckListenerChain()
+{
+    NS_ASSERTION(NS_IsMainThread(), "Should be on main thread!");
+    nsresult rv = NS_OK;
+    nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener =
+        do_QueryInterface(mListener, &rv);
+    if (retargetableListener) {
+        rv = retargetableListener->CheckListenerChain();
+    }
+    return rv;
+}
+
+//-----------------------------------------------------------------------------
 // nsHttpChannel::nsITransportEventSink
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsHttpChannel::OnTransportStatus(nsITransport *trans, nsresult status,
                                  uint64_t progress, uint64_t progressMax)
 {
     // cache the progress sink so we don't have to query for it each time.
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -23,16 +23,18 @@
 #include "nsIResumableChannel.h"
 #include "nsIProtocolProxyCallback.h"
 #include "nsICancelable.h"
 #include "nsIHttpAuthenticableChannel.h"
 #include "nsIHttpChannelAuthProvider.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "nsITimedChannel.h"
 #include "nsIFile.h"
+#include "nsIThreadRetargetableRequest.h"
+#include "nsIThreadRetargetableStreamListener.h"
 #include "nsDNSPrefetch.h"
 #include "TimingStruct.h"
 #include "AutoClose.h"
 #include "mozilla/Telemetry.h"
 
 class nsAHttpConnection;
 
 namespace mozilla { namespace net {
@@ -49,31 +51,35 @@ class nsHttpChannel : public HttpBaseCha
                     , public nsICachingChannel
                     , public nsICacheListener
                     , public nsITransportEventSink
                     , public nsIProtocolProxyCallback
                     , public nsIHttpAuthenticableChannel
                     , public nsIApplicationCacheChannel
                     , public nsIAsyncVerifyRedirectCallback
                     , public nsITimedChannel
+                    , public nsIThreadRetargetableRequest
+                    , public nsIThreadRetargetableStreamListener
 {
 public:
     NS_DECL_ISUPPORTS_INHERITED
     NS_DECL_NSIREQUESTOBSERVER
     NS_DECL_NSISTREAMLISTENER
+    NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
     NS_DECL_NSICACHEINFOCHANNEL
     NS_DECL_NSICACHINGCHANNEL
     NS_DECL_NSICACHELISTENER
     NS_DECL_NSITRANSPORTEVENTSINK
     NS_DECL_NSIPROTOCOLPROXYCALLBACK
     NS_DECL_NSIPROXIEDCHANNEL
     NS_DECL_NSIAPPLICATIONCACHECONTAINER
     NS_DECL_NSIAPPLICATIONCACHECHANNEL
     NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK
     NS_DECL_NSITIMEDCHANNEL
+    NS_DECL_NSITHREADRETARGETABLEREQUEST
 
     // nsIHttpAuthenticableChannel. We can't use
     // NS_DECL_NSIHTTPAUTHENTICABLECHANNEL because it duplicates cancel() and
     // others.
     NS_IMETHOD GetIsSSL(bool *aIsSSL);
     NS_IMETHOD GetProxyMethodIsConnect(bool *aProxyMethodIsConnect);
     NS_IMETHOD GetServerResponseHeader(nsACString & aServerResponseHeader);
     NS_IMETHOD GetProxyChallenges(nsACString & aChallenges);
@@ -145,16 +151,18 @@ public: /* internal necko use only */
              , mCacheKey(key)
         {}
 
         nsresult MarkAsForeign();
     };
 
     OfflineCacheEntryAsForeignMarker* GetOfflineCacheEntryAsForeignMarker();
 
+    nsresult OnStopRequestCleanup(nsresult aStatus);
+
 private:
     typedef nsresult (nsHttpChannel::*nsContinueRedirectionFunc)(nsresult result);
 
     bool     RequestIsConditional();
     nsresult BeginConnect();
     nsresult Connect();
     nsresult ContinueConnect();
     void     SpeculativeConnect();
--- a/netwerk/test/httpserver/httpd.js
+++ b/netwerk/test/httpserver/httpd.js
@@ -3969,17 +3969,20 @@ Response.prototype =
     if (!this._processAsync)
     {
       dumpn("*** non-async response, set Content-Length");
 
       var bodyStream = this._bodyInputStream;
       var avail = bodyStream ? bodyStream.available() : 0;
 
       // XXX assumes stream will always report the full amount of data available
-      headers.setHeader("Content-Length", "" + avail, false);
+      // Set "Content-Length" if not already set by request handler.
+      if (!headers.hasHeader("Content-Length")) {
+        headers.setHeader("Content-Length", "" + avail, false);
+      }
     }
 
 
     // construct and send response
     dumpn("*** header post-processing completed, sending response head...");
 
     // request-line
     var preambleData = [statusLine];
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/Makefile.in
@@ -0,0 +1,21 @@
+# -*- Mode: Makefile; 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/.
+
+DEPTH		= @DEPTH@
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir	= @relativesrcdir@
+FAIL_ON_WARNINGS := 1
+
+include $(DEPTH)/config/autoconf.mk
+
+MOCHITEST_FILES = \
+  partial_content.sjs \
+  test_partially_cached_content.html \
+  $(NULL)
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+MODULE = 'test_necko'
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/partial_content.sjs
@@ -0,0 +1,133 @@
+/* -*- 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/. */
+
+// HTTP Version from request, used for response.
+var gHttpVersion;
+
+/* Debug and Error wrapper functions for dump().
+ */
+function ERR(response, responseCode, responseCodeStr, msg)
+{
+  // Dump to console log and send to client in response.
+  dump("SERVER ERROR: " + msg + "\n");
+  response.setStatusLine(gHttpVersion, responseCode, responseCodeStr);
+  response.write(msg);
+}
+
+function DBG(msg)
+{
+  // Dump to console only.
+  dump("SERVER DEBUG: " + msg + "\n");
+}
+
+/* Delivers content in parts to test partially cached content: requires two
+ * requests for the same resource.
+ *
+ * First call will respond with partial content, but a 200 header and
+ * Content-Length equal to the full content length. No Range or If-Range
+ * headers are allowed in the request.
+ *
+ * Second call will require Range and If-Range in the request headers, and
+ * will respond with the range requested.
+ */
+function handleRequest(request, response)
+{
+  // Set http version for error responses.
+  gHttpVersion = request.httpVersion;
+
+  // All responses, inc. errors, are text/html.
+  response.setHeader("Content-Type", "text/html; charset=UTF-8", false);
+
+  // Get state var to determine if this is the first or second request.
+  var expectedRequestType;
+  if (getState("expectedRequestType") === "") {
+    DBG("First call: Should be requesting full content.");
+    expectedRequestType = "fullRequest";
+    // Set state var for second request.
+    setState("expectedRequestType", "partialRequest");
+  } else if (getState("expectedRequestType") === "partialRequest") {
+    DBG("Second call: Should be requesting undelivered content.");
+    expectedRequestType = "partialRequest";
+    // Reset state var for first request.
+    setState("expectedRequestType", "");
+  } else {
+    ERR(response, 500, "Internal Server Error",
+        "Invalid expectedRequestType \"" + expectedRequestType + "\"in " +
+        "server state db.");
+    return;
+  }
+
+  // Look for Range and If-Range
+  var range = request.hasHeader("Range") ? request.getHeader("Range") : "";
+  var ifRange = request.hasHeader("If-Range") ? request.getHeader("If-Range") : "";
+
+  if (expectedRequestType === "fullRequest") {
+    // Should not have Range or If-Range in first request.
+    if (range && range.length > 0) {
+      ERR(response, 400, "Bad Request",
+          "Should not receive \"Range: " + range + "\" for first, full request.");
+      return;
+    }
+    if (ifRange && ifRange.length > 0) {
+      ERR(response, 400, "Bad Request",
+          "Should not receive \"Range: " + range + "\" for first, full request.");
+      return;
+    }
+  } else if (expectedRequestType === "partialRequest") {
+    // Range AND If-Range should both be present in second request.
+    if (!range) {
+      ERR(response, 400, "Bad Request",
+          "Should receive \"Range: \" for second, partial request.");
+      return;
+    }
+    if (!ifRange) {
+      ERR(response, 400, "Bad Request",
+          "Should receive \"If-Range: \" for second, partial request.");
+      return;
+    }
+  } else {
+    // Somewhat redundant, but a check for errors in this test code.
+    ERR(response, 500, "Internal Server Error",
+        "expectedRequestType not set correctly: \"" + expectedRequestType + "\"");
+    return;
+  }
+
+  // Prepare content in two parts for responses.
+  var partialContent = "<html><head></head><body><p id=\"firstResponse\">" +
+                       "First response</p>";
+  var remainderContent = "<p id=\"secondResponse\">Second response</p>" +
+                         "</body></html>";
+  var totalLength = partialContent.length + remainderContent.length;
+
+  DBG("totalLength: " + totalLength);
+
+  // Prepare common headers for the two responses.
+  response.setHeader("ETag", "abcd0123", false);
+  response.setHeader("Accept-Ranges", "bytes", false);
+
+  // Prepare specific headers and content for first and second responses.
+  if (expectedRequestType === "fullRequest") {
+    DBG("First response: Sending partial content with a full header");
+    response.setStatusLine(request.httpVersion, 200, "OK");
+    response.write(partialContent, partialContent.length);
+    // Set Content-Length to full length of resource.
+    response.setHeader("Content-Length", "" + totalLength, false);
+  } else if (expectedRequestType === "partialRequest") {
+    DBG("Second response: Sending remaining content with a range header");
+    response.setStatusLine(request.httpVersion, 206, "Partial Content");
+    response.setHeader("Content-Range", "bytes " + partialContent.length + "-" +
+                       (totalLength - 1) + "/" + totalLength);
+    response.write(remainderContent);
+    // Set Content-Length to length of bytes transmitted.
+    response.setHeader("Content-Length", "" + remainderContent.length, false);
+  } else {
+    // Somewhat redundant, but a check for errors in this test code.
+    ERR(response, 500, "Internal Server Error",
+       "Something very bad happened here: expectedRequestType is invalid " +
+       "towards the end of handleRequest! - \"" + expectedRequestType + "\"");
+    return;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/netwerk/test/mochitests/test_partially_cached_content.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+  https://bugzilla.mozilla.org/show_bug.cgi?id=497003
+
+  This test verifies that partially cached content is read from the cache first
+  and then from the network. It is written in the mochitest framework to take
+  thread retargeting into consideration of nsIStreamListener callbacks (inc.
+  nsIRequestObserver). E.g. HTML5 Stream Parser requesting retargeting of
+  nsIStreamListener callbacks to the parser thread.
+-->
+<head>
+  <meta charset="UTF-8">
+  <title>Test for Bug 497003: support sending OnDataAvailable() to other threads</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+  <p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=497003">Mozilla Bug 497003: support sending OnDataAvailable() to other threads</a></p>
+  <p><iframe id="contentFrame" src="partial_content.sjs"></iframe></p>
+
+<pre id="test">
+<script>
+
+
+
+/* Check that the iframe has initial content only after the first load.
+ */
+function expectInitialContent(e) {
+  info("expectInitialContent",
+       "First response received: should have partial content");
+  var frameWindow = document.getElementById('contentFrame').contentWindow;
+
+  // Expect "First response" in received HTML.
+  var firstResponse = frameWindow.document.getElementById('firstResponse');
+  is(firstResponse.innerHTML, "First response", "firstResponse");
+
+  // Expect NOT to get any second response element.
+  var secondResponse = frameWindow.document.getElementById('secondResponse');
+  ok(!secondResponse, "Should not get text for second response in first.");
+
+  // Set up listener for second load.
+  e.target.removeEventListener("load", expectInitialContent, false);
+  e.target.addEventListener("load", expectFullContent, false);
+
+  // Reload.
+  e.target.src="partial_content.sjs";
+}
+
+/* Check that the iframe has all the content after the second load.
+ */
+function expectFullContent(e)
+{
+  info("expectFullContent",
+       "Second response received: should complete content from first load");
+  var frameWindow = document.getElementById('contentFrame').contentWindow;
+
+  // Expect "First response" to still be there
+  var firstResponse = frameWindow.document.getElementById('firstResponse');
+  is(firstResponse.innerHTML, "First response", "firstResponse");
+
+  // Expect "Second response" to be there also.
+  var secondResponse = frameWindow.document.getElementById('secondResponse');
+  is(secondResponse.innerHTML, "Second response", "secondResponse");
+
+  SimpleTest.finish();
+}
+
+// Set listener for first load to expect partial content.
+document.getElementById('contentFrame')
+  .addEventListener("load", expectInitialContent, false);
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
--- a/netwerk/test/moz.build
+++ b/netwerk/test/moz.build
@@ -1,15 +1,15 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
-TEST_DIRS += ['httpserver', 'browser']
+TEST_DIRS += ['httpserver', 'browser', 'mochitests']
 
 MODULE = 'test_necko'
 
 XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
 
 # FIXME/bug 575918: out-of-process xpcshell is broken on OS X
 if CONFIG['OS_ARCH'] != 'Darwin':
     XPCSHELL_TESTS_MANIFESTS += ['unit_ipc/xpcshell.ini']