Merge last green changeset from mozilla-inbound to mozilla-central
authorMarco Bonardo <mbonardo@mozilla.com>
Thu, 25 Aug 2011 13:26:15 +0200
changeset 77183 f6b61dde6a159dc3d5ea30c387b6c95687d35400
parent 77159 c5b2cbf6e31be28d0b941fb12117e960a603870b (current diff)
parent 77182 c72e3869695026552cad35f63523fd34e41d387d (diff)
child 77188 e136897709967e43e35ce4678db19a6204ee94ec
push id78
push userclegnitto@mozilla.com
push dateFri, 16 Dec 2011 17:32:24 +0000
treeherdermozilla-release@79d24e644fdd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone9.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge last green changeset from mozilla-inbound to mozilla-central
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6280,24 +6280,25 @@ var IndexedDBPromptHelper = {
     }
 
     var requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);
 
     var contentWindow = requestor.getInterface(Ci.nsIDOMWindow);
     var contentDocument = contentWindow.document;
     var browserWindow =
       OfflineApps._getBrowserWindowForContentWindow(contentWindow);
+
+    if (browserWindow != window) {
+      // Must belong to some other window.
+      return;
+    }
+
     var browser =
       OfflineApps._getBrowserForContentWindow(browserWindow, contentWindow);
 
-    if (!browser) {
-      // Must belong to some other window.
-      return;
-    }
-
     var host = contentDocument.documentURIObject.asciiHost;
 
     var message;
     var responseTopic;
     if (topic == this._permissionsPrompt) {
       message = gNavigatorBundle.getFormattedString("offlineApps.available",
                                                     [ host ]);
       responseTopic = this._permissionsResponse;
--- a/content/html/content/public/nsHTMLMediaElement.h
+++ b/content/html/content/public/nsHTMLMediaElement.h
@@ -520,16 +520,26 @@ protected:
   void DispatchAsyncSourceError(nsIContent* aSourceElement);
 
   /**
    * Resets the media element for an error condition as per aErrorCode.
    * aErrorCode must be one of nsIDOMHTMLMediaError codes.
    */
   void Error(PRUint16 aErrorCode);
 
+  /**
+   * Returns the URL spec of the currentSrc.
+   **/
+  void GetCurrentSpec(nsCString& aString);
+
+  /**
+   * Process any media fragment entries in the URI
+   */
+  void ProcessMediaFragmentURI();
+
   nsRefPtr<nsMediaDecoder> mDecoder;
 
   // A reference to the ImageContainer which contains the current frame
   // of video to display.
   nsRefPtr<ImageContainer> mImageContainer;
 
   // Holds a reference to the first channel we open to the media resource.
   // Once the decoder is created, control over the channel passes to the
@@ -606,16 +616,26 @@ protected:
   // Time that the last timeupdate event was fired. Read/Write from the
   // main thread only.
   TimeStamp mTimeUpdateTime;
 
   // Media 'currentTime' value when the last timeupdate event occurred.
   // Read/Write from the main thread only.
   double mLastCurrentTime;
 
+  // Logical start time of the media resource in seconds as obtained
+  // from any media fragments. A negative value indicates that no
+  // fragment time has been set. Read/Write from the main thread only.
+  double mFragmentStart;
+
+  // Logical end time of the media resource in seconds as obtained
+  // from any media fragments. A negative value indicates that no
+  // fragment time has been set. Read/Write from the main thread only.
+  double mFragmentEnd;
+
   nsRefPtr<gfxASurface> mPrintSurface;
 
   // Reference to the source element last returned by GetNextSource().
   // This is the child source element which we're trying to load from.
   nsCOMPtr<nsIContent> mSourceLoadCandidate;
 
   // An audio stream for writing audio directly from JS.
   nsRefPtr<nsAudioStream> mAudioStream;
--- a/content/html/content/src/Makefile.in
+++ b/content/html/content/src/Makefile.in
@@ -115,16 +115,17 @@ CPPSRCS		= \
 		nsDOMStringMap.cpp \
 		$(NULL)
 
 ifdef MOZ_MEDIA
 CPPSRCS		+= \
 		nsHTMLAudioElement.cpp \
 		nsHTMLMediaElement.cpp \
 		nsMediaError.cpp \
+		nsMediaFragmentURIParser.cpp \
 		nsHTMLSourceElement.cpp \
 		nsTimeRanges.cpp \
 		nsHTMLVideoElement.cpp \
 		$(NULL)
 endif
 
 # we don't want the shared lib, but we want to force the creation of a static lib.
 FORCE_STATIC_LIB = 1
--- a/content/html/content/src/nsHTMLMediaElement.cpp
+++ b/content/html/content/src/nsHTMLMediaElement.cpp
@@ -54,17 +54,16 @@
 #include "nsNetUtil.h"
 #include "prmem.h"
 #include "nsNetUtil.h"
 #include "nsXPCOMStrings.h"
 #include "prlock.h"
 #include "nsThreadUtils.h"
 #include "nsIThreadInternal.h"
 #include "nsContentUtils.h"
-
 #include "nsFrameManager.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsIXPConnect.h"
 #include "jsapi.h"
 
 #include "nsITimer.h"
 
 #include "nsEventDispatcher.h"
@@ -86,16 +85,17 @@
 #include <limits>
 #include "nsIDocShellTreeItem.h"
 #include "nsIAsyncVerifyRedirectCallback.h"
 #include "nsIAppShell.h"
 #include "nsWidgetsCID.h"
 
 #include "nsIPrivateDOMEvent.h"
 #include "nsIDOMNotifyAudioAvailableEvent.h"
+#include "nsMediaFragmentURIParser.h"
 
 #ifdef MOZ_OGG
 #include "nsOggDecoder.h"
 #endif
 #ifdef MOZ_WAVE
 #include "nsWaveDecoder.h"
 #endif
 #ifdef MOZ_WEBM
@@ -424,28 +424,18 @@ NS_IMETHODIMP nsHTMLMediaElement::GetEnd
 
   return NS_OK;
 }
 
 /* readonly attribute DOMString currentSrc; */
 NS_IMETHODIMP nsHTMLMediaElement::GetCurrentSrc(nsAString & aCurrentSrc)
 {
   nsCAutoString src;
-
-  if (mDecoder) {
-    nsMediaStream* stream = mDecoder->GetCurrentStream();
-    if (stream) {
-      stream->URI()->GetSpec(src);
-    }
-  } else if (mLoadingSrc) {
-    mLoadingSrc->GetSpec(src);
-  }
-
+  GetCurrentSpec(src);
   aCurrentSrc = NS_ConvertUTF8toUTF16(src);
-
   return NS_OK;
 }
 
 /* readonly attribute unsigned short networkState; */
 NS_IMETHODIMP nsHTMLMediaElement::GetNetworkState(PRUint16 *aNetworkState)
 {
   *aNetworkState = mNetworkState;
 
@@ -1287,16 +1277,18 @@ nsHTMLMediaElement::nsHTMLMediaElement(a
     mReadyState(nsIDOMHTMLMediaElement::HAVE_NOTHING),
     mLoadWaitStatus(NOT_WAITING),
     mVolume(1.0),
     mChannels(0),
     mRate(0),
     mPreloadAction(PRELOAD_UNDEFINED),
     mMediaSize(-1,-1),
     mLastCurrentTime(0.0),
+    mFragmentStart(-1.0),
+    mFragmentEnd(-1.0),
     mAllowAudioData(PR_FALSE),
     mBegun(PR_FALSE),
     mLoadedFirstFrame(PR_FALSE),
     mAutoplaying(PR_TRUE),
     mAutoplayEnabled(PR_TRUE),
     mPaused(PR_TRUE),
     mMuted(PR_FALSE),
     mPlayingBeforeSeek(PR_FALSE),
@@ -1947,23 +1939,50 @@ nsresult nsHTMLMediaElement::NewURIFromS
     // decode it.
     NS_RELEASE(*aURI);
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
 
   return NS_OK;
 }
 
+void nsHTMLMediaElement::ProcessMediaFragmentURI()
+{
+  nsCAutoString ref;
+  GetCurrentSpec(ref);
+  nsMediaFragmentURIParser parser(ref);
+  parser.Parse();
+  double start = parser.GetStartTime();
+  if (mDecoder) {
+    double end = parser.GetEndTime();
+    if (end < 0.0 || end > start) {
+      mFragmentEnd = end;
+    }
+    else {
+      start = -1.0;
+      end = -1.0;
+    }
+  }
+  if (start > 0.0) {
+    SetCurrentTime(start);
+    mFragmentStart = start;
+  }
+}
+
 void nsHTMLMediaElement::MetadataLoaded(PRUint32 aChannels, PRUint32 aRate)
 {
   mChannels = aChannels;
   mRate = aRate;
   ChangeReadyState(nsIDOMHTMLMediaElement::HAVE_METADATA);
   DispatchAsyncEvent(NS_LITERAL_STRING("durationchange"));
   DispatchAsyncEvent(NS_LITERAL_STRING("loadedmetadata"));
+  if (mDecoder && mDecoder->IsSeekable()) {
+    ProcessMediaFragmentURI();
+    mDecoder->SetEndTime(mFragmentEnd);
+  }
 }
 
 void nsHTMLMediaElement::FirstFrameLoaded(PRBool aResourceFullyLoaded)
 {
   ChangeReadyState(aResourceFullyLoaded ?
     nsIDOMHTMLMediaElement::HAVE_ENOUGH_DATA :
     nsIDOMHTMLMediaElement::HAVE_CURRENT_DATA);
   ChangeDelayLoadStatus(PR_FALSE);
@@ -2639,9 +2658,56 @@ void nsHTMLMediaElement::FireTimeUpdate(
   if (!aPeriodic ||
       (mLastCurrentTime != time &&
        (mTimeUpdateTime.IsNull() ||
         now - mTimeUpdateTime >= TimeDuration::FromMilliseconds(TIMEUPDATE_MS)))) {
     DispatchAsyncEvent(NS_LITERAL_STRING("timeupdate"));
     mTimeUpdateTime = now;
     mLastCurrentTime = time;
   }
+  if (mFragmentEnd >= 0.0 && time >= mFragmentEnd) {
+    Pause();
+    mFragmentEnd = -1.0;
+    mFragmentStart = -1.0;
+    mDecoder->SetEndTime(mFragmentEnd);
+  }
 }
+
+void nsHTMLMediaElement::GetCurrentSpec(nsCString& aString)
+{
+  if (mDecoder) {
+    nsMediaStream* stream = mDecoder->GetCurrentStream();
+    if (stream) {
+      stream->URI()->GetSpec(aString);
+    }
+  } else if (mLoadingSrc) {
+    mLoadingSrc->GetSpec(aString);
+  }
+}
+
+/* attribute double initialTime; */
+NS_IMETHODIMP nsHTMLMediaElement::GetInitialTime(double *aTime)
+{
+  // If there is no start fragment then the initalTime is zero.
+  // Clamp to duration if it is greater than duration.
+  double duration = 0.0;
+  nsresult rv = GetDuration(&duration);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  *aTime = mFragmentStart < 0.0 ? 0.0 : mFragmentStart;
+  if (*aTime > duration) {
+    *aTime = duration;
+  }
+  return NS_OK;
+}
+
+/* attribute double mozFragmentEnd; */
+NS_IMETHODIMP nsHTMLMediaElement::GetMozFragmentEnd(double *aTime)
+{
+  double duration = 0.0;
+  nsresult rv = GetDuration(&duration);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // If there is no end fragment, or the fragment end is greater than the
+  // duration, return the duration.
+  *aTime = (mFragmentEnd < 0.0 || mFragmentEnd > duration) ? duration : mFragmentEnd;
+  return NS_OK;
+}
new file mode 100644
--- /dev/null
+++ b/content/html/content/src/nsMediaFragmentURIParser.cpp
@@ -0,0 +1,339 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the media fragment URI parser.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Chris Double <chris.double@double.co.nz>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+#include "nsCharSeparatedTokenizer.h"
+#include "nsEscape.h"
+#include "nsMediaFragmentURIParser.h"
+
+nsMediaFragmentURIParser::nsMediaFragmentURIParser(const nsCString& aSpec)
+{
+  nsReadingIterator<char> start, end;
+  aSpec.BeginReading(start);
+  aSpec.EndReading(end);
+  if (FindCharInReadable('#', start, end)) {
+    mHash = Substring(++start, end);
+  }
+}
+
+PRBool nsMediaFragmentURIParser::ParseNPT(nsDependentSubstring& aString, double& aStart, double& aEnd)
+{
+  nsDependentSubstring original(aString);
+  if (aString.Length() > 4 &&
+      aString[0] == 'n' && aString[1] == 'p' &&
+      aString[2] == 't' && aString[3] == ':') {
+    aString.Rebind(aString, 4);
+  }
+
+  if (aString.Length() == 0) {
+    return PR_FALSE;
+  }
+
+  double start = -1.0;
+  double end = -1.0;
+
+  if (ParseNPTTime(aString, start)) {
+    aStart = start;
+  }
+
+  if (aString.Length() == 0) {
+    return PR_TRUE;
+  }
+
+  if (aString[0] != ',') {
+    aString.Rebind(original, 0);
+    return PR_FALSE;
+  }
+
+  aString.Rebind(aString, 1);
+
+  if (aString.Length() == 0) {
+    aString.Rebind(original, 0);
+    return PR_FALSE;
+  }
+
+  if (ParseNPTTime(aString, end)) {
+    aEnd = end;
+  }
+
+  if (aString.Length() != 0) {
+    aString.Rebind(original, 0);
+    return PR_FALSE;
+  }
+
+  return PR_TRUE;
+}
+
+PRBool nsMediaFragmentURIParser::ParseNPTTime(nsDependentSubstring& aString, double& aTime)
+{
+  if (aString.Length() == 0) {
+    return PR_FALSE;
+  }
+
+  return
+    ParseNPTHHMMSS(aString, aTime) ||
+    ParseNPTMMSS(aString, aTime) ||
+    ParseNPTSec(aString, aTime);
+}
+
+// Return PR_TRUE if the given character is a numeric character
+static PRBool IsDigit(nsDependentSubstring::char_type aChar)
+{
+  return (aChar >= '0' && aChar <= '9');
+}
+
+// Return the index of the first character in the string that is not
+// a numerical digit, starting from 'aStart'.
+static PRUint32 FirstNonDigit(nsDependentSubstring& aString, PRUint32 aStart)
+{
+   while (aStart < aString.Length() && IsDigit(aString[aStart])) {
+    ++aStart;
+  }
+  return aStart;
+}
+ 
+PRBool nsMediaFragmentURIParser::ParseNPTSec(nsDependentSubstring& aString, double& aSec)
+{
+  nsDependentSubstring original(aString);
+  if (aString.Length() == 0) {
+    return PR_FALSE;
+  }
+
+  PRUint32 index = FirstNonDigit(aString, 0);
+  if (index == 0) {
+    return PR_FALSE;
+  }
+
+  nsDependentSubstring n(aString, 0, index);
+  PRInt32 ec;
+  PRInt32 s = PromiseFlatString(n).ToInteger(&ec);
+  if (NS_FAILED(ec)) {
+    return PR_FALSE;
+  }
+
+  aString.Rebind(aString, index);
+  double fraction = 0.0;
+  if (!ParseNPTFraction(aString, fraction)) {
+    aString.Rebind(original, 0);
+    return PR_FALSE;
+  }
+
+  aSec = s + fraction;
+  return PR_TRUE;
+}
+
+PRBool nsMediaFragmentURIParser::ParseNPTMMSS(nsDependentSubstring& aString, double& aTime)
+{
+  nsDependentSubstring original(aString);
+  PRUint32 mm = 0;
+  PRUint32 ss = 0;
+  double fraction = 0.0;
+  if (!ParseNPTMM(aString, mm)) {
+    aString.Rebind(original, 0);
+    return PR_FALSE;
+  }
+
+  if (aString.Length() < 2 || aString[0] != ':') {
+    aString.Rebind(original, 0);
+    return PR_FALSE;
+  }
+
+  aString.Rebind(aString, 1);
+  if (!ParseNPTSS(aString, ss)) {
+    aString.Rebind(original, 0);
+    return PR_FALSE;
+  }
+
+  if (!ParseNPTFraction(aString, fraction)) {
+    aString.Rebind(original, 0);
+    return PR_FALSE;
+  }
+  aTime = mm * 60 + ss + fraction;
+  return PR_TRUE;
+}
+
+PRBool nsMediaFragmentURIParser::ParseNPTFraction(nsDependentSubstring& aString, double& aFraction)
+{
+  double fraction = 0.0;
+
+  if (aString.Length() > 0 && aString[0] == '.') {
+    PRUint32 index = FirstNonDigit(aString, 1);
+
+    if (index > 1) {
+      nsDependentSubstring number(aString, 0, index);
+      PRInt32 ec;
+      fraction = PromiseFlatString(number).ToDouble(&ec);
+      if (NS_FAILED(ec)) {
+        return PR_FALSE;
+      }
+    }
+    aString.Rebind(aString, index);
+  }
+
+  aFraction = fraction;
+  return PR_TRUE;
+}
+
+PRBool nsMediaFragmentURIParser::ParseNPTHHMMSS(nsDependentSubstring& aString, double& aTime)
+{
+  nsDependentSubstring original(aString);
+  PRUint32 hh = 0;
+  double seconds = 0.0;
+  if (!ParseNPTHH(aString, hh)) {
+    return PR_FALSE;
+  }
+
+  if (aString.Length() < 2 || aString[0] != ':') {
+    aString.Rebind(original, 0);
+    return PR_FALSE;
+  }
+
+  aString.Rebind(aString, 1);
+  if (!ParseNPTMMSS(aString, seconds)) {
+    aString.Rebind(original, 0);
+    return PR_FALSE;
+  }
+
+  aTime = hh * 3600 + seconds;
+  return PR_TRUE;
+}
+
+PRBool nsMediaFragmentURIParser::ParseNPTHH(nsDependentSubstring& aString, PRUint32& aHour)
+{
+  if (aString.Length() == 0) {
+    return PR_FALSE;
+  }
+
+  PRUint32 index = FirstNonDigit(aString, 0);
+  if (index == 0) {
+    return PR_FALSE;
+  }
+
+  nsDependentSubstring n(aString, 0, index);
+  PRInt32 ec;
+  PRInt32 u = PromiseFlatString(n).ToInteger(&ec);
+  if (NS_FAILED(ec)) {
+    return PR_FALSE;
+  }
+
+  aString.Rebind(aString, index);
+  aHour = u;
+  return PR_TRUE;
+}
+
+PRBool nsMediaFragmentURIParser::ParseNPTMM(nsDependentSubstring& aString, PRUint32& aMinute)
+{
+  return ParseNPTSS(aString, aMinute);
+}
+
+PRBool nsMediaFragmentURIParser::ParseNPTSS(nsDependentSubstring& aString, PRUint32& aSecond)
+{
+  if (aString.Length() < 2) {
+    return PR_FALSE;
+  }
+
+  if (IsDigit(aString[0]) && IsDigit(aString[1])) {
+    nsDependentSubstring n(aString, 0, 2);
+    PRInt32 ec;
+
+    PRInt32 u = PromiseFlatString(n).ToInteger(&ec);
+    if (NS_FAILED(ec)) {
+      return PR_FALSE;
+    }
+
+    aString.Rebind(aString, 2);
+    if (u >= 60)
+      return PR_FALSE;
+
+    aSecond = u;
+    return PR_TRUE;
+  }
+
+  return PR_FALSE;
+}
+
+void nsMediaFragmentURIParser::Parse()
+{
+  nsCCharSeparatedTokenizer tokenizer(mHash, '&');
+  while (tokenizer.hasMoreTokens()) {
+    const nsCSubstring& nv = tokenizer.nextToken();
+    PRInt32 index = nv.FindChar('=');
+    if (index >= 0) {
+      nsCAutoString name;
+      nsCAutoString value;
+      NS_UnescapeURL(StringHead(nv, index), esc_Ref | esc_AlwaysCopy, name);
+      NS_UnescapeURL(Substring(nv, index + 1, nv.Length()),
+                     esc_Ref | esc_AlwaysCopy, value);
+      nsAutoString a = NS_ConvertUTF8toUTF16(name);
+      nsAutoString b = NS_ConvertUTF8toUTF16(value);
+      mFragments.AppendElement(Pair(a, b));
+    }
+  }
+}
+
+double nsMediaFragmentURIParser::GetStartTime()
+{
+  for (PRUint32 i = 0; i < mFragments.Length(); ++i) {
+    PRUint32 index = mFragments.Length() - i - 1;
+    if (mFragments[index].mName.EqualsLiteral("t")) {
+      double start = -1;
+      double end = -1;
+      nsDependentSubstring s(mFragments[index].mValue, 0);
+      if (ParseNPT(s, start, end)) {
+        return start;
+      }
+    }
+  }
+  return 0.0;
+}
+
+double nsMediaFragmentURIParser::GetEndTime()
+{
+  for (PRUint32 i = 0; i < mFragments.Length(); ++i) {
+    PRUint32 index = mFragments.Length() - i - 1;
+    if (mFragments[index].mName.EqualsLiteral("t")) {
+      double start = -1;
+      double end = -1;
+      nsDependentSubstring s(mFragments[index].mValue, 0);
+      if (ParseNPT(s, start, end)) {
+        return end;
+      }
+    }
+  }
+  return -1;
+}
+
+
new file mode 100644
--- /dev/null
+++ b/content/html/content/src/nsMediaFragmentURIParser.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the media fragment URI parser.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Chris Double <chris.double@double.co.nz>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+#if !defined(nsMediaFragmentURIParser_h__)
+#define nsMediaFragmentURIParser_h__
+
+#include "nsString.h"
+#include "nsTArray.h"
+
+// Class to handle parsing of a W3C media fragment URI as per
+// spec at: http://www.w3.org/TR/media-frags/
+// Only the temporaral URI portion of the spec is implemented.
+// To use:
+// a) Construct an instance with the URI containing the fragment
+// b) Call Parse() method to parse the fragment
+// c) Use GetStartTime() and GetEndTime() to get the start/end
+//    times from any temporal fragment included in the URI.
+class nsMediaFragmentURIParser
+{
+  struct Pair
+  {
+    Pair(const nsAString& aName, const nsAString& aValue) :
+      mName(aName), mValue(aValue) { }
+
+    nsString mName;
+    nsString mValue;
+  };
+
+public:
+  // Create a parser, with the URL including fragment identifier
+  // in 'aSpec'.
+  nsMediaFragmentURIParser(const nsCString& aSpec);
+
+  // Parse the URI fragment included in the URI that was passed
+  // on construction.
+  void Parse();
+
+  // Return the start time in seconds obtained from the URI
+  // fragment. If no start time or no valid temporal fragment
+  // exists then 0 is returned.
+  double GetStartTime();
+
+  // Return the end time in seconds obtained from the URI
+  // fragment. If no end time or no valid temporal fragment
+  // exists then -1 is returned.
+  double GetEndTime();
+
+private:
+  // The following methods parse the fragment as per the media
+  // fragments specification. 'aString' contains the remaining
+  // fragment data to be parsed. The method returns PR_TRUE
+  // if the parse was successful and leaves the remaining unparsed
+  // data in 'aString'. If the parse fails then PR_FALSE is returned
+  // and 'aString' is left as it was when called.
+  PRBool ParseNPT(nsDependentSubstring& aString, double& aStart, double& aEnd);
+  PRBool ParseNPTTime(nsDependentSubstring& aString, double& aTime);
+  PRBool ParseNPTSec(nsDependentSubstring& aString, double& aSec);
+  PRBool ParseNPTFraction(nsDependentSubstring& aString, double& aFraction);
+  PRBool ParseNPTMMSS(nsDependentSubstring& aString, double& aTime);
+  PRBool ParseNPTHHMMSS(nsDependentSubstring& aString, double& aTime);
+  PRBool ParseNPTHH(nsDependentSubstring& aString, PRUint32& aHour);
+  PRBool ParseNPTMM(nsDependentSubstring& aString, PRUint32& aMinute);
+  PRBool ParseNPTSS(nsDependentSubstring& aString, PRUint32& aSecond);
+
+  // Fragment portion of the URI given on construction
+  nsCAutoString mHash;
+
+  // An array of name/value pairs containing the media fragments
+  // parsed from the URI.
+  nsTArray<Pair> mFragments;
+};
+
+#endif
--- a/content/media/nsBuiltinDecoder.cpp
+++ b/content/media/nsBuiltinDecoder.cpp
@@ -934,16 +934,25 @@ nsresult nsBuiltinDecoder::GetSeekable(n
                               : initialTime + GetDuration();
     aSeekable->Add(initialTime, end);
     return NS_OK;
   }
 
   return GetBuffered(aSeekable);
 }
 
+void nsBuiltinDecoder::SetEndTime(double aTime)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+  if (mDecoderStateMachine) {
+    ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+    mDecoderStateMachine->SetFragmentEndTime(static_cast<PRInt64>(aTime * USECS_PER_S));
+  }
+}
+
 void nsBuiltinDecoder::Suspend()
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
   if (mStream) {
     mStream->Suspend(PR_TRUE);
   }
 }
 
--- a/content/media/nsBuiltinDecoder.h
+++ b/content/media/nsBuiltinDecoder.h
@@ -276,16 +276,19 @@ public:
   // aDuration is in microseconds.
   virtual void SetDuration(PRInt64 aDuration) = 0;
 
   // Called while decoding metadata to set the end time of the media
   // resource. The decoder monitor must be obtained before calling this.
   // aEndTime is in microseconds.
   virtual void SetEndTime(PRInt64 aEndTime) = 0;
 
+  // Set the media fragment end time. aEndTime is in microseconds.
+  virtual void SetFragmentEndTime(PRInt64 aEndTime) = 0;
+
   // Functions used by assertions to ensure we're calling things
   // on the appropriate threads.
   virtual PRBool OnDecodeThread() const = 0;
 
   // Returns PR_TRUE if the current thread is the state machine thread.
   virtual PRBool OnStateMachineThread() const = 0;
 
   virtual nsHTMLMediaElement::NextFrameStatus GetNextFrameStatus() = 0;
@@ -432,16 +435,20 @@ class nsBuiltinDecoder : public nsMediaD
   // Set a flag indicating whether seeking is supported
   virtual void SetSeekable(PRBool aSeekable);
 
   // Return PR_TRUE if seeking is supported.
   virtual PRBool IsSeekable();
 
   virtual nsresult GetSeekable(nsTimeRanges* aSeekable);
 
+  // Set the end time of the media resource. When playback reaches
+  // this point the media pauses. aTime is in seconds.
+  virtual void SetEndTime(double aTime);
+
   virtual Statistics GetStatistics();
 
   // Suspend any media downloads that are in progress. Called by the
   // media element when it is sent to the bfcache. Call on the main
   // thread only.
   virtual void Suspend();
 
   // Resume any media downloads that have been suspended. Called by the
--- a/content/media/nsBuiltinDecoderStateMachine.cpp
+++ b/content/media/nsBuiltinDecoderStateMachine.cpp
@@ -197,16 +197,17 @@ nsBuiltinDecoderStateMachine::nsBuiltinD
                                                            nsBuiltinDecoderReader* aReader) :
   mDecoder(aDecoder),
   mState(DECODER_STATE_DECODING_METADATA),
   mCbCrSize(0),
   mPlayDuration(0),
   mStartTime(-1),
   mEndTime(-1),
   mSeekTime(0),
+  mFragmentEndTime(-1),
   mReader(aReader),
   mCurrentFrameTime(0),
   mAudioStartTime(-1),
   mAudioEndTime(-1),
   mVideoFrameEndTime(-1),
   mVolume(1.0),
   mSeekable(PR_TRUE),
   mPositionChangeQueued(PR_FALSE),
@@ -848,25 +849,30 @@ void nsBuiltinDecoderStateMachine::Updat
     NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
   }
 }
 
 void nsBuiltinDecoderStateMachine::UpdatePlaybackPosition(PRInt64 aTime)
 {
   UpdatePlaybackPositionInternal(aTime);
 
-  if (!mPositionChangeQueued) {
+  PRBool fragmentEnded = mFragmentEndTime >= 0 && GetMediaTime() >= mFragmentEndTime;
+  if (!mPositionChangeQueued || fragmentEnded) {
     mPositionChangeQueued = PR_TRUE;
     nsCOMPtr<nsIRunnable> event =
       NS_NewRunnableMethod(mDecoder, &nsBuiltinDecoder::PlaybackPositionChanged);
     NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
   }
 
   // Notify DOM of any queued up audioavailable events
   mEventManager.DispatchPendingEvents(GetMediaTime());
+
+  if (fragmentEnded) {
+    StopPlayback();
+  }
 }
 
 void nsBuiltinDecoderStateMachine::ClearPositionChangeFlag()
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
 
   mPositionChangeQueued = PR_FALSE;
@@ -930,16 +936,23 @@ void nsBuiltinDecoderStateMachine::SetDu
 void nsBuiltinDecoderStateMachine::SetEndTime(PRInt64 aEndTime)
 {
   NS_ASSERTION(OnDecodeThread(), "Should be on decode thread");
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
 
   mEndTime = aEndTime;
 }
 
+void nsBuiltinDecoderStateMachine::SetFragmentEndTime(PRInt64 aEndTime)
+{
+  mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
+
+  mFragmentEndTime = aEndTime < 0 ? aEndTime : aEndTime + mStartTime;
+}
+
 void nsBuiltinDecoderStateMachine::SetSeekable(PRBool aSeekable)
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
 
   mSeekable = aSeekable;
 }
 
@@ -1701,17 +1714,17 @@ void nsBuiltinDecoderStateMachine::Advan
     }
     StartBuffering();
     ScheduleStateMachine();
     return;
   }
 
   // We've got enough data to keep playing until at least the next frame.
   // Start playing now if need be.
-  if (!IsPlaying()) {
+  if (!IsPlaying() && ((mFragmentEndTime >= 0 && clock_time < mFragmentEndTime) || mFragmentEndTime < 0)) {
     StartPlayback();
   }
 
   if (currentFrame) {
     // Decode one frame and display it.
     TimeStamp presTime = mPlayStartTime - UsecsToDuration(mPlayDuration) +
                           UsecsToDuration(currentFrame->mTime - mStartTime);
     NS_ASSERTION(currentFrame->mTime >= mStartTime, "Should have positive frame time");
--- a/content/media/nsBuiltinDecoderStateMachine.h
+++ b/content/media/nsBuiltinDecoderStateMachine.h
@@ -273,16 +273,19 @@ public:
   // Schedules the shared state machine thread to run the state machine
   // in aUsecs microseconds from now, if it's not already scheduled to run
   // earlier, in which case the request is discarded.
   nsresult ScheduleStateMachine(PRInt64 aUsecs);
 
   // Timer function to implement ScheduleStateMachine(aUsecs).
   void TimeoutExpired();
 
+  // Set the media fragment end time. aEndTime is in microseconds.
+  void SetFragmentEndTime(PRInt64 aEndTime);
+
 protected:
 
   // Returns PR_TRUE if we've got less than aAudioUsecs microseconds of decoded
   // and playable data. The decoder monitor must be held.
   PRBool HasLowDecodedData(PRInt64 aAudioUsecs) const;
 
   // Returns PR_TRUE if we're running low on data which is not yet decoded.
   // The decoder monitor must be held.
@@ -506,16 +509,19 @@ protected:
   // machine, decode, and main threads. Access controlled by decoder monitor.
   PRInt64 mEndTime;
 
   // Position to seek to in microseconds when the seek state transition occurs.
   // The decoder monitor lock must be obtained before reading or writing
   // this value. Accessed on main and decode thread.
   PRInt64 mSeekTime;
 
+  // Media Fragment end time in microseconds. Access controlled by decoder monitor.
+  PRInt64 mFragmentEndTime;
+
   // The audio stream resource. Used on the state machine, and audio threads.
   // This is created and destroyed on the audio thread, while holding the
   // decoder monitor, so if this is used off the audio thread, you must
   // first acquire the decoder monitor and check that it is non-null.
   nsRefPtr<nsAudioStream> mAudioStream;
 
   // The reader, don't call its methods with the decoder monitor held.
   // This is created in the play state machine's constructor, and destroyed
--- a/content/media/nsMediaDecoder.h
+++ b/content/media/nsMediaDecoder.h
@@ -291,16 +291,20 @@ public:
   virtual void SetSeekable(PRBool aSeekable) = 0;
 
   // Return PR_TRUE if seeking is supported.
   virtual PRBool IsSeekable() = 0;
 
   // Return the time ranges that can be seeked into.
   virtual nsresult GetSeekable(nsTimeRanges* aSeekable) = 0;
 
+  // Set the end time of the media resource. When playback reaches
+  // this point the media pauses. aTime is in seconds.
+  virtual void SetEndTime(double aTime) = 0;
+
   // Invalidate the frame.
   virtual void Invalidate();
 
   // Fire progress events if needed according to the time and byte
   // constraints outlined in the specification. aTimer is PR_TRUE
   // if the method is called as a result of the progress timer rather
   // than the result of downloaded data.
   virtual void Progress(PRBool aTimer);
--- a/content/media/test/Makefile.in
+++ b/content/media/test/Makefile.in
@@ -69,16 +69,18 @@ include $(topsrcdir)/config/rules.mk
 _TEST_FILES = \
 		allowed.sjs \
 		can_play_type_ogg.js \
 		can_play_type_wave.js \
 		can_play_type_webm.js \
 		cancellable_request.sjs \
 		dynamic_redirect.sjs \
 		file_access_controls.html \
+		fragment_play.js \
+		fragment_noplay.js \
 		manifest.js \
 		reactivate_helper.html \
 		redirect.sjs \
 		referer.sjs \
 		seek1.js \
 		seek2.js \
 		seek3.js \
 		seek4.js \
@@ -283,16 +285,18 @@ else
 _TEST_FILES += \
 		test_can_play_type_no_webm.html \
 		$(NULL)
 endif
 
 ifdef MOZ_WAVE
 _TEST_FILES += \
 		test_can_play_type_wave.html \
+		test_fragment_play.html \
+		test_fragment_noplay.html \
 		test_wave_data_u8.html \
 		test_wave_data_s16.html \
 		$(NULL)
 else
 _TEST_FILES += \
 		test_can_play_type_no_wave.html \
 		$(NULL)
 endif
new file mode 100644
--- /dev/null
+++ b/content/media/test/fragment_noplay.js
@@ -0,0 +1,19 @@
+function test_fragment_noplay(v, start, end, is, ok, finish) {
+
+var completed = false;
+
+function onLoadedMetadata() {
+  var s = start == null ? 0 : start;
+  var e = end == null ? v.duration : end;
+  var a = s - 0.15;
+  var b = s + 0.15;
+  ok(v.currentTime >= a && v.currentTime <= b, "loadedmetadata currentTime is " + a + " < " + v.currentTime + " < " + b);
+  ok(v.initialTime == s, "initialTime (" + v.initialTime + ") == start Time (" + s + ")");
+  ok(v.mozFragmentEnd == e, "mozFragmentEnd (" + v.mozFragmentEnd + ") == end Time (" + e + ")");
+  completed = true;
+  finish();
+  return false;
+}
+
+v.addEventListener("loadedmetadata", onLoadedMetadata, false);
+}
new file mode 100644
--- /dev/null
+++ b/content/media/test/fragment_play.js
@@ -0,0 +1,74 @@
+function test_fragment_play(v, start, end, is, ok, finish) {
+
+var completed = false;
+var loadedMetadataRaised = false;
+var seekedRaised = false;
+var pausedRaised = false;
+
+function onLoadedMetadata() {
+  var s = start == null ? 0 : start;
+  var e = end == null ? v.duration : end;
+  ok(v.currentTime == s, "loadedmetadata currentTime is " + v.currentTime + " != " + s);
+  ok(v.initialTime == s, "initialTime (" + v.initialTime + ") == start Time (" + s + ")");
+  ok(v.mozFragmentEnd == e, "mozFragmentEnd (" + v.mozFragmentEnd + ") == end Time (" + e + ")");
+  loadedMetadataRaised = true; 
+  v.play();
+  return false;
+}
+
+function onSeeked() {
+  if (completed)
+    return false;
+
+  var s = start == null ? 0 : start;
+  ok(v.currentTime == s, "seeked currentTime is " + v.currentTime + " != " + s);
+
+  seekedRaised = true;
+  return false;
+}
+
+function onTimeUpdate() {
+  if (completed)
+    return false;
+
+  v._lastTimeUpdate = v.currentTime;
+  return false;
+}
+
+
+function onPause() {
+  if (completed)
+    return false;
+
+  var e = end == null ? v.duration : end;
+  var a = e - 0.05;
+  var b = e + 0.05;
+  ok(v.currentTime >= a && v.currentTime <= b, "paused currentTime is " + a + " < " + v.currentTime + " < " + b + " ? " + v._lastTimeUpdate);
+  pausedRaised = true;
+  v.play();
+  return false;
+}
+
+
+function onEnded() {
+  if (completed)
+    return false;
+
+  completed = true;
+  ok(loadedMetadataRaised, "loadedmetadata event");
+  if (start) {
+    ok(seekedRaised, "seeked event");
+  }
+  if (end) {
+    ok(pausedRaised, "paused event: " + end + " " + v.duration);
+  }
+  finish();
+  return false;
+}
+
+v.addEventListener("ended", onEnded, false);
+v.addEventListener("loadedmetadata", onLoadedMetadata, false);
+v.addEventListener("seeked", onSeeked, false);
+v.addEventListener("pause", onPause, false);
+v.addEventListener("timeupdate", onTimeUpdate, false);
+}
--- a/content/media/test/manifest.js
+++ b/content/media/test/manifest.js
@@ -261,16 +261,22 @@ var gDecodeErrorTests = [
   { name:"dirac.ogg", type:"video/ogg" },
   // Invalid files
   { name:"bogus.wav", type:"audio/x-wav" },
   { name:"bogus.ogv", type:"video/ogg" },
 
   { name:"bogus.duh", type:"bogus/duh" }
 ];
 
+// These are files that are used for media fragments tests
+var gFragmentTests = [
+  { name:"big.wav", type:"audio/x-wav", duration:9.28, size:102444 }
+];
+
+
 function checkMetadata(msg, e, test) {
   if (test.width) {
     is(e.videoWidth, test.width, msg + " video width");
   }
   if (test.height) {
     is(e.videoHeight, test.height, msg + " video height");
   }
   if (test.duration) {
new file mode 100644
--- /dev/null
+++ b/content/media/test/test_fragment_noplay.html
@@ -0,0 +1,129 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Media test: fragment tests</title>
+  <script type="text/javascript" src="/MochiKit/packed.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="manifest.js"></script>
+  <script type="text/javascript" src="fragment_noplay.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+// Fragment parameters to try
+var gFragmentParams = [
+  // W3C Media fragment tests
+  // http://www.w3.org/2008/WebVideo/Fragments/TC/ua-test-cases
+  { fragment: "#t=banana", start: null, end: null }, // TC0027-UA
+  { fragment: "#t=3,banana", start: null, end: null }, // TC0028-UA
+  { fragment: "#t=banana,7", start: null, end: null }, // TC0029-UA
+  { fragment: "#t='3'", start: null, end: null }, // TC0030-UA
+  { fragment: "#t=3-7", start: null, end: null }, // TC0031-UA
+  { fragment: "#t=3:7", start: null, end: null }, // TC0032-UA
+  { fragment: "#t=3,7,9", start: null, end: null }, // TC0033-UA
+  { fragment: "#t%3D3", start: null, end: null }, // TC0034-UA
+  { fragment: "#%74=3", start: 3, end: null }, // TC0035-UA
+  { fragment: "#t=%33", start: 3, end: null }, // TC0036-UA
+  { fragment: "#t=3%2C7", start: 3, end: 7 }, // TC0037-UA
+  { fragment: "#t=%6Ept:3", start: 3, end: null }, // TC0038-UA
+  { fragment: "#t=npt%3A3", start: 3, end: null }, // TC0039-UA
+  { fragment: "#t=-1,3", start: null, end: null }, // TC0044-UA
+  { fragment: "#t=3&", start: 3, end: null }, // TC0051-UA
+  { fragment: "#u=12&t=3", start: 3, end: null }, // TC0052-UA
+  { fragment: "#t=foo:7&t=npt:3", start: 3, end: null }, // TC0053-UA
+  { fragment: "#&&=&=tom&jerry=&t=3&t=meow:0#", start: 3, end: null }, // TC0054-UA
+  { fragment: "#t=7&t=3", start: 3, end: null }, // TC0055-UA
+  { fragment: "#T=3,7", start: null, end: null }, // TC0058-UA
+  { fragment: "#t=", start: null, end: null }, // TC0061-UA
+  { fragment: "#t=.", start: null, end: null }, // TC0062-UA
+  { fragment: "#t=.0", start: null, end: null }, // TC0063-UA
+  { fragment: "#t=0s", start: null, end: null }, // TC0064-UA
+  { fragment: "#t=,0s", start: null, end: null }, // TC0065-UA
+  { fragment: "#t=0s,0s", start: null, end: null }, // TC0066-UA
+  { fragment: "#t=00:00:00s", start: null, end: null }, // TC0067-UA
+  { fragment: "#t=s", start: null, end: null }, // TC0068-UA
+  { fragment: "#t=npt:", start: null, end: null }, // TC0069-UA
+  { fragment: "#t=1e-1:", start: null, end: null }, // TC0070-UA
+  { fragment: "#t=00:00:01.1e-1", start: null, end: null }, // TC0071-UA
+  { fragment: "#t=3.", start: 3, end: null }, // TC0072-UA
+  { fragment: "#t=0:0:0", start: null, end: null }, // TC0073-UA
+  { fragment: "#t=0:00:60", start: null, end: null }, // TC0074-UA
+  { fragment: "#t=0:01:60", start: null, end: null }, // TC0075-UA
+  { fragment: "#t=0:60:00", start: null, end: null }, // TC0076-UA
+  { fragment: "#t=0:000:000", start: null, end: null }, // TC0077-UA
+  { fragment: "#t=00:00:03,00:00:07", start: 3, end: 7 }, // TC0078-UA
+  { fragment: "#t=3,00:00:07", start: 3, end: 7 }, // TC0079-UA
+  { fragment: "#t=00:00.", start: null, end: null }, // TC0080-UA
+  { fragment: "#t=0:00:00.", start: null, end: null }, // TC0081-UA
+  { fragment: "#t=0:00:10e-1", start: null, end: null }, // TC0082-UA
+  { fragment: "#t=0:00:60.000", start: null, end: null }, // TC0083-UA
+  { fragment: "#t=0:60:00.000", start: null, end: null }, // TC0084-UA
+  { fragment: "#t=3,7&t=foo", start: 3, end: 7 }, // TC0085-UA
+  { fragment: "#foo&t=3,7", start: 3, end: 7 }, // TC0086-UA
+  { fragment: "#t=3,7&foo", start: 3, end: 7 }, // TC0087-UA
+  { fragment: "#t=3,7&&", start: 3, end: 7 }, // TC0088-UA
+  { fragment: "#&t=3,7", start: 3, end: 7 }, // TC0089-UA
+  { fragment: "#&&t=3,7", start: 3, end: 7 }, // TC0090-UA
+  { fragment: "#&t=3,7&", start: 3, end: 7 }, // TC0091-UA
+  { fragment: "#t%3d10", start: null, end: null }, // TC0092-UA
+  { fragment: "#t=10%26", start: null, end: null }, // TC0093-UA
+  { fragment: "#&t=3,7,", start: null, end: null } // TC0094-UA
+];
+
+function createTestArray() {
+  var tests = [];
+  var tmpVid = document.createElement("video");
+
+  for (var testNum=0; testNum<gFragmentTests.length; testNum++) {
+    var test = gFragmentTests[testNum];
+    if (!tmpVid.canPlayType(test.type)) {
+      continue;
+    }
+
+    for (var fragNum=0; fragNum<gFragmentParams.length; fragNum++) {
+      var p = gFragmentParams[fragNum];
+      var t = new Object;
+      t.name = test.name + p.fragment;
+      t.type = test.type;
+      t.duration = test.duration;
+      t.start = p.start;
+      t.end = p.end;
+      tests.push(t);
+    }
+  }
+  return tests;
+}
+
+function startTest(test, token) {
+  var v = document.createElement('video');
+  manager.started(token);
+  v.src = test.name;
+  v.token = token;
+  v.controls = true;
+  document.body.appendChild(v);
+  var name = test.name + " fragment test";
+  var localIs = function(name) { return function(a, b, msg) {
+    is(a, b, name + ": " + msg);
+  }}(name);
+  var localOk = function(name) { return function(a, msg) {
+    ok(a, name + ": " + msg);
+  }}(name);
+  var localFinish = function(v, manager) { return function() {
+    if (v.parentNode) {
+      v.parentNode.removeChild(v);
+    }
+    manager.finished(v.token);
+  }}(v, manager);
+  window['test_fragment_noplay'](v, test.start, test.end, localIs, localOk, localFinish);
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/content/media/test/test_fragment_play.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Media test: seek tests</title>
+  <script type="text/javascript" src="/MochiKit/packed.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="manifest.js"></script>
+  <script type="text/javascript" src="fragment_play.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+// Fragment parameters to try. These tests
+// try playing the video. Tests for other fragment
+// formats are in test_fragment_noplay.html.
+var gFragmentParams = [
+  { fragment: "", start: null, end: null },
+  { fragment: "#t=,", start: null, end: null },
+  { fragment: "#t=3,3", start: null, end: null },
+  { fragment: "#t=7,3", start: null, end: null },
+  { fragment: "#t=7,15", start: 7, end: null },
+  { fragment: "#t=15,20", start: 9.287981, end: null, todo: "See Bug 679262" },
+  { fragment: "#t=5", start: 5, end: null },
+  { fragment: "#t=5.5", start: 5.5, end: null },
+  { fragment: "#t=5,", start: null, end: null },
+  { fragment: "#t=,5", start: 0, end: 5 },
+  { fragment: "#t=2.5,5.5", start: 2.5, end: 5.5 },
+  { fragment: "#t=1,2.5", start: 1, end: 2.5 },
+  { fragment: "#t=,15", start: 0, end: null }
+];
+
+function createTestArray() {
+  var tests = [];
+  var tmpVid = document.createElement("video");
+
+  for (var testNum=0; testNum<gFragmentTests.length; testNum++) {
+    var test = gFragmentTests[testNum];
+    if (!tmpVid.canPlayType(test.type)) {
+      continue;
+    }
+
+    for (var fragNum=0; fragNum<gFragmentParams.length; fragNum++) {
+      var p = gFragmentParams[fragNum];
+      var t = new Object;
+      t.name = test.name + p.fragment;
+      t.type = test.type;
+      t.duration = test.duration;
+      t.start = p.start;
+      t.end = p.end;
+      t.todo = p.todo;
+      tests.push(t);
+    }
+  }
+  return tests;
+}
+
+function startTest(test, token) {
+  if (test.todo) {
+    todo(false, test.todo);
+    return;
+  }
+  var v = document.createElement('video');
+  manager.started(token);
+  v.src = test.name;
+  v.token = token;
+  v.controls = true;
+  document.body.appendChild(v);
+  var name = test.name + " fragment test";
+  var localIs = function(name) { return function(a, b, msg) {
+    is(a, b, name + ": " + msg);
+  }}(name);
+  var localOk = function(name) { return function(a, msg) {
+    ok(a, name + ": " + msg);
+  }}(name);
+  var localFinish = function(v, manager) { return function() {
+    if (v.parentNode) {
+      v.parentNode.removeChild(v);
+    }
+    manager.finished(v.token);
+  }}(v, manager);
+  window['test_fragment_play'](v, test.start, test.end, localIs, localOk, localFinish);
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
--- a/docshell/base/nsDefaultURIFixup.cpp
+++ b/docshell/base/nsDefaultURIFixup.cpp
@@ -460,18 +460,16 @@ PRBool nsDefaultURIFixup::MakeAlternateU
     oldHost.EndReading(iterEnd);
     while (iter != iterEnd) {
         if (*iter == '.')
             numDots++;
         ++iter;
     }
 
 
-    nsresult rv;
-
     // Get the prefix and suffix to stick onto the new hostname. By default these
     // are www. & .com but they could be any other value, e.g. www. & .org
 
     nsCAutoString prefix("www.");
     nsAdoptingCString prefPrefix =
         Preferences::GetCString("browser.fixup.alternate.prefix");
     if (prefPrefix)
     {
--- a/dom/interfaces/html/nsIDOMHTMLMediaElement.idl
+++ b/dom/interfaces/html/nsIDOMHTMLMediaElement.idl
@@ -52,17 +52,17 @@
 
 // undef the GetCurrentTime macro defined in WinBase.h from the MS Platform SDK
 %{C++
 #ifdef GetCurrentTime
 #undef GetCurrentTime
 #endif
 %}
 
-[scriptable, uuid(9441effd-fbe1-4f48-aa90-1741c1e390a1)]
+[scriptable, uuid(16b81ded-30cb-427d-831d-1635dc68d346)]
 interface nsIDOMHTMLMediaElement : nsIDOMHTMLElement
 {
   // error state
   readonly attribute nsIDOMMediaError error;
 
   // network state
            attribute DOMString src;
   readonly attribute DOMString currentSrc;
@@ -82,16 +82,17 @@ interface nsIDOMHTMLMediaElement : nsIDO
   const unsigned short HAVE_CURRENT_DATA = 2;
   const unsigned short HAVE_FUTURE_DATA = 3;
   const unsigned short HAVE_ENOUGH_DATA = 4;
   readonly attribute unsigned short readyState;
   readonly attribute boolean seeking;
 
   // playback state
            attribute double currentTime;
+  readonly attribute double initialTime;
   readonly attribute double duration;
   readonly attribute boolean paused;
   readonly attribute nsIDOMTimeRanges seekable;
   readonly attribute boolean ended;
   readonly attribute boolean mozAutoplayEnabled;
            attribute boolean autoplay;
   void play();
   void pause();
@@ -113,9 +114,14 @@ interface nsIDOMHTMLMediaElement : nsIDO
 
   // Mozilla extension: load data from another media element. This is like
   // load() but we don't run the resource selection algorithm; instead
   // we just set our source to other's currentSrc. This is optimized
   // so that this element will get access to all of other's cached/
   // buffered data. In fact any future data downloaded by this element or
   // other will be sharable by both elements.
   void mozLoadFrom(in nsIDOMHTMLMediaElement other);
+
+  // Mozilla extension: provides access to the fragment end time if
+  // the media element has a fragment URI for the currentSrc, otherwise
+  // it is equal to the media duration.
+  readonly attribute double mozFragmentEnd;
 };
--- a/dom/tests/unit/test_geolocation_provider.js
+++ b/dom/tests/unit/test_geolocation_provider.js
@@ -1,41 +1,80 @@
+do_load_httpd_js();
 
-const Ci = Components.interfaces;
-const Cc = Components.classes;
+var httpserver = null;
+var geolocation = null;
+var success = false;
+var watchId = -1;
 
-function successCallback(pos){}
+function terminate(succ) {
+      success = succ;
+      geolocation.clearWatch(watchID);
+    }
+
+function successCallback(pos){ terminate(true); }
+function errorCallback(pos) { terminate(false); } 
 
 var observer = {
     QueryInterface: function(iid) {
 	if (iid.equals(Components.interfaces.nsISupports) ||
 	    iid.equals(Components.interfaces.nsIObserver))
 	    return this;
 	throw Components.results.NS_ERROR_NO_INTERFACE;
     },
 
     observe: function(subject, topic, data) {
-	if (data == "shutdown") {
-	    do_check_true(1)
-	    do_test_finished();
-	}
-	else if (data == "starting") {
-	    do_check_true(1)
-	}
+        if (data == "shutdown") {
+            do_check_true(1);
+            this._numProviders--;
+            if (!this._numProviders) {
+                httpserver.stop(function() {
+                        do_check_true(success);
+                        do_test_finished();
+                    });
+            }
+        }
+        else if (data == "starting") {
+            do_check_true(1);
+            this._numProviders++;
+        }
+    },
 
-    },
+    _numProviders: 0,
 };
 
+function geoHandler(metadata, response)
+{
+    var georesponse = {
+        status: "OK",
+        location: {
+            lat: 42,
+            lng: 42,
+        },
+        accuracy: 42,
+    };
+  var position = JSON.stringify(georesponse);
+  response.setStatusLine("1.0", 200, "OK");
+  response.setHeader("Cache-Control", "no-cache", false);
+  response.setHeader("Content-Type", "aplication/x-javascript", false);
+  response.write(position);
+}
 
 function run_test()
 {
     // only kill this test when shutdown is called on the provider.
     do_test_pending();
+  
+    httpserver = new nsHttpServer();
+    httpserver.registerPathHandler("/geo", geoHandler);
+    httpserver.start(4444);
+  
+    var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+    prefs.setCharPref("geo.wifi.uri", "http://localhost:4444/geo");
 
     var obs = Cc["@mozilla.org/observer-service;1"].getService();
     obs = obs.QueryInterface(Ci.nsIObserverService);
     obs.addObserver(observer, "geolocation-device-events", false); 
 
-    var geolocation = Cc["@mozilla.org/geolocation;1"].getService(Ci.nsIDOMGeoGeolocation);
-    var watchID = geolocation.watchPosition(successCallback);
-    do_timeout(1000, function() { geolocation.clearWatch(watchID);})
+    geolocation = Cc["@mozilla.org/geolocation;1"].getService(Ci.nsIDOMGeoGeolocation);
+    watchID = geolocation.watchPosition(successCallback, errorCallback);
 }
 
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -3222,105 +3222,104 @@ WorkerPrivate::RunExpiredTimeouts(JSCont
   if (mRunningExpiredTimeouts || !mTimerRunning) {
     return true;
   }
 
   NS_ASSERTION(!mTimeouts.IsEmpty(), "Should have some work to do!");
 
   bool retval = true;
 
-  TimeStamp now = TimeStamp::Now();
+  AutoPtrComparator<TimeoutInfo> comparator = GetAutoPtrComparator(mTimeouts);
+  JSObject* global = JS_GetGlobalObject(aCx);
+  JSPrincipals* principal = GetWorkerPrincipal();
 
   // We want to make sure to run *something*, even if the timer fired a little
   // early. Fudge the value of now to at least include the first timeout.
-  now = NS_MAX(now, mTimeouts[0]->mTargetTime);
+  const TimeStamp now = NS_MAX(TimeStamp::Now(), mTimeouts[0]->mTargetTime);
 
   nsAutoTArray<TimeoutInfo*, 10> expiredTimeouts;
   for (PRUint32 index = 0; index < mTimeouts.Length(); index++) {
     nsAutoPtr<TimeoutInfo>& info = mTimeouts[index];
-    if (info->mTargetTime <= now || info->mCanceled) {
-      expiredTimeouts.AppendElement(info);
+    if (info->mTargetTime > now) {
+      break;
     }
+    expiredTimeouts.AppendElement(info);
   }
 
   // Guard against recursion.
   mRunningExpiredTimeouts = true;
 
   // Run expired timeouts.
   for (PRUint32 index = 0; index < expiredTimeouts.Length(); index++) {
     TimeoutInfo*& info = expiredTimeouts[index];
 
     if (info->mCanceled) {
       continue;
     }
 
-    JSObject* global = JS_GetGlobalObject(aCx);
+    // Always call JS_ReportPendingException if something fails, and if
+    // JS_ReportPendingException returns false (i.e. uncatchable exception) then
+    // break out of the loop.
 
     if (JSVAL_IS_STRING(info->mTimeoutVal)) {
       JSString* expression = JSVAL_TO_STRING(info->mTimeoutVal);
 
       size_t stringLength;
       const jschar* string = JS_GetStringCharsAndLength(aCx, expression,
                                                         &stringLength);
-      if (!string) {
-        if (!JS_ReportPendingException(aCx)) {
-          retval = false;
-          break;
-        }
-        continue;
-      }
-
-      if (!JS_EvaluateUCScriptForPrincipals(aCx, global, GetWorkerPrincipal(),
-                                            string, stringLength,
-                                            info->mFilename.get(),
-                                            info->mLineNumber, nsnull)) {
-        if (!JS_ReportPendingException(aCx)) {
-          retval = false;
-          break;
-        }
-        continue;
+
+      if ((!string ||
+           !JS_EvaluateUCScriptForPrincipals(aCx, global, principal, string,
+                                             stringLength,
+                                             info->mFilename.get(),
+                                             info->mLineNumber, nsnull)) &&
+          !JS_ReportPendingException(aCx)) {
+        retval = false;
+        break;
       }
     }
     else {
       jsval rval;
       if (!JS_CallFunctionValue(aCx, global, info->mTimeoutVal,
                                 info->mExtraArgVals.Length(),
-                                info->mExtraArgVals.Elements(), &rval)) {
-        if (!JS_ReportPendingException(aCx)) {
-          retval = false;
-          break;
-        }
+                                info->mExtraArgVals.Elements(), &rval) &&
+          !JS_ReportPendingException(aCx)) {
+        retval = false;
+        break;
       }
     }
+
+    // Reschedule intervals.
+    if (info->mIsInterval) {
+      PRUint32 timeoutIndex = mTimeouts.IndexOf(info);
+      NS_ASSERTION(timeoutIndex != PRUint32(-1),
+                   "Should still be in the main list!");
+
+      mTimeouts[timeoutIndex].forget();
+      mTimeouts.RemoveElementAt(timeoutIndex);
+
+      NS_ASSERTION(!mTimeouts.Contains(info), "Shouldn't have duplicates!");
+
+      info->mTargetTime += info->mInterval;
+      mTimeouts.InsertElementSorted(info, comparator);
+    }
   }
 
   // No longer possible to be called recursively.
   mRunningExpiredTimeouts = false;
 
-  // Clean up expired and canceled timeouts, reschedule intervals.
-  for (PRUint32 index = 0; index < expiredTimeouts.Length(); index++) {
-    TimeoutInfo*& info = expiredTimeouts[index];
-    if (info->mCanceled || !info->mIsInterval) {
+  // Now remove canceled and expired timeouts from the main list.
+  for (PRUint32 index = 0; index < mTimeouts.Length(); ) {
+    nsAutoPtr<TimeoutInfo>& info = mTimeouts[index];
+    if (info->mTargetTime <= now || info->mCanceled) {
       mTimeouts.RemoveElement(info);
-      continue;
     }
-
-    PRUint32 timeoutIndex = mTimeouts.IndexOf(info);
-    NS_ASSERTION(timeoutIndex != PRUint32(-1),
-                 "Should still be in the other list!");
-
-    mTimeouts[timeoutIndex].forget();
-    mTimeouts.RemoveElementAt(timeoutIndex);
-
-    NS_ASSERTION(!mTimeouts.Contains(info), "Shouldn't have duplicates!");
-
-    AutoPtrComparator<TimeoutInfo> comparator = GetAutoPtrComparator(mTimeouts);
-
-    info->mTargetTime += info->mInterval;
-    mTimeouts.InsertElementSorted(info, comparator);
+    else {
+      index++;
+    }
   }
 
   // Signal the parent that we're no longer using timeouts or reschedule the
   // timer.
   if (mTimeouts.IsEmpty()) {
     if (!ModifyBusyCountFromWorker(aCx, false)) {
       retval = false;
     }
--- a/dom/workers/test/close_worker.js
+++ b/dom/workers/test/close_worker.js
@@ -1,9 +1,14 @@
 /**
  * Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 onclose = function() {
   postMessage("closed");
 };
 
-setTimeout(function() { close(); }, 1000);
+setTimeout(function () {
+  setTimeout(function () {
+    throw new Error("I should never run!");
+  }, 1000);
+  close();
+}, 1000);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/bug657975.js
@@ -0,0 +1,8 @@
+// |jit-test| debug
+
+// bug 658491
+function f7() {
+  try { y = w; } catch(y) {}
+}
+trap(f7, 14, '')
+f7()
--- a/js/src/jit-test/tests/debug/Frame-arguments-07.js
+++ b/js/src/jit-test/tests/debug/Frame-arguments-07.js
@@ -9,15 +9,15 @@ g.eval("function f(a, b) {\n" +
 
 var dbg = Debugger(g);
 var hits = 0;
 dbg.onDebuggerStatement = function (frame) {
     var argc = frame.eval("arguments.length").return;
     var args = frame.arguments;
     assertEq(args.length, argc);
     for (var i = 0; i < argc; i++)
-	assertEq(args[i], i);
+        assertEq(args[i], i);
     hits++;
 }
 
 g.f(9);
 g.f(9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9);
 assertEq(hits, 2);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-01.js
@@ -0,0 +1,24 @@
+// Simple Debugger.Frame.prototype.onStep test.
+// Test that onStep fires often enough to see all four values of a.
+
+var g = newGlobal('new-compartment');
+g.a = 0;
+g.eval("function f() {\n" +
+       "    a += 2;\n" +
+       "    a += 2;\n" +
+       "    a += 2;\n" +
+       "    return a;\n" +
+       "}\n");
+
+var dbg = Debugger(g);
+var seen = [0, 0, 0, 0, 0, 0, 0];
+dbg.onEnterFrame = function (frame) {
+    frame.onStep = function () {
+        assertEq(arguments.length, 0);
+        assertEq(this, frame);
+        seen[g.a] = 1;
+    };
+}
+
+g.f();
+assertEq(seen.join(""), "1010101");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-02.js
@@ -0,0 +1,27 @@
+// Setting frame.onStep to undefined turns off single-stepping.
+
+var g = newGlobal('new-compartment');
+g.a = 0;
+g.eval("function f() {\n" +
+       "    a++;\n" +
+       "    a++;\n" +
+       "    a++;\n" +
+       "    a++;\n" +
+       "    return a;\n" +
+       "}\n");
+
+var dbg = Debugger(g);
+var seen = [0, 0, 0, 0, 0];
+dbg.onEnterFrame = function (frame) {
+    seen[g.a] = 1;
+    frame.onStep = function () {
+        seen[g.a] = 1;
+        if (g.a === 2) {
+            frame.onStep = undefined;
+            assertEq(frame.onStep, undefined);
+        }
+    };
+}
+
+g.f();
+assertEq(seen.join(","), "1,1,1,0,0");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-03.js
@@ -0,0 +1,28 @@
+// Setting onStep does not affect later calls to the same function.
+// (onStep is per-frame, not per-function.)
+
+var g = newGlobal('new-compartment');
+g.a = 1;
+g.eval("function f(a) {\n" +
+       "    var x = 2 * a;\n" +
+       "    return x * x;\n" +
+       "}\n");
+
+var dbg = Debugger(g);
+var log = '';
+dbg.onEnterFrame = function (frame) {
+    log += '+';
+    frame.onStep = function () {
+        if (log.charAt(log.length - 1) != 's')
+            log += 's';
+    };
+};
+
+g.f(1);
+log += '|';
+g.f(2);
+log += '|';
+dbg.onEnterFrame = undefined;
+g.f(3);
+
+assertEq(log, '+s|+s|');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-04.js
@@ -0,0 +1,34 @@
+// When a recursive function has many frames on the stack, onStep may be set or
+// not independently on each frame.
+
+var g = newGlobal('new-compartment');
+g.eval("function f(x) {\n" +
+       "    if (x > 0)\n" +
+       "        f(x - 1);\n" +
+       "    else\n" +
+       "        debugger;\n" +
+       "    return x;\n" +
+       "}");
+
+var dbg = Debugger(g);
+var seen = [0, 0, 0, 0, 0, 0, 0, 0];
+function step() {
+    seen[this.arguments[0]] = 1;
+}
+dbg.onEnterFrame = function (frame) {
+    // Turn on stepping for even-numbered frames.
+    var x = frame.arguments[0];
+    if (x % 2 === 0)
+        frame.onStep = step;
+};
+dbg.onDebuggerStatement = function (frame) {
+    // This is called with 8 call frames on the stack, 7 down to 0.
+    // At this point we should have seen all the even-numbered frames.
+    assertEq(seen.join(""), "10101010");
+
+    // Now reset seen to see which frames fire onStep on the way out.
+    seen = [0, 0, 0, 0, 0, 0, 0, 0];
+};
+
+g.f(7);
+assertEq(seen.join(""), "10101010");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-05.js
@@ -0,0 +1,14 @@
+// Upon returning to a frame with an onStep hook, the hook is called before the
+// next line.
+
+var g = newGlobal('new-compartment');
+g.log = '';
+g.eval("function f() { debugger; }");
+
+var dbg = Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+    frame.older.onStep = function () { g.log += 's'; };
+};
+g.eval("f();\n" +
+       "log += 'x';\n");
+assertEq(g.log.charAt(0), 's');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-06.js
@@ -0,0 +1,66 @@
+// After returning from an implicit toString call, the calling frame's onStep
+// hook fires.
+
+var g = newGlobal('new-compartment');
+g.eval("var originalX = {toString: function () { debugger; log += 'x'; return 1; }};\n");
+
+var dbg = Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+    g.log += 'd';
+    frame.older.onStep = function () {
+        if (!g.log.match(/[sy]$/))
+            g.log += 's';
+    };
+};
+
+// expr is an expression that will trigger an implicit toString call.
+function check(expr) {
+    g.log = '';
+    g.x = g.originalX;
+    g.eval(expr + ";\n" +
+	   "log += 'y';\n");
+    assertEq(g.log, 'dxsy');
+}
+
+check("'' + x");
+check("0 + x");
+check("0 - x");
+check("0 * x");
+check("0 / x");
+check("0 % x");
+check("+x");
+check("x in {}");
+check("x++");
+check("++x");
+check("x--");
+check("--x");
+check("x < 0");
+check("x > 0");
+check("x >= 0");
+check("x <= 0");
+check("x == 0");
+check("x != 0");
+check("x & 1");
+check("x | 1");
+check("x ^ 1");
+check("~x");
+check("x << 1");
+check("x >> 1");
+check("x >>> 1");
+
+g.eval("function lastStep() { throw StopIteration; }");
+g.eval("function emptyIterator() { debugger; log += 'x'; return { next: lastStep }; }");
+g.eval("var customEmptyIterator = { __iterator__: emptyIterator };");
+g.log = '';
+g.eval("for (i in customEmptyIterator);\n" +
+       "log += 'y';\n");
+assertEq(g.log, 'dxsy');
+
+g.eval("var getter = { get x() { debugger; return log += 'x'; } }");
+check("getter.x");
+
+g.eval("var setter = { set x(v) { debugger; return log += 'x'; } }");
+check("setter.x = 1");
+
+g.eval("Object.defineProperty(this, 'thisgetter', { get: function() { debugger; log += 'x'; }});");
+check("thisgetter");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-07.js
@@ -0,0 +1,23 @@
+// The tracejit does not interfere with frame.onStep.
+//
+// The function f() writes 'L' to the log in a loop. If we enable stepping and
+// write an 's' each time frame.onStep is called, any two Ls should have at
+// least one 's' between them.
+
+var g = newGlobal('new-compartment');
+g.N = RUNLOOP + 2;
+g.log = '';
+g.eval("function f() {\n" +
+       "    for (var i = 0; i <= N; i++)\n" +
+       "        log += 'L';\n" +
+       "}\n");
+g.f();
+assertEq(/LL/.exec(g.log) !== null, true);
+
+var dbg = Debugger(g);
+dbg.onEnterFrame = function (frame) {
+    frame.onStep = function () { g.log += 's'; };
+};
+g.log = '';
+g.f();
+assertEq(/LL/.exec(g.log), null);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-08.js
@@ -0,0 +1,29 @@
+// frame.onStep can coexist with breakpoints.
+
+var g = newGlobal('new-compartment');
+var dbg = Debugger(g);
+var log = '';
+dbg.onEnterFrame = function (frame) {
+    var handler = {hit: function () { log += 'B'; }};
+    var lines = frame.script.getAllOffsets();
+    for (var line in lines) {
+        line = Number(line);
+        var offs = lines[line];
+        for (var i = 0; i < offs.length; i++)
+            frame.script.setBreakpoint(offs[i], handler);
+    }
+
+    frame.onStep = function () { log += 's'; };
+};
+
+g.eval("one = 1;\n" +
+       "two = 2;\n" +
+       "three = 3;\n" +
+       "four = 4;\n");
+assertEq(g.four, 4);
+
+// Breakpoints hit on all four lines.
+assertEq(log.replace(/[^B]/g, ''), 'BBBB');
+
+// onStep was called between each pair of breakpoints.
+assertEq(/BB/.exec(log), null);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-09.js
@@ -0,0 +1,24 @@
+// After an implicit toString call throws an exception, the calling frame's
+// onStep hook fires.
+
+var g = newGlobal('new-compartment');
+g.eval("var x = {toString: function () { debugger; log += 'x'; throw 'mud'; }};");
+
+var dbg = Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+    g.log += 'd';
+    frame.older.onStep = function () {
+        if (!g.log.match(/[sy]$/))
+            g.log += 's';
+    };
+};
+
+g.log = '';
+g.eval("try { x + ''; } catch (x) { }\n" +
+       "log += 'y';\n");
+assertEq(g.log, "dxsy");
+
+g.log = '';
+g.eval("try { '' + x; } catch (x) { }\n" +
+       "log += 'y';\n");
+assertEq(g.log, "dxsy");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-10.js
@@ -0,0 +1,28 @@
+// Throwing and catching an error in an onStep handler shouldn't interfere
+// with throwing and catching in the debuggee.
+
+var g = newGlobal('new-compartment');
+g.eval("function f() { debugger; throw 'mud'; }");
+
+var dbg = Debugger(g);
+var stepped = false;
+dbg.onDebuggerStatement = function (frame) {
+    frame.older.onStep = function () {
+        stepped = true;
+        try {
+            throw 'snow';
+        } catch (x) {
+            assertEq(x, 'snow');
+        }
+    };
+};
+
+stepped = false;
+g.eval("var caught;\n" +
+       "try {\n" +
+       "    f();\n" +
+       "} catch (x) {\n" +
+       "    caught = x;\n" +
+       "}\n");
+assertEq(stepped, true);
+assertEq(g.caught, 'mud');
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-lines-01.js
@@ -0,0 +1,78 @@
+// Test that a frame's onStep handler gets called at least once on each line of a function.
+
+var g = newGlobal('new-compartment');
+var dbg = new Debugger(g);
+
+// When we hit a 'debugger' statement, set offsets to the frame's script's
+// table of line offsets --- a sparse array indexed by line number. Begin
+// single-stepping the current frame; for each source line we hit, delete
+// the line's entry in offsets. Thus, at the end, offsets is an array with
+// an element for each line we did not reach.
+var doSingleStep = true;
+var offsets;
+dbg.onDebuggerStatement = function (frame) {
+    var script = frame.script;
+    offsets = script.getAllOffsets();
+    print("debugger line: " + script.getOffsetLine(frame.offset));
+    print("original lines: " + uneval(Object.keys(offsets)));
+    if (doSingleStep) {
+	frame.onStep = function onStepHandler() {
+	    var line = script.getOffsetLine(this.offset);
+	    delete offsets[line];
+	};
+    }
+};
+
+g.eval(
+       'function t(a, b, c) {                \n' +
+       '    debugger;                        \n' +
+       '    var x = a;                       \n' +
+       '    x += b;                          \n' +
+       '    if (x < 10)                      \n' +
+       '        x -= c;                      \n' +
+       '    return x;                        \n' +
+       '}                                    \n'
+       );
+
+// This should stop at every line but the first of the function.
+g.eval('t(1,2,3)');
+assertEq(Object.keys(offsets).length, 1);
+
+// This should stop at every line but the first of the function, and the
+// body of the 'if'.
+g.eval('t(10,20,30)');
+assertEq(Object.keys(offsets).length, 2);
+
+// This shouldn't stop at all. It's the frame that's in single-step mode,
+// not the script, so the prior execution of t in single-step mode should
+// have no effect on this one.
+doSingleStep = false;
+g.eval('t(0, 0, 0)');
+assertEq(Object.keys(offsets).length, 6);
+doSingleStep = true;
+
+// Single-step in an eval frame. This should reach every line but the
+// first.
+g.eval(
+       'debugger;                        \n' +
+       'var a=1, b=2, c=3;               \n' +
+       'var x = a;                       \n' +
+       'x += b;                          \n' +
+       'if (x < 10)                      \n' +
+       '    x -= c;                      \n'
+       );
+print("final lines: " + uneval(Object.keys(offsets)));
+assertEq(Object.keys(offsets).length, 1);
+
+// Single-step in a global code frame. This should reach every line but the
+// first.
+g.evaluate(
+           'debugger;                        \n' +
+           'var a=1, b=2, c=3;               \n' +
+           'var x = a;                       \n' +
+           'x += b;                          \n' +
+           'if (x < 10)                      \n' +
+           '    x -= c;                      \n'
+           );
+print("final lines: " + uneval(Object.keys(offsets)));
+assertEq(Object.keys(offsets).length, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-01.js
@@ -0,0 +1,14 @@
+// If frame.onStep returns {return:val}, the frame returns.
+
+var g = newGlobal('new-compartment');
+g.eval("function f(x) {\n" +
+       "    var a = x * x;\n" +
+       "    return a;\n" +
+       "}\n");
+
+var dbg = Debugger(g);
+dbg.onEnterFrame = function (frame) {
+    frame.onStep = function () { return {return: "pass"}; };
+};
+
+assertEq(g.f(4), "pass");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-02.js
@@ -0,0 +1,17 @@
+// If frame.onStep returns {throw:}, an exception is thrown in the debuggee.
+
+load(libdir + "asserts.js");
+
+var g = newGlobal('new-compartment');
+g.eval("function h() { debugger; }\n" +
+       "function f() {\n" +
+       "    h();\n" +
+       "    return 'fail';\n" +
+       "}\n");
+
+var dbg = Debugger(g);
+dbg.onDebuggerStatement = function (frame) {
+    frame.older.onStep = function () { return {throw: "pass"}; };
+};
+
+assertThrowsValue(g.f, "pass");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-03.js
@@ -0,0 +1,19 @@
+// If frame.onStep returns null, the debuggee terminates.
+
+var g = newGlobal('new-compartment');
+g.eval("function h() { debugger; }");
+
+var dbg = Debugger(g);
+var hits = 0;
+dbg.onDebuggerStatement = function (frame) {
+    hits++;
+    if (hits == 1) {
+        var rv = frame.eval("h();\n" +
+                            "throw 'fail';\n");
+        assertEq(rv, null);
+    } else {
+        frame.older.onStep = function () { return null; };
+    }
+};
+g.h();
+assertEq(hits, 2);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-resumption-04.js
@@ -0,0 +1,31 @@
+// If frame.onStep returns null, debuggee catch and finally blocks are skipped.
+
+var g = newGlobal('new-compartment');
+g.eval("function h() { debugger; }");
+
+var dbg = Debugger(g);
+var hits = 0;
+dbg.onDebuggerStatement = function (frame) {
+    hits++;
+    if (hits == 1) {
+        var rv = frame.eval("try {\n" +
+                            "    h();\n" +
+                            "    throw 'fail';\n" +
+                            "} catch (exc) {\n" +
+                            "    caught = exc;\n" +
+                            "} finally {\n" +
+                            "    finallyHit = true;\n" +
+                            "}\n");
+        assertEq(rv, null);
+    } else {
+        frame.older.onStep = function () {
+            this.onStep = undefined;
+            return null;
+        };
+    }
+};
+
+g.h();
+assertEq(hits, 2);
+assertEq("caught" in g, false);
+assertEq("finallyHit" in g, false);
--- a/js/src/jit-test/tests/debug/Object-deleteProperty-error-01.js
+++ b/js/src/jit-test/tests/debug/Object-deleteProperty-error-01.js
@@ -1,16 +1,16 @@
 // Don't crash when a scripted proxy handler throws Error.prototype.
 
 var g = newGlobal('new-compartment');
 var dbg = Debugger(g);
 dbg.onDebuggerStatement = function (frame) {
     try {
-	frame.arguments[0].deleteProperty("x");
+        frame.arguments[0].deleteProperty("x");
     } catch (exc) {
-	return;
+        return;
     }
     throw new Error("deleteProperty should throw");
 };
 
 g.eval("function h(x) { debugger; }");
 g.eval("h(Proxy.create({delete: function () { throw Error.prototype; }}));");
 
--- a/js/src/jit-test/tests/debug/Object-seal-01.js
+++ b/js/src/jit-test/tests/debug/Object-seal-01.js
@@ -22,24 +22,24 @@ g.eval("function compareObjects() {\n" +
 
 function test(code) {
     g.code = code;
     g.eval("x = (" + code + ");");
     g.eval("y = (" + code + ");");
     var xw = gw.getOwnPropertyDescriptor("x").value;
 
     function check() {
-	// The Debugger.Object seal/freeze/preventExtensions methods
-	// had the same effect as the corresponding ES5 Object methods.
-	g.compareObjects();
+        // The Debugger.Object seal/freeze/preventExtensions methods
+        // had the same effect as the corresponding ES5 Object methods.
+        g.compareObjects();
 
-	// The Debugger.Object introspection methods agree with the ES5 Object methods.
-	assertEq(xw.isExtensible(), g.Object.isExtensible(g.x), code + ' isExtensible');
-	assertEq(xw.isSealed(), g.Object.isSealed(g.x), code + ' isSealed');
-	assertEq(xw.isFrozen(), g.Object.isFrozen(g.x), code + ' isFrozen');
+        // The Debugger.Object introspection methods agree with the ES5 Object methods.
+        assertEq(xw.isExtensible(), g.Object.isExtensible(g.x), code + ' isExtensible');
+        assertEq(xw.isSealed(), g.Object.isSealed(g.x), code + ' isSealed');
+        assertEq(xw.isFrozen(), g.Object.isFrozen(g.x), code + ' isFrozen');
     }
 
     check();
 
     xw.preventExtensions();
     assertEq(g.Object.isExtensible(g.x), false, code + ' preventExtensions');
     g.Object.preventExtensions(g.y);
     check();
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-gc-03.js
@@ -0,0 +1,25 @@
+// |jit-test| debug
+// The GC can cope with old and new breakpoints at the same position.
+
+// This is a regression test for a bug Bill McCloskey found just by looking at
+// the source code. See bug 677386 comment 8. Here we're testing that the trap
+// string is correctly marked. (The silly expression for the trap string is to
+// ensure that it isn't constant-folded; it's harder to get a compile-time
+// constant to be GC'd.)
+
+var g = newGlobal('new-compartment');
+g.eval("var d = 0;\n" +
+       "function f() { return 'ok'; }\n" +
+       "trap(f, 0, Array(17).join('\\n') + 'd++;');\n");
+
+var dbg = new Debugger;
+var gw = dbg.addDebuggee(g);
+var fw = gw.getOwnPropertyDescriptor("f").value;
+var bp = {hits: 0, hit: function (frame) { this.hits++; }};
+fw.script.setBreakpoint(0, bp);
+
+gc();
+
+g.f();
+assertEq(g.d, 1);
+assertEq(bp.hits, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-gc-04.js
@@ -0,0 +1,23 @@
+// Enabled debuggers keep breakpoint handlers alive.
+
+var g = newGlobal('new-compartment');
+g.eval("var line0 = Error().lineNumber;\n" +
+       "function f() {\n" +     // line0 + 1
+       "    return 2;\n" +      // line0 + 2
+       "}\n");
+var N = 4;
+var hits = 0;
+for (var i = 0; i < N; i++) {
+    var dbg = Debugger(g);
+    dbg.onDebuggerStatement = function (frame) {
+        var handler = {hit: function () { hits++; }};
+        var s = frame.eval("f").return.script;
+        var offs = s.getLineOffsets(g.line0 + 2);
+        for (var j = 0; j < offs.length; j++)
+            s.setBreakpoint(offs[j], handler);
+    };
+}
+g.eval('debugger;');
+gc({});
+assertEq(g.f(), 2);
+assertEq(hits, N);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/breakpoint-gc-05.js
@@ -0,0 +1,25 @@
+// Disabled debuggers keep breakpoint handlers alive.
+
+var g = newGlobal('new-compartment');
+g.eval("var line0 = Error().lineNumber;\n" +
+       "function f() {\n" +     // line0 + 1
+       "    return 2;\n" +      // line0 + 2
+       "}\n");
+var N = 4;
+var hits = 0;
+for (var i = 0; i < N; i++) {
+    var dbg = Debugger(g);
+    dbg.onDebuggerStatement = function (frame) {
+        var handler = {hit: function () { hits++; }};
+        var s = frame.eval("f").return.script;
+        var offs = s.getLineOffsets(g.line0 + 2);
+        for (var j = 0; j < offs.length; j++)
+            s.setBreakpoint(offs[j], handler);
+    };
+}
+g.eval('debugger;');
+dbg.enabled = false;
+gc({});
+dbg.enabled = true;
+assertEq(g.f(), 2);
+assertEq(hits, N);
copy from js/src/jit-test/tests/debug/onEnterFrame-05.js
copy to js/src/jit-test/tests/debug/onEnterFrame-04.js
--- a/js/src/jit-test/tests/debug/onEnterFrame-05.js
+++ b/js/src/jit-test/tests/debug/onEnterFrame-05.js
@@ -1,17 +1,15 @@
-// We detect and stop the runaway recursion caused by making onEnterFrame a wrapper of a debuggee function.
+// The tracejit does not prevent onEnterFrame from being called.
 
 var g = newGlobal('new-compartment');
-g.n = 0;
-g.eval("function f(frame) { n++; return 42; }");
-print('ok');
-var dbg = Debugger(g);
-dbg.onEnterFrame = g.f;
+g.eval("function f() { return 1; }\n");
+var N = g.N = RUNLOOP + 2;
+g.eval("function h() {\n" +
+       "    for (var i = 0; i < N; i += f()) {}\n" +
+       "}");
+g.h(); // record loop
 
-// Since enterFrame cannot throw, the InternalError is reported and execution proceeds.
-var x = g.f();
-assertEq(x, 42);
-assertEq(g.n > 20, true);
-
-// When an error is reported, the shell usually exits with a nonzero exit
-// code. If we get here, the test passed, so override that behavior.
-quit(0);
+var dbg = Debugger(g);
+var log = '';
+dbg.onEnterFrame = function (frame) { log += frame.callee.name; };
+g.h();
+assertEq(log, 'h' + Array(N + 1).join('f'));
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/onEnterFrame-06.js
@@ -0,0 +1,19 @@
+// The tracejit does not prevent onEnterFrame from being called after entering
+// a debuggee compartment from a non-debuggee compartment.
+
+var g1 = newGlobal('new-compartment');
+var g2 = newGlobal('new-compartment');
+var dbg = Debugger(g1, g2);
+dbg.removeDebuggee(g2); // turn off debug mode in g2
+
+g1.eval("function f() { return 1; }\n");
+var N = g1.N = RUNLOOP + 2;
+g1.eval("function h() {\n" +
+       "    for (var i = 0; i < N; i += f()) {}\n" +
+       "}");
+g1.h(); // record loop
+
+var log = '';
+dbg.onEnterFrame = function (frame) { log += frame.callee.name; };
+g1.h();
+assertEq(log, 'h' + Array(N + 1).join('f'));
--- a/js/src/jit-test/tests/debug/onExceptionUnwind-07.js
+++ b/js/src/jit-test/tests/debug/onExceptionUnwind-07.js
@@ -1,15 +1,15 @@
 // Unwinding due to uncatchable errors does not trigger onExceptionUnwind.
 
 var g = newGlobal('new-compartment');
 var dbg = Debugger(g);
 var hits = 0;
 dbg.onExceptionUnwind = function (frame, value) { hits = 'BAD'; };
 dbg.onDebuggerStatement = function (frame) {
     if (hits++ === 0)
-	assertEq(frame.eval("debugger;"), null);
+        assertEq(frame.eval("debugger;"), null);
     else
-	return null;
+        return null;
 }
 
 assertEq(g.eval("debugger; 2"), 2);
 assertEq(hits, 2);
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -365,8 +365,9 @@ MSG_DEF(JSMSG_DEBUG_OBJECT_WRONG_OWNER, 
 MSG_DEF(JSMSG_DEBUG_OBJECT_PROTO,     279, 0, JSEXN_TYPEERR, "Debugger.Object.prototype is not a valid Debugger.Object")
 MSG_DEF(JSMSG_DEBUG_LOOP,             280, 0, JSEXN_TYPEERR, "cannot debug an object in same compartment as debugger or a compartment that is already debugging the debugger")
 MSG_DEF(JSMSG_DEBUG_NOT_IDLE,         281, 0, JSEXN_ERR, "can't start debugging: a debuggee script is on the stack")
 MSG_DEF(JSMSG_DEBUG_BAD_OFFSET,       282, 0, JSEXN_TYPEERR, "invalid script offset")
 MSG_DEF(JSMSG_DEBUG_BAD_LINE,         283, 0, JSEXN_TYPEERR, "invalid line number")
 MSG_DEF(JSMSG_DEBUG_NOT_DEBUGGING,    284, 0, JSEXN_ERR, "can't set breakpoint: script global is not a debuggee")
 MSG_DEF(JSMSG_DEBUG_COMPARTMENT_MISMATCH, 285, 2, JSEXN_TYPEERR, "{0}: descriptor .{1} property is an object in a different compartment than the target object")
 MSG_DEF(JSMSG_NOT_CALLABLE_OR_UNDEFINED, 286, 0, JSEXN_TYPEERR, "value is not a function or undefined")
+MSG_DEF(JSMSG_DEBUG_NOT_SCRIPT_FRAME, 287, 0, JSEXN_ERR, "stack frame is not running JavaScript code")
--- a/js/src/jsapi-tests/testDebugger.cpp
+++ b/js/src/jsapi-tests/testDebugger.cpp
@@ -1,14 +1,15 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
  * vim: set ts=8 sw=4 et tw=99:
  */
 
 #include "tests.h"
 #include "jsdbgapi.h"
+#include "jscntxt.h"
 
 static int callCount[2] = {0, 0};
 
 static void *
 callCountHook(JSContext *cx, JSStackFrame *fp, JSBool before, JSBool *ok, void *closure)
 {
     callCount[before]++;
 
@@ -247,8 +248,47 @@ bool testIndirectEval(JSObject *scope, J
     }
 
     jsval hitsv;
     EVAL("hits", &hitsv);
     CHECK_SAME(hitsv, INT_TO_JSVAL(expectedHits));
     return true;
 }
 END_TEST(testDebugger_newScriptHook)
+
+BEGIN_TEST(testDebugger_singleStepThrow)
+    {
+        CHECK(JS_SetDebugModeForCompartment(cx, cx->compartment, true));
+        CHECK(JS_SetInterrupt(rt, onStep, NULL));
+
+        uint32 opts = JS_GetOptions(cx);
+        opts |= JSOPTION_METHODJIT | JSOPTION_METHODJIT_ALWAYS;
+        opts &= ~JSOPTION_JIT;
+        JS_SetOptions(cx, opts);
+
+        CHECK(JS_DefineFunction(cx, global, "setStepMode", setStepMode, 0, 0));
+        EXEC("var e;\n"
+             "setStepMode();\n"
+             "function f() { throw 0; }\n"
+             "try { f(); }\n"
+             "catch (x) { e = x; }\n");
+        return true;
+    }
+
+    static JSBool
+    setStepMode(JSContext *cx, uintN argc, jsval *vp)
+    {
+        JSStackFrame *fp = JS_GetScriptedCaller(cx, NULL);
+        JS_ASSERT(fp);
+        JSScript *script = JS_GetFrameScript(cx, fp);
+        JS_ASSERT(script);
+        if (!JS_SetSingleStepMode(cx, script, true))
+            return false;
+        JS_SET_RVAL(cx, vp, JSVAL_VOID);
+        return true;
+    }
+
+    static JSTrapStatus
+    onStep(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, void *closure)
+    {
+        return JSTRAP_CONTINUE;
+    }
+END_TEST(testDebugger_singleStepThrow)
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -4400,53 +4400,60 @@ JS_DefineFunctionById(JSContext *cx, JSO
                     uintN nargs, uintN attrs)
 {
     JS_THREADSAFE_ASSERT(cx->compartment != cx->runtime->atomsCompartment);
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj);
     return js_DefineFunction(cx, obj, id, Valueify(call), nargs, attrs);
 }
 
-inline static void
-LAST_FRAME_EXCEPTION_CHECK(JSContext *cx, bool result)
-{
-    if (!result && !cx->hasRunOption(JSOPTION_DONT_REPORT_UNCAUGHT))
-        js_ReportUncaughtException(cx);
-}
-
-inline static void
-LAST_FRAME_CHECKS(JSContext *cx, bool result)
-{
-    if (!JS_IsRunning(cx)) {
-        LAST_FRAME_EXCEPTION_CHECK(cx, result);
+struct AutoLastFrameCheck {
+    AutoLastFrameCheck(JSContext *cx JS_GUARD_OBJECT_NOTIFIER_PARAM)
+      : cx(cx) {
+        JS_ASSERT(cx);
+        JS_GUARD_OBJECT_NOTIFIER_INIT;
     }
-}
+
+    ~AutoLastFrameCheck() {
+        if (cx->isExceptionPending() &&
+            !JS_IsRunning(cx) &&
+            !cx->hasRunOption(JSOPTION_DONT_REPORT_UNCAUGHT)) {
+            js_ReportUncaughtException(cx);
+        }
+    }
+
+  private:
+    JSContext       *cx;
+    JS_DECL_USE_GUARD_OBJECT_NOTIFIER
+};
 
 inline static uint32
 JS_OPTIONS_TO_TCFLAGS(JSContext *cx)
 {
     return (cx->hasRunOption(JSOPTION_COMPILE_N_GO) ? TCF_COMPILE_N_GO : 0) |
            (cx->hasRunOption(JSOPTION_NO_SCRIPT_RVAL) ? TCF_NO_SCRIPT_RVAL : 0);
 }
 
 static JSObject *
 CompileUCScriptForPrincipalsCommon(JSContext *cx, JSObject *obj, JSPrincipals *principals,
                                       const jschar *chars, size_t length,
                                       const char *filename, uintN lineno, JSVersion version)
 {
     JS_THREADSAFE_ASSERT(cx->compartment != cx->runtime->atomsCompartment);
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj, principals);
+    AutoLastFrameCheck lfc(cx);
 
     uint32 tcflags = JS_OPTIONS_TO_TCFLAGS(cx) | TCF_NEED_MUTABLE_SCRIPT | TCF_NEED_SCRIPT_OBJECT;
     JSScript *script = Compiler::compileScript(cx, obj, NULL, principals, tcflags,
                                                chars, length, filename, lineno, version);
-    JS_ASSERT_IF(script, script->u.object);
-    LAST_FRAME_CHECKS(cx, script);
-    return script ? script->u.object : NULL;
+    if (!script)
+        return NULL;
+    JS_ASSERT(script->u.object);
+    return script->u.object;
 }
 
 extern JS_PUBLIC_API(JSObject *)
 JS_CompileUCScriptForPrincipalsVersion(JSContext *cx, JSObject *obj,
                                        JSPrincipals *principals,
                                        const jschar *chars, size_t length,
                                        const char *filename, uintN lineno,
                                        JSVersion version)
@@ -4625,53 +4632,48 @@ CompileFileHelper(JSContext *cx, JSObjec
     JS_ASSERT(script->u.object);
     return script->u.object;
 }
 
 JS_PUBLIC_API(JSObject *)
 JS_CompileFile(JSContext *cx, JSObject *obj, const char *filename)
 {
     JS_THREADSAFE_ASSERT(cx->compartment != cx->runtime->atomsCompartment);
-
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj);
-    JSObject *scriptObj = NULL;
-    do {
-        FILE *fp;
-        if (!filename || strcmp(filename, "-") == 0) {
-            fp = stdin;
-        } else {
-            fp = fopen(filename, "r");
-            if (!fp) {
-                JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_CANT_OPEN,
-                                     filename, "No such file or directory");
-                break;
-            }
+    AutoLastFrameCheck lfc(cx);
+
+    FILE *fp;
+    if (!filename || strcmp(filename, "-") == 0) {
+        fp = stdin;
+    } else {
+        fp = fopen(filename, "r");
+        if (!fp) {
+            JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_CANT_OPEN,
+                                 filename, "No such file or directory");
+            return NULL;
         }
-
-        scriptObj = CompileFileHelper(cx, obj, NULL, filename, fp);
-        if (fp != stdin)
-            fclose(fp);
-    } while (false);
-
-    LAST_FRAME_CHECKS(cx, scriptObj);
+    }
+
+    JSObject *scriptObj = CompileFileHelper(cx, obj, NULL, filename, fp);
+    if (fp != stdin)
+        fclose(fp);
     return scriptObj;
 }
 
 JS_PUBLIC_API(JSObject *)
 JS_CompileFileHandleForPrincipals(JSContext *cx, JSObject *obj, const char *filename,
                                   FILE *file, JSPrincipals *principals)
 {
     JS_THREADSAFE_ASSERT(cx->compartment != cx->runtime->atomsCompartment);
-
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj, principals);
-    JSObject *scriptObj = CompileFileHelper(cx, obj, principals, filename, file);
-    LAST_FRAME_CHECKS(cx, scriptObj);
-    return scriptObj;
+    AutoLastFrameCheck lfc(cx);
+
+    return CompileFileHelper(cx, obj, principals, filename, file);
 }
 
 JS_PUBLIC_API(JSObject *)
 JS_CompileFileHandleForPrincipalsVersion(JSContext *cx, JSObject *obj, const char *filename,
                                          FILE *file, JSPrincipals *principals, JSVersion version)
 {
     AutoVersionAPI ava(cx, version);
     return JS_CompileFileHandleForPrincipals(cx, obj, filename, file, principals);
@@ -4695,75 +4697,57 @@ JS_GetScriptFromObject(JSObject *scriptO
 static JSFunction *
 CompileUCFunctionForPrincipalsCommon(JSContext *cx, JSObject *obj,
                                      JSPrincipals *principals, const char *name,
                                      uintN nargs, const char **argnames,
                                      const jschar *chars, size_t length,
                                      const char *filename, uintN lineno, JSVersion version)
 {
     JS_THREADSAFE_ASSERT(cx->compartment != cx->runtime->atomsCompartment);
-    JSFunction *fun;
-    JSAtom *funAtom, *argAtom;
-    uintN i;
-
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj, principals);
+    AutoLastFrameCheck lfc(cx);
+
+    JSAtom *funAtom;
     if (!name) {
         funAtom = NULL;
     } else {
         funAtom = js_Atomize(cx, name, strlen(name));
-        if (!funAtom) {
-            fun = NULL;
-            goto out;
-        }
+        if (!funAtom)
+            return NULL;
     }
 
-    fun = js_NewFunction(cx, NULL, NULL, 0, JSFUN_INTERPRETED, obj, funAtom);
+    EmptyShape *emptyCallShape = EmptyShape::getEmptyCallShape(cx);
+    if (!emptyCallShape)
+        return NULL;
+
+    Bindings bindings(cx, emptyCallShape);
+    AutoBindingsRooter root(cx, bindings);
+    for (uintN i = 0; i < nargs; i++) {
+        uint16 dummy;
+        JSAtom *argAtom = js_Atomize(cx, argnames[i], strlen(argnames[i]));
+        if (!argAtom || !bindings.addArgument(cx, argAtom, &dummy))
+            return NULL;
+    }
+
+    JSFunction *fun = js_NewFunction(cx, NULL, NULL, 0, JSFUN_INTERPRETED, obj, funAtom);
     if (!fun)
-        goto out;
-
-    {
-        EmptyShape *emptyCallShape = EmptyShape::getEmptyCallShape(cx);
-        if (!emptyCallShape)
-            fun = NULL;
-        AutoShapeRooter shapeRoot(cx, emptyCallShape);
-
-        AutoObjectRooter tvr(cx, fun);
-
-        Bindings bindings(cx, emptyCallShape);
-        AutoBindingsRooter root(cx, bindings);
-        for (i = 0; i < nargs; i++) {
-            argAtom = js_Atomize(cx, argnames[i], strlen(argnames[i]));
-            if (!argAtom) {
-                fun = NULL;
-                goto out;
-            }
-
-            uint16 dummy;
-            if (!bindings.addArgument(cx, argAtom, &dummy)) {
-                fun = NULL;
-                goto out;
-            }
-        }
-
-        if (!Compiler::compileFunctionBody(cx, fun, principals, &bindings,
-                                           chars, length, filename, lineno, version)) {
-            fun = NULL;
-            goto out;
-        }
-
-        if (obj && funAtom &&
-            !obj->defineProperty(cx, ATOM_TO_JSID(funAtom), ObjectValue(*fun),
-                                 NULL, NULL, JSPROP_ENUMERATE)) {
-            fun = NULL;
-        }
+        return NULL;
+
+    if (!Compiler::compileFunctionBody(cx, fun, principals, &bindings,
+                                       chars, length, filename, lineno, version)) {
+        return NULL;
     }
-
-  out:
-    LAST_FRAME_CHECKS(cx, fun);
+    
+    if (obj && funAtom &&
+        !obj->defineProperty(cx, ATOM_TO_JSID(funAtom), ObjectValue(*fun),
+                             NULL, NULL, JSPROP_ENUMERATE)) {
+        return NULL;
+    }
+
     return fun;
 }
 
 JS_PUBLIC_API(JSFunction *)
 JS_CompileUCFunctionForPrincipalsVersion(JSContext *cx, JSObject *obj,
                                          JSPrincipals *principals, const char *name,
                                          uintN nargs, const char **argnames,
                                          const jschar *chars, size_t length,
@@ -4881,23 +4865,21 @@ JS_DecompileFunctionBody(JSContext *cx, 
                                 !(indent & JS_DONT_PRETTY_PRINT),
                                 false, false, js_DecompileFunctionBody);
 }
 
 JS_PUBLIC_API(JSBool)
 JS_ExecuteScript(JSContext *cx, JSObject *obj, JSObject *scriptObj, jsval *rval)
 {
     JS_THREADSAFE_ASSERT(cx->compartment != cx->runtime->atomsCompartment);
-
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj, scriptObj);
-
-    JSBool ok = Execute(cx, scriptObj->getScript(), *obj, Valueify(rval));
-    LAST_FRAME_CHECKS(cx, ok);
-    return ok;
+    AutoLastFrameCheck lfc(cx);
+
+    return Execute(cx, scriptObj->getScript(), *obj, Valueify(rval));
 }
 
 JS_PUBLIC_API(JSBool)
 JS_ExecuteScriptVersion(JSContext *cx, JSObject *obj, JSObject *scriptObj, jsval *rval,
                         JSVersion version)
 {
     AutoVersionAPI ava(cx, version);
     return JS_ExecuteScript(cx, obj, scriptObj, rval);
@@ -4906,31 +4888,29 @@ JS_ExecuteScriptVersion(JSContext *cx, J
 bool
 EvaluateUCScriptForPrincipalsCommon(JSContext *cx, JSObject *obj,
                                     JSPrincipals *principals,
                                     const jschar *chars, uintN length,
                                     const char *filename, uintN lineno,
                                     jsval *rval, JSVersion compileVersion)
 {
     JS_THREADSAFE_ASSERT(cx->compartment != cx->runtime->atomsCompartment);
-
     CHECK_REQUEST(cx);
+    AutoLastFrameCheck lfc(cx);
+
     JSScript *script = Compiler::compileScript(cx, obj, NULL, principals,
                                                !rval
                                                ? TCF_COMPILE_N_GO | TCF_NO_SCRIPT_RVAL
                                                : TCF_COMPILE_N_GO,
                                                chars, length, filename, lineno, compileVersion);
-    if (!script) {
-        LAST_FRAME_CHECKS(cx, script);
+    if (!script)
         return false;
-    }
     JS_ASSERT(script->getVersion() == compileVersion);
 
     bool ok = Execute(cx, script, *obj, Valueify(rval));
-    LAST_FRAME_CHECKS(cx, ok);
     js_DestroyScript(cx, script, 5);
     return ok;
 
 }
 
 JS_PUBLIC_API(JSBool)
 JS_EvaluateUCScriptForPrincipalsVersion(JSContext *cx, JSObject *obj,
                                         JSPrincipals *principals,
@@ -4999,109 +4979,101 @@ JS_EvaluateScript(JSContext *cx, JSObjec
 
 JS_PUBLIC_API(JSBool)
 JS_CallFunction(JSContext *cx, JSObject *obj, JSFunction *fun, uintN argc, jsval *argv,
                 jsval *rval)
 {
     JS_THREADSAFE_ASSERT(cx->compartment != cx->runtime->atomsCompartment);
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj, fun, JSValueArray(argv, argc));
-    JSBool ok = Invoke(cx, ObjectOrNullValue(obj), ObjectValue(*fun), argc, Valueify(argv),
-                       Valueify(rval));
-    LAST_FRAME_CHECKS(cx, ok);
-    return ok;
+    AutoLastFrameCheck lfc(cx);
+
+    return Invoke(cx, ObjectOrNullValue(obj), ObjectValue(*fun), argc, Valueify(argv),
+                  Valueify(rval));
 }
 
 JS_PUBLIC_API(JSBool)
 JS_CallFunctionName(JSContext *cx, JSObject *obj, const char *name, uintN argc, jsval *argv,
                     jsval *rval)
 {
     JS_THREADSAFE_ASSERT(cx->compartment != cx->runtime->atomsCompartment);
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj, JSValueArray(argv, argc));
-
-    AutoValueRooter tvr(cx);
+    AutoLastFrameCheck lfc(cx);
+
+    Value v;
     JSAtom *atom = js_Atomize(cx, name, strlen(name));
-    JSBool ok =
-        atom &&
-        js_GetMethod(cx, obj, ATOM_TO_JSID(atom), JSGET_NO_METHOD_BARRIER, tvr.addr()) &&
-        Invoke(cx, ObjectOrNullValue(obj), tvr.value(), argc, Valueify(argv), Valueify(rval));
-    LAST_FRAME_CHECKS(cx, ok);
-    return ok;
+    return atom &&
+           js_GetMethod(cx, obj, ATOM_TO_JSID(atom), JSGET_NO_METHOD_BARRIER, &v) &&
+           Invoke(cx, ObjectOrNullValue(obj), v, argc, Valueify(argv), Valueify(rval));
 }
 
 JS_PUBLIC_API(JSBool)
 JS_CallFunctionValue(JSContext *cx, JSObject *obj, jsval fval, uintN argc, jsval *argv,
                      jsval *rval)
 {
     JS_THREADSAFE_ASSERT(cx->compartment != cx->runtime->atomsCompartment);
-
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj, fval, JSValueArray(argv, argc));
-    JSBool ok = Invoke(cx, ObjectOrNullValue(obj), Valueify(fval), argc, Valueify(argv),
-                       Valueify(rval));
-    LAST_FRAME_CHECKS(cx, ok);
-    return ok;
+    AutoLastFrameCheck lfc(cx);
+
+    return Invoke(cx, ObjectOrNullValue(obj), Valueify(fval), argc, Valueify(argv),
+                  Valueify(rval));
 }
 
 namespace JS {
 
 JS_PUBLIC_API(bool)
 Call(JSContext *cx, jsval thisv, jsval fval, uintN argc, jsval *argv, jsval *rval)
 {
-    JSBool ok;
-
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, thisv, fval, JSValueArray(argv, argc));
-    ok = Invoke(cx, Valueify(thisv), Valueify(fval), argc, Valueify(argv), Valueify(rval));
-    LAST_FRAME_CHECKS(cx, ok);
-    return ok;
+    AutoLastFrameCheck lfc(cx);
+
+    return Invoke(cx, Valueify(thisv), Valueify(fval), argc, Valueify(argv), Valueify(rval));
 }
 
 } // namespace JS
 
 JS_PUBLIC_API(JSObject *)
 JS_New(JSContext *cx, JSObject *ctor, uintN argc, jsval *argv)
 {
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, ctor, JSValueArray(argv, argc));
+    AutoLastFrameCheck lfc(cx);
 
     // This is not a simple variation of JS_CallFunctionValue because JSOP_NEW
     // is not a simple variation of JSOP_CALL. We have to determine what class
     // of object to create, create it, and clamp the return value to an object,
     // among other details. InvokeConstructor does the hard work.
     InvokeArgsGuard args;
     if (!cx->stack.pushInvokeArgs(cx, argc, &args))
         return NULL;
 
     args.calleev().setObject(*ctor);
     args.thisv().setNull();
     memcpy(args.argv(), argv, argc * sizeof(jsval));
 
-    bool ok = InvokeConstructor(cx, args);
-
-    JSObject *obj = NULL;
-    if (ok) {
-        if (args.rval().isObject()) {
-            obj = &args.rval().toObject();
-        } else {
-            /*
-             * Although constructors may return primitives (via proxies), this
-             * API is asking for an object, so we report an error.
-             */
-            JSAutoByteString bytes;
-            if (js_ValueToPrintable(cx, args.rval(), &bytes)) {
-                JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_BAD_NEW_RESULT,
-                                     bytes.ptr());
-            }
+    if (!InvokeConstructor(cx, args))
+        return NULL;
+
+    if (!args.rval().isObject()) {
+        /*
+         * Although constructors may return primitives (via proxies), this
+         * API is asking for an object, so we report an error.
+         */
+        JSAutoByteString bytes;
+        if (js_ValueToPrintable(cx, args.rval(), &bytes)) {
+            JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_BAD_NEW_RESULT,
+                                 bytes.ptr());
         }
+        return NULL;
     }
 
-    LAST_FRAME_CHECKS(cx, ok);
-    return obj;
+    return &args.rval().toObject();
 }
 
 JS_PUBLIC_API(JSOperationCallback)
 JS_SetOperationCallback(JSContext *cx, JSOperationCallback callback)
 {
 #ifdef JS_THREADSAFE
     JS_ASSERT(CURRENT_THREAD_IS_ME(cx->thread()));
 #endif
--- a/js/src/jscntxt.cpp
+++ b/js/src/jscntxt.cpp
@@ -106,17 +106,18 @@ ThreadData::ThreadData()
     onTraceCompartment(NULL),
     recordingCompartment(NULL),
     profilingCompartment(NULL),
     maxCodeCacheBytes(DEFAULT_JIT_CACHE_SIZE),
 #endif
     waiveGCQuota(false),
     dtoaState(NULL),
     nativeStackBase(GetNativeStackBase()),
-    pendingProxyOperation(NULL)
+    pendingProxyOperation(NULL),
+    interpreterFrames(NULL)
 {
 }
 
 ThreadData::~ThreadData()
 {
     if (dtoaState)
         js_DestroyDtoaState(dtoaState);
 }
@@ -565,17 +566,17 @@ js_DestroyContext(JSContext *cx, JSDestr
 JSContext *
 js_ContextIterator(JSRuntime *rt, JSBool unlocked, JSContext **iterp)
 {
     JSContext *cx = *iterp;
 
     Maybe<AutoLockGC> lockIf;
     if (unlocked)
         lockIf.construct(rt);
-    cx = js_ContextFromLinkField(cx ? cx->link.next : rt->contextList.next);
+    cx = JSContext::fromLinkField(cx ? cx->link.next : rt->contextList.next);
     if (&cx->link == &rt->contextList)
         cx = NULL;
     *iterp = cx;
     return cx;
 }
 
 JS_FRIEND_API(JSContext *)
 js_NextActiveContext(JSRuntime *rt, JSContext *cx)
@@ -1382,19 +1383,19 @@ JSContext::resetCompartment()
          * can only fail due to bugs in the engine or embedding.)
          */
         OBJ_TO_INNER_OBJECT(this, scopeobj);
         if (!scopeobj)
             goto error;
     }
 
     compartment = scopeobj->compartment();
-
     if (isExceptionPending())
         wrapPendingException();
+    updateJITEnabled();
     return;
 
 error:
 
     /*
      * If we try to use the context without a selected compartment,
      * we will crash.
      */
@@ -1575,16 +1576,18 @@ IsJITBrokenHere()
 #endif
 
 void
 JSContext::updateJITEnabled()
 {
 #ifdef JS_TRACER
     traceJitEnabled = ((runOptions & JSOPTION_JIT) &&
                        !IsJITBrokenHere() &&
+                       compartment &&
+                       !compartment->debugMode() &&
                        (debugHooks == &js_NullDebugHooks ||
                         (debugHooks == &runtime->globalDebugHooks &&
                          !runtime->debuggerInhibitsJIT())));
 #endif
 #ifdef JS_METHODJIT
     methodJitEnabled = (runOptions & JSOPTION_METHODJIT) &&
                        !IsJITBrokenHere()
 # if defined JS_CPU_X86 || defined JS_CPU_X64
--- a/js/src/jscntxt.h
+++ b/js/src/jscntxt.h
@@ -221,16 +221,22 @@ struct ThreadData {
         gsnCache.purge();
 
         /* FIXME: bug 506341. */
         propertyCache.purge(cx);
     }
 
     /* This must be called with the GC lock held. */
     void triggerOperationCallback(JSRuntime *rt);
+
+    /*
+     * Frames currently running in js::Interpret. See InterpreterFrames for
+     * details.
+     */
+    InterpreterFrames *interpreterFrames;
 };
 
 } /* namespace js */
 
 #ifdef JS_THREADSAFE
 
 /*
  * Structure uniquely representing a thread.  It holds thread-private data
@@ -1139,19 +1145,16 @@ struct JSContext
 
     void setThread(JSThread *thread);
     static const size_t threadOffset() { return offsetof(JSContext, thread_); }
 
     unsigned            outstandingRequests;/* number of JS_BeginRequest calls
                                                without the corresponding
                                                JS_EndRequest. */
     JSCList             threadLinks;        /* JSThread contextList linkage */
-
-#define CX_FROM_THREAD_LINKS(tl) \
-    ((JSContext *)((char *)(tl) - offsetof(JSContext, threadLinks)))
 #endif
 
     /* Stack of thread-stack-allocated GC roots. */
     js::AutoGCRooter   *autoGCRooters;
 
     /* Debug hooks associated with the current context. */
     const JSDebugHooks  *debugHooks;
 
@@ -1320,16 +1323,28 @@ struct JSContext
 #endif
 
     /*
      * See JS_SetTrustedPrincipals in jsapi.h.
      * Note: !cx->compartment is treated as trusted.
      */
     bool runningWithTrustedPrincipals() const;
 
+    static inline JSContext *fromLinkField(JSCList *link) {
+        JS_ASSERT(link);
+        return reinterpret_cast<JSContext *>(uintptr_t(link) - offsetof(JSContext, link));
+    }
+
+#ifdef JS_THREADSAFE
+    static inline JSContext *fromThreadLinks(JSCList *link) {
+        JS_ASSERT(link);
+        return reinterpret_cast<JSContext *>(uintptr_t(link) - offsetof(JSContext, threadLinks));
+    }
+#endif
+
   private:
     /*
      * The allocation code calls the function to indicate either OOM failure
      * when p is null or that a memory pressure counter has reached some
      * threshold when p is not null. The function takes the pointer and not
      * a boolean flag to minimize the amount of code in its inlined callers.
      */
     JS_FRIEND_API(void) checkMallocGCPressure(void *p);
@@ -1462,17 +1477,18 @@ class AutoGCRooter {
         XML =          -9, /* js::AutoXMLRooter */
         OBJECT =      -10, /* js::AutoObjectRooter */
         ID =          -11, /* js::AutoIdRooter */
         VALVECTOR =   -12, /* js::AutoValueVector */
         DESCRIPTOR =  -13, /* js::AutoPropertyDescriptorRooter */
         STRING =      -14, /* js::AutoStringRooter */
         IDVECTOR =    -15, /* js::AutoIdVector */
         BINDINGS =    -16, /* js::Bindings */
-        SHAPEVECTOR = -17  /* js::AutoShapeVector */
+        SHAPEVECTOR = -17, /* js::AutoShapeVector */
+        OBJVECTOR =   -18  /* js::AutoObjectVector */
     };
 
     private:
     /* No copy or assignment semantics. */
     AutoGCRooter(AutoGCRooter &ida);
     void operator=(AutoGCRooter &ida);
 };
 
@@ -2132,35 +2148,58 @@ class ThreadDataIter
     ThreadData *threadData() const {
         JS_ASSERT(!done);
         return &runtime->threadData;
     }
 };
 
 #endif  /* !JS_THREADSAFE */
 
+/*
+ * Enumerate all contexts in a runtime that are in the same thread as a given
+ * context.
+ */
+class ThreadContextRange {
+    JSCList *begin;
+    JSCList *end;
+
+public:
+    explicit ThreadContextRange(JSContext *cx) {
+#ifdef JS_THREADSAFE
+        end = &cx->thread()->contextList;
+#else
+        end = &cx->runtime->contextList;
+#endif
+        begin = end->next;
+    }
+
+    bool empty() const { return begin == end; }
+    void popFront() { JS_ASSERT(!empty()); begin = begin->next; }
+
+    JSContext *front() const {
+#ifdef JS_THREADSAFE
+        return JSContext::fromThreadLinks(begin);
+#else
+        return JSContext::fromLinkField(begin);
+#endif
+    }
+};
+
 } /* namespace js */
 
 /*
  * Create and destroy functions for JSContext, which is manually allocated
  * and exclusively owned.
  */
 extern JSContext *
 js_NewContext(JSRuntime *rt, size_t stackChunkSize);
 
 extern void
 js_DestroyContext(JSContext *cx, JSDestroyContextMode mode);
 
-static JS_INLINE JSContext *
-js_ContextFromLinkField(JSCList *link)
-{
-    JS_ASSERT(link);
-    return reinterpret_cast<JSContext *>(uintptr_t(link) - offsetof(JSContext, link));
-}
-
 /*
  * If unlocked, acquire and release rt->gcLock around *iterp update; otherwise
  * the caller must be holding rt->gcLock.
  */
 extern JSContext *
 js_ContextIterator(JSRuntime *rt, JSBool unlocked, JSContext **iterp);
 
 /*
@@ -2444,16 +2483,29 @@ class AutoValueVector : public AutoVecto
     jsval *jsval_begin() { return Jsvalify(begin()); }
 
     const jsval *jsval_end() const { return Jsvalify(end()); }
     jsval *jsval_end() { return Jsvalify(end()); }
 
     JS_DECL_USE_GUARD_OBJECT_NOTIFIER
 };
 
+class AutoObjectVector : public AutoVectorRooter<JSObject *>
+{
+  public:
+    explicit AutoObjectVector(JSContext *cx
+                              JS_GUARD_OBJECT_NOTIFIER_PARAM)
+        : AutoVectorRooter<JSObject *>(cx, OBJVECTOR)
+    {
+        JS_GUARD_OBJECT_NOTIFIER_INIT;
+    }
+
+    JS_DECL_USE_GUARD_OBJECT_NOTIFIER
+};
+
 class AutoIdVector : public AutoVectorRooter<jsid>
 {
   public:
     explicit AutoIdVector(JSContext *cx
                           JS_GUARD_OBJECT_NOTIFIER_PARAM)
         : AutoVectorRooter<jsid>(cx, IDVECTOR)
     {
         JS_GUARD_OBJECT_NOTIFIER_INIT;
--- a/js/src/jscompartment.cpp
+++ b/js/src/jscompartment.cpp
@@ -662,24 +662,30 @@ JSCompartment::setDebugModeFromC(JSConte
         if (b && onStack) {
             JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_NOT_IDLE);
             return false;
         }
     }
 
     debugModeBits = (debugModeBits & ~uintN(DebugFromC)) | (b ? DebugFromC : 0);
     JS_ASSERT(debugMode() == enabledAfter);
-    if (enabledBefore != enabledAfter && !onStack)
+    if (enabledBefore != enabledAfter)
         updateForDebugMode(cx);
     return true;
 }
 
 void
 JSCompartment::updateForDebugMode(JSContext *cx)
 {
+    for (ThreadContextRange r(cx); !r.empty(); r.popFront()) {
+        JSContext *cx = r.front();
+        if (cx->compartment == this) 
+            cx->updateJITEnabled();
+    }
+
 #ifdef JS_METHODJIT
     bool enabled = debugMode();
 
     if (enabled) {
         JS_ASSERT(!hasScriptsOnStack(cx));
     } else if (hasScriptsOnStack(cx)) {
         hasDebugModeCodeToDrop = true;
         return;
@@ -793,55 +799,35 @@ JSCompartment::clearTraps(JSContext *cx,
     for (BreakpointSiteMap::Enum e(breakpointSites); !e.empty(); e.popFront()) {
         BreakpointSite *site = e.front().value;
         if (!script || site->script == script)
             site->clearTrap(cx, &e);
     }
 }
 
 bool
-JSCompartment::markBreakpointsIteratively(JSTracer *trc)
+JSCompartment::markTrapClosuresIteratively(JSTracer *trc)
 {
     bool markedAny = false;
     JSContext *cx = trc->context;
     for (BreakpointSiteMap::Range r = breakpointSites.all(); !r.empty(); r.popFront()) {
         BreakpointSite *site = r.front().value;
 
         // Mark jsdbgapi state if any. But if we know the scriptObject, put off
         // marking trap state until we know the scriptObject is live.
         if (site->trapHandler &&
-            (!site->scriptObject || IsAboutToBeFinalized(cx, site->scriptObject)))
+            (!site->scriptObject || !IsAboutToBeFinalized(cx, site->scriptObject)))
         {
             if (site->trapClosure.isMarkable() &&
                 IsAboutToBeFinalized(cx, site->trapClosure.toGCThing()))
             {
                 markedAny = true;
             }
             MarkValue(trc, site->trapClosure, "trap closure");
         }
-
-        // Mark js::Debugger breakpoints. If either the debugger or the script is
-        // collected, then the breakpoint is collected along with it. So do not
-        // mark the handler in that case.
-        //
-        // If scriptObject is non-null, examine it to see if the script will be
-        // collected. If scriptObject is null, then site->script is an eval
-        // script on the stack, so it is definitely live.
-        //
-        if (!site->scriptObject || !IsAboutToBeFinalized(cx, site->scriptObject)) {
-            for (Breakpoint *bp = site->firstBreakpoint(); bp; bp = bp->nextInSite()) {
-                if (!IsAboutToBeFinalized(cx, bp->debugger->toJSObject()) &&
-                    bp->handler &&
-                    IsAboutToBeFinalized(cx, bp->handler))
-                {
-                    MarkObject(trc, *bp->handler, "breakpoint handler");
-                    markedAny = true;
-                }
-            }
-        }
     }
     return markedAny;
 }
 
 void
 JSCompartment::sweepBreakpoints(JSContext *cx)
 {
     for (BreakpointSiteMap::Enum e(breakpointSites); !e.empty(); e.popFront()) {
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -606,17 +606,17 @@ struct JS_FRIEND_API(JSCompartment) {
                         js::GlobalObjectSet::Enum *debuggeesEnum = NULL);
     bool setDebugModeFromC(JSContext *cx, bool b);
 
     js::BreakpointSite *getBreakpointSite(jsbytecode *pc);
     js::BreakpointSite *getOrCreateBreakpointSite(JSContext *cx, JSScript *script, jsbytecode *pc,
                                                   JSObject *scriptObject);
     void clearBreakpointsIn(JSContext *cx, js::Debugger *dbg, JSScript *script, JSObject *handler);
     void clearTraps(JSContext *cx, JSScript *script);
-    bool markBreakpointsIteratively(JSTracer *trc);
+    bool markTrapClosuresIteratively(JSTracer *trc);
 
   private:
     void sweepBreakpoints(JSContext *cx);
 
   public:
     js::WatchpointMap *watchpointMap;
 };
 
--- a/js/src/jsdbgapi.cpp
+++ b/js/src/jsdbgapi.cpp
@@ -76,19 +76,16 @@
 #include "jsinterpinlines.h"
 #include "jsscopeinlines.h"
 #include "jsscriptinlines.h"
 
 #include "vm/Stack-inl.h"
 
 #include "jsautooplen.h"
 
-#include "methodjit/MethodJIT.h"
-#include "methodjit/Retcon.h"
-
 #ifdef __APPLE__
 #include "sharkctl.h"
 #endif
 
 using namespace js;
 using namespace js::gc;
 
 JS_PUBLIC_API(JSBool)
@@ -149,44 +146,16 @@ ScriptDebugEpilogue(JSContext *cx, Stack
 } /* namespace js */
 
 JS_FRIEND_API(JSBool)
 JS_SetDebugModeForCompartment(JSContext *cx, JSCompartment *comp, JSBool debug)
 {
     return comp->setDebugModeFromC(cx, !!debug);
 }
 
-JS_FRIEND_API(JSBool)
-js_SetSingleStepMode(JSContext *cx, JSScript *script, JSBool singleStep)
-{
-    assertSameCompartment(cx, script);
-
-#ifdef JS_METHODJIT
-    if (!script->singleStepMode == !singleStep)
-        return JS_TRUE;
-#endif
-
-    JS_ASSERT_IF(singleStep, cx->compartment->debugMode());
-
-#ifdef JS_METHODJIT
-    /* request the next recompile to inject single step interrupts */
-    script->singleStepMode = !!singleStep;
-
-    js::mjit::JITScript *jit = script->jitNormal ? script->jitNormal : script->jitCtor;
-    if (jit && script->singleStepMode != jit->singleStepMode) {
-        js::mjit::Recompiler recompiler(cx, script);
-        if (!recompiler.recompile()) {
-            script->singleStepMode = !singleStep;
-            return JS_FALSE;
-        }
-    }
-#endif
-    return JS_TRUE;
-}
-
 static JSBool
 CheckDebugMode(JSContext *cx)
 {
     JSBool debugMode = JS_GetDebugMode(cx);
     /*
      * :TODO:
      * This probably should be an assertion, since it's indicative of a severe
      * API misuse.
@@ -200,17 +169,17 @@ CheckDebugMode(JSContext *cx)
 
 JS_PUBLIC_API(JSBool)
 JS_SetSingleStepMode(JSContext *cx, JSScript *script, JSBool singleStep)
 {
     assertSameCompartment(cx, script);
     if (!CheckDebugMode(cx))
         return JS_FALSE;
 
-    return js_SetSingleStepMode(cx, script, singleStep);
+    return script->setStepModeFlag(cx, singleStep);
 }
 
 jsbytecode *
 js_UntrapScriptCode(JSContext *cx, JSScript *script)
 {
     jsbytecode *code = script->code;
     BreakpointSiteMap &sites = script->compartment->breakpointSites;
     for (BreakpointSiteMap::Range r = sites.all(); !r.empty(); r.popFront()) {
@@ -284,21 +253,21 @@ JS_ClearAllTrapsForCompartment(JSContext
 
 #ifdef JS_TRACER
 static void
 JITInhibitingHookChange(JSRuntime *rt, bool wasInhibited)
 {
     if (wasInhibited) {
         if (!rt->debuggerInhibitsJIT()) {
             for (JSCList *cl = rt->contextList.next; cl != &rt->contextList; cl = cl->next)
-                js_ContextFromLinkField(cl)->updateJITEnabled();
+                JSContext::fromLinkField(cl)->updateJITEnabled();
         }
     } else if (rt->debuggerInhibitsJIT()) {
         for (JSCList *cl = rt->contextList.next; cl != &rt->contextList; cl = cl->next)
-            js_ContextFromLinkField(cl)->traceJitEnabled = false;
+            JSContext::fromLinkField(cl)->traceJitEnabled = false;
     }
 }
 #endif
 
 JS_PUBLIC_API(JSBool)
 JS_SetInterrupt(JSRuntime *rt, JSInterruptHook hook, void *closure)
 {
 #ifdef JS_TRACER
--- a/js/src/jsdbgapi.h
+++ b/js/src/jsdbgapi.h
@@ -126,20 +126,16 @@ JS_FRIEND_API(JSBool)
 JS_SetDebugModeForCompartment(JSContext *cx, JSCompartment *comp, JSBool debug);
 
 /*
  * Turn on/off debugging mode for a context's compartment.
  */
 JS_FRIEND_API(JSBool)
 JS_SetDebugMode(JSContext *cx, JSBool debug);
 
-/* Turn on single step mode. Requires debug mode. */
-extern JS_FRIEND_API(JSBool)
-js_SetSingleStepMode(JSContext *cx, JSScript *script, JSBool singleStep);
-
 /* Turn on single step mode. */
 extern JS_PUBLIC_API(JSBool)
 JS_SetSingleStepMode(JSContext *cx, JSScript *script, JSBool singleStep);
 
 /*
  * Unexported library-private helper used to unpatch all traps in a script.
  * Returns script->code if script has no traps, else a JS_malloc'ed copy of
  * script->code which the caller must JS_free, or null on JS_malloc OOM.
--- a/js/src/jsfun.cpp
+++ b/js/src/jsfun.cpp
@@ -2133,35 +2133,19 @@ Function(JSContext *cx, uintN argc, Valu
 
     /* Block this call if security callbacks forbid it. */
     GlobalObject *global = call.callee().getGlobal();
     if (!global->isRuntimeCodeGenEnabled(cx)) {
         JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_CSP_BLOCKED_FUNCTION);
         return false;
     }
 
-    JS::Anchor<JSObject *> obj(NewFunction(cx, *global));
-    if (!obj.get())
-        return false;
-
-    /*
-     * NB: (new Function) is not lexically closed by its caller, it's just an
-     * anonymous function in the top-level scope that its constructor inhabits.
-     * Thus 'var x = 42; f = new Function("return x"); print(f())' prints 42,
-     * and so would a call to f from another top-level's script or function.
-     */
-    JSFunction *fun = js_NewFunction(cx, obj.get(), NULL, 0, JSFUN_LAMBDA | JSFUN_INTERPRETED,
-                                     global, cx->runtime->atomState.anonymousAtom);
-    if (!fun)
-        return false;
-
     EmptyShape *emptyCallShape = EmptyShape::getEmptyCallShape(cx);
     if (!emptyCallShape)
         return false;
-    AutoShapeRooter shapeRoot(cx, emptyCallShape);
 
     Bindings bindings(cx, emptyCallShape);
     AutoBindingsRooter root(cx, bindings);
 
     uintN lineno;
     const char *filename = CurrentScriptFileAndLine(cx, &lineno);
 
     Value *argv = call.argv();
@@ -2299,21 +2283,32 @@ Function(JSContext *cx, uintN argc, Valu
         strAnchor.set(str);
         chars = str->getChars(cx);
         length = str->length();
     } else {
         chars = cx->runtime->emptyString->chars();
         length = 0;
     }
 
+    /*
+     * NB: (new Function) is not lexically closed by its caller, it's just an
+     * anonymous function in the top-level scope that its constructor inhabits.
+     * Thus 'var x = 42; f = new Function("return x"); print(f())' prints 42,
+     * and so would a call to f from another top-level's script or function.
+     */
+    JSFunction *fun = js_NewFunction(cx, NULL, NULL, 0, JSFUN_LAMBDA | JSFUN_INTERPRETED,
+                                     global, cx->runtime->atomState.anonymousAtom);
+    if (!fun)
+        return false;
+
     JSPrincipals *principals = PrincipalsForCompiledCode(call, cx);
     bool ok = Compiler::compileFunctionBody(cx, fun, principals, &bindings,
                                             chars, length, filename, lineno,
                                             cx->findVersion());
-    call.rval().setObject(obj);
+    call.rval().setObject(*fun);
     return ok;
 }
 
 namespace js {
 
 bool
 IsBuiltinFunctionConstructor(JSFunction *fun)
 {
@@ -2404,17 +2399,17 @@ js_NewFunction(JSContext *cx, JSObject *
         JS_ASSERT(funobj->isFunction());
         funobj->setParent(parent);
     } else {
         funobj = NewFunction(cx, parent);
         if (!funobj)
             return NULL;
     }
     JS_ASSERT(!funobj->getPrivate());
-    fun = (JSFunction *) funobj;
+    fun = static_cast<JSFunction *>(funobj);
 
     /* Initialize all function members. */
     fun->nargs = uint16(nargs);
     fun->flags = flags & (JSFUN_FLAGS_MASK | JSFUN_KINDMASK | JSFUN_TRCINFO);
     if ((flags & JSFUN_KINDMASK) >= JSFUN_INTERPRETED) {
         JS_ASSERT(!native);
         JS_ASSERT(nargs == 0);
         fun->u.i.skipmin = 0;
--- a/js/src/jsgc.cpp
+++ b/js/src/jsgc.cpp
@@ -1779,16 +1779,22 @@ AutoGCRooter::trace(JSTracer *trc)
         MarkShapeRange(trc, vector.length(), vector.begin(), "js::AutoShapeVector.vector");
         return;
       }
 
       case BINDINGS: {
         static_cast<js::AutoBindingsRooter *>(this)->bindings.trace(trc);
         return;
       }
+
+      case OBJVECTOR: {
+        AutoObjectVector::VectorImpl &vector = static_cast<AutoObjectVector *>(this)->vector;
+        MarkObjectRange(trc, vector.length(), vector.begin(), "js::AutoObjectVector.vector");
+        return;
+      }
     }
 
     JS_ASSERT(tag >= 0);
     MarkValueRange(trc, tag, static_cast<AutoArrayRooter *>(this)->array, "js::AutoArrayRooter.array");
 }
 
 namespace js {
 
--- a/js/src/jsinterp.cpp
+++ b/js/src/jsinterp.cpp
@@ -1513,16 +1513,40 @@ CanIncDecWithoutOverflow(int32_t i)
     (__IBMC__ >= 700 && defined __IBM_COMPUTED_GOTO) ||                       \
     __SUNPRO_C >= 0x570)
 #  define JS_THREADED_INTERP 1
 # else
 #  define JS_THREADED_INTERP 0
 # endif
 #endif
 
+template<typename T>
+class GenericInterruptEnabler : public InterpreterFrames::InterruptEnablerBase {
+  public:
+    GenericInterruptEnabler(T *variable, T value) : variable(variable), value(value) { }
+    void enableInterrupts() const { *variable = value; }
+
+  private:
+    T *variable;
+    T value;
+};
+
+inline InterpreterFrames::InterpreterFrames(JSContext *cx, FrameRegs *regs, 
+                                            const InterruptEnablerBase &enabler)
+  : context(cx), regs(regs), enabler(enabler)
+{
+    older = JS_THREAD_DATA(cx)->interpreterFrames;
+    JS_THREAD_DATA(cx)->interpreterFrames = this;
+}
+ 
+inline InterpreterFrames::~InterpreterFrames()
+{
+    JS_THREAD_DATA(context)->interpreterFrames = older;
+}
+
 /*
  * Deadlocks or else bad races are likely if JS_THREADSAFE, so we must rely on
  * single-thread DEBUG js shell testing to verify property cache hits.
  */
 #if defined DEBUG && !defined JS_THREADSAFE
 
 # define ASSERT_VALID_PROPERTY_CACHE_HIT(pcoff,obj,pobj,entry)                \
     JS_BEGIN_MACRO                                                            \
@@ -1700,17 +1724,18 @@ Interpret(JSContext *cx, StackFrame *ent
 # define OPDEF(op,val,name,token,length,nuses,ndefs,prec,format)              \
         JS_EXTENSION &&interrupt,
 # include "jsopcode.tbl"
 # undef OPDEF
     };
 
     register void * const *jumpTable = normalJumpTable;
 
-# define ENABLE_INTERRUPTS() ((void) (jumpTable = interruptJumpTable))
+    typedef GenericInterruptEnabler<void * const *> InterruptEnabler;
+    InterruptEnabler interruptEnabler(&jumpTable, interruptJumpTable);
 
 # ifdef JS_TRACER
 #  define CHECK_RECORDER()                                                    \
     JS_ASSERT_IF(TRACE_RECORDER(cx), jumpTable == interruptJumpTable)
 # else
 #  define CHECK_RECORDER()  ((void)0)
 # endif
 
@@ -1733,18 +1758,18 @@ Interpret(JSContext *cx, StackFrame *ent
                                 DO_OP();
 
 # define END_EMPTY_CASES
 
 #else /* !JS_THREADED_INTERP */
 
     register intN switchMask = 0;
     intN switchOp;
-
-# define ENABLE_INTERRUPTS() ((void) (switchMask = -1))
+    typedef GenericInterruptEnabler<intN> InterruptEnabler;
+    InterruptEnabler interruptEnabler(&switchMask, -1);
 
 # ifdef JS_TRACER
 #  define CHECK_RECORDER()                                                    \
     JS_ASSERT_IF(TRACE_RECORDER(cx), switchMask == -1)
 # else
 #  define CHECK_RECORDER()  ((void)0)
 # endif
 
@@ -1769,16 +1794,18 @@ Interpret(JSContext *cx, StackFrame *ent
 # define END_CASE_LEN4      len = 4; goto advance_pc;
 # define END_CASE_LEN5      len = 5; goto advance_pc;
 # define END_VARLEN_CASE    goto advance_pc;
 # define ADD_EMPTY_CASE(OP) BEGIN_CASE(OP)
 # define END_EMPTY_CASES    goto advance_pc_by_one;
 
 #endif /* !JS_THREADED_INTERP */
 
+#define ENABLE_INTERRUPTS() (interruptEnabler.enableInterrupts())
+
 #define LOAD_ATOM(PCOFF, atom)                                                \
     JS_BEGIN_MACRO                                                            \
         JS_ASSERT(regs.fp()->hasImacropc()                                    \
                   ? atoms == rt->atomState.commonAtomsStart() &&              \
                     GET_INDEX(regs.pc + PCOFF) < js_common_atom_count         \
                   : (size_t)(atoms - script->atomMap.vector) <                \
                     (size_t)(script->atomMap.length -                         \
                              GET_INDEX(regs.pc + PCOFF)));                    \
@@ -1854,22 +1881,23 @@ Interpret(JSContext *cx, StackFrame *ent
     JS_END_MACRO
 #endif
 #else
 #define MONITOR_BRANCH_TRACEVIS
 #endif
 
 #define RESTORE_INTERP_VARS()                                                 \
     JS_BEGIN_MACRO                                                            \
-        script = regs.fp()->script();                                         \
+        SET_SCRIPT(regs.fp()->script());                                      \
         argv = regs.fp()->maybeFormalArgs();                                  \
         atoms = FrameAtomBase(cx, regs.fp());                                 \
         JS_ASSERT(&cx->regs() == &regs);                                      \
         if (cx->isExceptionPending())                                         \
             goto error;                                                       \
+        CHECK_INTERRUPT_HANDLER();                                            \
     JS_END_MACRO
 
 #define MONITOR_BRANCH()                                                      \
     JS_BEGIN_MACRO                                                            \
         if (TRACING_ENABLED(cx)) {                                            \
             if (!TRACE_RECORDER(cx) && !TRACE_PROFILER(cx) && useMethodJIT) { \
                 MONITOR_BRANCH_METHODJIT();                                   \
             } else {                                                          \
@@ -1942,24 +1970,37 @@ Interpret(JSContext *cx, StackFrame *ent
                 MONITOR_BRANCH();                                             \
                 op = (JSOp) *regs.pc;                                         \
             }                                                                 \
         }                                                                     \
         LEAVE_ON_SAFE_POINT();                                                \
         DO_OP();                                                              \
     JS_END_MACRO
 
+#define SET_SCRIPT(s)                                                         \
+    JS_BEGIN_MACRO                                                            \
+        script = (s);                                                         \
+        if (script->stepModeEnabled())                                        \
+            ENABLE_INTERRUPTS();                                              \
+    JS_END_MACRO
+
 #define CHECK_INTERRUPT_HANDLER()                                             \
     JS_BEGIN_MACRO                                                            \
         if (cx->debugHooks->interruptHook)                                    \
             ENABLE_INTERRUPTS();                                              \
     JS_END_MACRO
 
     FrameRegs regs = cx->regs();
 
+    /*
+     * Help Debugger find frames running scripts that it has put in
+     * single-step mode.
+     */
+    InterpreterFrames interpreterFrame(cx, &regs, interruptEnabler);
+
     /* Repoint cx->regs to a local variable for faster access. */
     struct InterpExitGuard {
         JSContext *cx;
         const FrameRegs &regs;
         FrameRegs *prevContextRegs;
         InterpExitGuard(JSContext *cx, FrameRegs &regs)
           : cx(cx), regs(regs), prevContextRegs(&cx->regs()) {
             cx->stack.repointRegs(&regs);
@@ -1968,17 +2009,18 @@ Interpret(JSContext *cx, StackFrame *ent
             JS_ASSERT(&cx->regs() == &regs);
             *prevContextRegs = regs;
             cx->stack.repointRegs(prevContextRegs);
         }
     } interpGuard(cx, regs);
 
     /* Copy in hot values that change infrequently. */
     JSRuntime *const rt = cx->runtime;
-    JSScript *script = regs.fp()->script();
+    JSScript *script;
+    SET_SCRIPT(regs.fp()->script());
     int *pcCounts = script->pcCounters.get(JSRUNMODE_INTERP);
     Value *argv = regs.fp()->maybeFormalArgs();
     CHECK_INTERRUPT_HANDLER();
 
 #if defined(JS_TRACER) && defined(JS_METHODJIT)
     bool leaveOnSafePoint = (interpMode == JSINTERP_SAFEPOINT);
 # define CLEAR_LEAVE_ON_TRACE_POINT() ((void) (leaveOnSafePoint = false))
 #else
@@ -2113,28 +2155,34 @@ Interpret(JSContext *cx, StackFrame *ent
   interrupt:
 #else /* !JS_THREADED_INTERP */
   case -1:
     JS_ASSERT(switchMask == -1);
 #endif /* !JS_THREADED_INTERP */
     {
         bool moreInterrupts = false;
         JSInterruptHook hook = cx->debugHooks->interruptHook;
-        if (hook) {
+        if (hook || script->stepModeEnabled()) {
 #ifdef JS_TRACER
             if (TRACE_RECORDER(cx))
-                AbortRecording(cx, "interrupt hook");
+                AbortRecording(cx, "interrupt hook or singleStepMode");
 #ifdef JS_METHODJIT
             if (TRACE_PROFILER(cx))
                 AbortProfiling(cx);
 #endif
 #endif
             Value rval;
-            switch (hook(cx, script, regs.pc, Jsvalify(&rval),
-                         cx->debugHooks->interruptHookData)) {
+            JSTrapStatus status = JSTRAP_CONTINUE;
+            if (hook) {
+                status = hook(cx, script, regs.pc, Jsvalify(&rval),
+                              cx->debugHooks->interruptHookData);
+            }
+            if (status == JSTRAP_CONTINUE && script->stepModeEnabled())
+                status = Debugger::onSingleStep(cx, &rval);
+            switch (status) {
               case JSTRAP_ERROR:
                 goto error;
               case JSTRAP_CONTINUE:
                 break;
               case JSTRAP_RETURN:
                 regs.fp()->setReturnValue(rval);
                 interpReturnOK = JS_TRUE;
                 goto forced_return;
@@ -2343,29 +2391,29 @@ BEGIN_CASE(JSOP_STOP)
 
     interpReturnOK = true;
     if (entryFrame != regs.fp())
   inline_return:
     {
         JS_ASSERT(!regs.fp()->hasImacropc());
         JS_ASSERT(!js_IsActiveWithOrBlock(cx, &regs.fp()->scopeChain(), 0));
         interpReturnOK = ScriptEpilogue(cx, regs.fp(), interpReturnOK);
-        CHECK_INTERRUPT_HANDLER();
 
         /* The JIT inlines ScriptEpilogue. */
 #ifdef JS_METHODJIT
   jit_return:
 #endif
         cx->stack.popInlineFrame(regs);
 
         /* Sync interpreter locals. */
-        script = regs.fp()->script();
+        SET_SCRIPT(regs.fp()->script());
         pcCounts = script->pcCounters.get(JSRUNMODE_INTERP);
         argv = regs.fp()->maybeFormalArgs();
         atoms = FrameAtomBase(cx, regs.fp());
+        CHECK_INTERRUPT_HANDLER();
 
         JS_ASSERT(*regs.pc == JSOP_TRAP || *regs.pc == JSOP_NEW || *regs.pc == JSOP_CALL ||
                   *regs.pc == JSOP_FUNCALL || *regs.pc == JSOP_FUNAPPLY);
 
         /* Resume execution in the calling frame. */
         RESET_USE_METHODJIT();
         if (JS_LIKELY(interpReturnOK)) {
             TRACE_0(LeaveFrame);
@@ -4022,17 +4070,17 @@ BEGIN_CASE(JSOP_FUNAPPLY)
         DO_NEXT_OP(len);
     }
 
     JSScript *newScript = fun->script();
     if (!cx->stack.pushInlineFrame(cx, regs, args, *callee, fun, newScript, construct))
         goto error;
 
     /* Refresh local js::Interpret state. */
-    script = newScript;
+    SET_SCRIPT(newScript);
     pcCounts = script->pcCounters.get(JSRUNMODE_INTERP);
     argv = regs.fp()->formalArgsEnd() - fun->nargs;
     atoms = script->atomMap.vector;
 
     /* Only create call object after frame is rooted. */
     if (fun->isHeavyweight() && !CreateFunCallObject(cx, regs.fp()))
         goto error;
 
@@ -4045,17 +4093,16 @@ BEGIN_CASE(JSOP_FUNAPPLY)
         mjit::CompileRequest request = (interpMode == JSINTERP_NORMAL)
                                        ? mjit::CompileRequest_Interpreter
                                        : mjit::CompileRequest_JIT;
         mjit::CompileStatus status = mjit::CanMethodJIT(cx, script, regs.fp(), request);
         if (status == mjit::Compile_Error)
             goto error;
         if (!TRACE_RECORDER(cx) && !TRACE_PROFILER(cx) && status == mjit::Compile_Okay) {
             interpReturnOK = mjit::JaegerShot(cx);
-            CHECK_INTERRUPT_HANDLER();
             goto jit_return;
         }
     }
 #endif
 
     if (!ScriptPrologue(cx, regs.fp()))
         goto error;
 
--- a/js/src/jsinterp.h
+++ b/js/src/jsinterp.h
@@ -301,16 +301,63 @@ ValueToId(JSContext *cx, const Value &v,
  */
 extern const Value &
 GetUpvar(JSContext *cx, uintN level, UpvarCookie cookie);
 
 /* Search the call stack for the nearest frame with static level targetLevel. */
 extern StackFrame *
 FindUpvarFrame(JSContext *cx, uintN targetLevel);
 
+/*
+ * A linked list of the |FrameRegs regs;| variables belonging to all
+ * js::Interpret C++ frames on this thread's stack.
+ *
+ * Note that this is *not* a list of all JS frames running under the
+ * interpreter; that would include inlined frames, whose FrameRegs are
+ * saved in various pieces in various places. Rather, this lists each
+ * js::Interpret call's live 'regs'; when control returns to that call, it
+ * will resume execution with this 'regs' instance.
+ *
+ * When Debugger puts a script in single-step mode, all js::Interpret
+ * invocations that might be presently running that script must have
+ * interrupts enabled. It's not practical to simply check
+ * script->stepModeEnabled() at each point some callee could have changed
+ * it, because there are so many places js::Interpret could possibly cause
+ * JavaScript to run: each place an object might be coerced to a primitive
+ * or a number, for example. So instead, we simply expose a list of the
+ * 'regs' those frames are using, and let Debugger tweak the affected
+ * js::Interpret frames when an onStep handler is established.
+ *
+ * Elements of this list are allocated within the js::Interpret stack
+ * frames themselves; the list is headed by this thread's js::ThreadData.
+ */
+class InterpreterFrames {
+  public:
+    class InterruptEnablerBase {
+      public:
+        virtual void enableInterrupts() const = 0;
+    };
+
+    InterpreterFrames(JSContext *cx, FrameRegs *regs, const InterruptEnablerBase &enabler);
+    ~InterpreterFrames();
+
+    /* If this js::Interpret frame is running |script|, enable interrupts. */
+    void enableInterruptsIfRunning(JSScript *script) {
+        if (script == regs->fp()->script())
+            enabler.enableInterrupts();
+    }
+
+    InterpreterFrames *older;
+
+  private:
+    JSContext *context;
+    FrameRegs *regs;
+    const InterruptEnablerBase &enabler;
+};
+
 } /* namespace js */
 
 /*
  * JS_LONE_INTERPRET indicates that the compiler should see just the code for
  * the js_Interpret function when compiling jsinterp.cpp. The rest of the code
  * from the file should be visible only when compiling jsinvoke.cpp. It allows
  * platform builds to optimize selectively js_Interpret when the granularity
  * of the optimizations with the given compiler is a compilation unit.
--- a/js/src/jsscript.cpp
+++ b/js/src/jsscript.cpp
@@ -64,16 +64,17 @@
 #include "jsparse.h"
 #include "jsscope.h"
 #include "jsscript.h"
 #include "jstracer.h"
 #if JS_HAS_XDR
 #include "jsxdrapi.h"
 #endif
 #include "methodjit/MethodJIT.h"
+#include "methodjit/Retcon.h"
 #include "vm/Debugger.h"
 
 #include "jsobjinlines.h"
 #include "jsscriptinlines.h"
 
 using namespace js;
 using namespace js::gc;
 
@@ -1824,8 +1825,63 @@ js_CloneScript(JSContext *cx, JSScript *
     return script;
 }
 
 void
 JSScript::copyClosedSlotsTo(JSScript *other)
 {
     memcpy(other->closedSlots, closedSlots, nClosedArgs + nClosedVars);
 }
+
+bool
+JSScript::recompileForStepMode(JSContext *cx)
+{
+#ifdef JS_METHODJIT
+    js::mjit::JITScript *jit = jitNormal ? jitNormal : jitCtor;
+    if (jit && stepModeEnabled() != jit->singleStepMode) {
+        js::mjit::Recompiler recompiler(cx, this);
+        return recompiler.recompile();
+    }
+#endif
+    return true;
+}
+
+bool
+JSScript::tryNewStepMode(JSContext *cx, uint32 newValue)
+{
+    uint32 prior = stepMode;
+    stepMode = newValue;
+
+    if (!prior != !newValue) {
+        /* Step mode has been enabled or disabled. Alert the methodjit. */
+        if (!recompileForStepMode(cx)) {
+            stepMode = prior;
+            return false;
+        }
+
+        if (newValue) {
+            /* Step mode has been enabled. Alert the interpreter. */
+            InterpreterFrames *frames;
+            for (frames = JS_THREAD_DATA(cx)->interpreterFrames; frames; frames = frames->older)
+                frames->enableInterruptsIfRunning(this);
+        }
+    }
+    return true;
+}
+
+bool
+JSScript::setStepModeFlag(JSContext *cx, bool step)
+{
+    return tryNewStepMode(cx, (stepMode & stepCountMask) | (step ? stepFlagMask : 0));
+}
+
+bool
+JSScript::changeStepModeCount(JSContext *cx, int delta)
+{
+    assertSameCompartment(cx, this);
+    JS_ASSERT_IF(delta > 0, cx->compartment->debugMode());
+
+    uint32 count = stepMode & stepCountMask;
+    JS_ASSERT(((count + delta) & stepCountMask) == count + delta);
+    return tryNewStepMode(cx, 
+                          (stepMode & stepFlagMask) |
+                          ((count + delta) & stepCountMask));
+}
--- a/js/src/jsscript.h
+++ b/js/src/jsscript.h
@@ -458,16 +458,25 @@ struct JSScript {
 
   public:
     uint16          nfixed;     /* number of slots besides stack operands in
                                    slot array */
   private:
     size_t          callCount_; /* Number of times the script has been called. */
 
     /*
+     * When non-zero, compile script in single-step mode. The top bit is set and
+     * cleared by setStepMode, as used by JSD. The lower bits are a count,
+     * adjusted by changeStepModeCount, used by the Debugger object. Only
+     * when the bit is clear and the count is zero may we compile the script
+     * without single-step support.
+     */
+    uint32          stepMode;
+
+    /*
      * Offsets to various array structures from the end of this script, or
      * JSScript::INVALID_OFFSET if the array has length 0.
      */
   public:
     uint8           objectsOffset;  /* offset to the array of nested function,
                                        block, scope, xml and one-time regexps
                                        objects */
     uint8           upvarsOffset;   /* offset of the array of display ("up")
@@ -487,17 +496,16 @@ struct JSScript {
     bool            usesEval:1;       /* script uses eval() */
     bool            usesArguments:1;  /* script uses arguments */
     bool            warnedAboutTwoArgumentEval:1; /* have warned about use of
                                                      obsolete eval(s, o) in
                                                      this script */
     bool            hasSingletons:1;  /* script has singleton objects */
 #ifdef JS_METHODJIT
     bool            debugMode:1;      /* script was compiled in debug mode */
-    bool            singleStepMode:1; /* compile script in single-step mode */
 #endif
 
     jsbytecode      *main;      /* main entry point, after predef'ing prolog */
     JSAtomMap       atomMap;    /* maps immediate index to literal struct */
     JSCompartment   *compartment; /* compartment the script was compiled for */
     const char      *filename;  /* source filename or null */
     uint32          lineno;     /* base line number of script */
     uint16          nslots;     /* vars plus maximum stack depth */
@@ -674,16 +682,52 @@ struct JSScript {
     }
 
     uint32 getClosedVar(uint32 index) {
         JS_ASSERT(index < nClosedVars);
         return closedSlots[nClosedArgs + index];
     }
 
     void copyClosedSlotsTo(JSScript *other);
+
+  private:
+    static const uint32 stepFlagMask = 0x80000000U;
+    static const uint32 stepCountMask = 0x7fffffffU;
+
+    /*
+     * Attempt to recompile with or without single-stepping support, as directed
+     * by stepModeEnabled().
+     */
+    bool recompileForStepMode(JSContext *cx);
+
+    /* Attempt to change this->stepMode to |newValue|. */
+    bool tryNewStepMode(JSContext *cx, uint32 newValue);
+
+  public:
+    /*
+     * Set or clear the single-step flag. If the flag is set or the count
+     * (adjusted by changeStepModeCount) is non-zero, then the script is in
+     * single-step mode. (JSD uses an on/off-style interface; Debugger uses a
+     * count-style interface.)
+     */
+    bool setStepModeFlag(JSContext *cx, bool step);
+    
+    /*
+     * Increment or decrement the single-step count. If the count is non-zero or
+     * the flag (set by setStepModeFlag) is set, then the script is in
+     * single-step mode. (JSD uses an on/off-style interface; Debugger uses a
+     * count-style interface.)
+     */
+    bool changeStepModeCount(JSContext *cx, int delta);
+
+    bool stepModeEnabled() { return !!stepMode; }
+
+#ifdef DEBUG
+    uint32 stepModeCount() { return stepMode & stepCountMask; }
+#endif
 };
 
 #define SHARP_NSLOTS            2       /* [#array, #depth] slots if the script
                                            uses sharp variables */
 
 static JS_INLINE uintN
 StackDepth(JSScript *script)
 {
--- a/js/src/methodjit/Compiler.cpp
+++ b/js/src/methodjit/Compiler.cpp
@@ -469,17 +469,17 @@ mjit::Compiler::finishThisUp(JITScript *
         return Compile_Error;
     }
 
     JITScript *jit = new(cursor) JITScript;
     cursor += sizeof(JITScript);
 
     jit->code = JSC::MacroAssemblerCodeRef(result, execPool, masm.size() + stubcc.size());
     jit->invokeEntry = result;
-    jit->singleStepMode = script->singleStepMode;
+    jit->singleStepMode = script->stepModeEnabled();
     if (fun) {
         jit->arityCheckEntry = stubCode.locationOf(arityLabel).executableAddress();
         jit->fastEntry = fullCode.locationOf(invokeLabel).executableAddress();
     }
 
     /* 
      * WARNING: mics(), callICs() et al depend on the ordering of these
      * variable-length sections.  See JITScript's declaration for details.
@@ -898,17 +898,17 @@ mjit::Compiler::generateMethod()
         JSOp op = JSOp(*PC);
         int trap = stubs::JSTRAP_NONE;
         if (op == JSOP_TRAP) {
             if (!trapper.untrap(PC))
                 return Compile_Error;
             op = JSOp(*PC);
             trap |= stubs::JSTRAP_TRAP;
         }
-        if (script->singleStepMode && scanner.firstOpInLine(PC - script->code))
+        if (script->stepModeEnabled() && scanner.firstOpInLine(PC - script->code))
             trap |= stubs::JSTRAP_SINGLESTEP;
 
         if (cx->hasRunOption(JSOPTION_PCCOUNT) && script->pcCounters) {
             RegisterID r1 = frame.allocReg();
             RegisterID r2 = frame.allocReg();
 
             masm.move(ImmPtr(&script->pcCounters.get(JSRUNMODE_METHODJIT, PC - script->code)), r1);
             Address pcCounter(r1);
@@ -937,16 +937,27 @@ mjit::Compiler::generateMethod()
             frame.syncAndForgetEverything(opinfo->stackDepth);
             opinfo->safePoint = true;
         }
         jumpMap[uint32(PC - script->code)] = masm.label();
 
         SPEW_OPCODE();
         JS_ASSERT(frame.stackDepth() == opinfo->stackDepth);
 
+        // If this is an exception entry point, then jsl_InternalThrow has set
+        // VMFrame::fp to the correct fp for the entry point. We need to copy
+        // that value here to FpReg so that FpReg also has the correct sp.
+        // Otherwise, we would simply be using a stale FpReg value.
+        // Additionally, we check the interrupt flag to allow interrupting
+        // deeply nested exception handling.
+        if (op == JSOP_ENTERBLOCK && analysis->getCode(PC).exceptionEntry) {
+            restoreFrameRegs(masm);
+            interruptCheckHelper();
+        }
+
         if (trap) {
             prepareStubCall(Uses(0));
             masm.move(Imm32(trap), Registers::ArgReg1);
             Call cl = emitStubCall(JS_FUNC_TO_DATA_PTR(void *, stubs::Trap));
             InternalCallSite site(masm.callReturnOffset(cl), PC,
                                   CallSite::MAGIC_TRAP_ID, true, false);
             addCallSite(site);
         } else if (savedTraps && savedTraps[PC - script->code]) {
@@ -4826,27 +4837,16 @@ mjit::Compiler::jumpAndTrace(Jump j, jsb
     stubcc.masm.jump(Registers::ReturnReg);
 #endif
     return true;
 }
 
 void
 mjit::Compiler::enterBlock(JSObject *obj)
 {
-    // If this is an exception entry point, then jsl_InternalThrow has set
-    // VMFrame::fp to the correct fp for the entry point. We need to copy
-    // that value here to FpReg so that FpReg also has the correct sp.
-    // Otherwise, we would simply be using a stale FpReg value.
-    // Additionally, we check the interrupt flag to allow interrupting
-    // deeply nested exception handling.
-    if (analysis->getCode(PC).exceptionEntry) {
-        restoreFrameRegs(masm);
-        interruptCheckHelper();
-    }
-
     uint32 oldFrameDepth = frame.localSlots();
 
     /* For now, don't bother doing anything for this opcode. */
     frame.syncAndForgetEverything();
     masm.move(ImmPtr(obj), Registers::ArgReg1);
     uint32 n = js_GetEnterBlockStackDefs(cx, script, PC);
     INLINE_STUBCALL(stubs::EnterBlock);
     frame.enterBlock(n);
--- a/js/src/methodjit/StubCalls.cpp
+++ b/js/src/methodjit/StubCalls.cpp
@@ -1188,16 +1188,19 @@ stubs::Trap(VMFrame &f, uint32 trapTypes
         /*
          * single step mode may be paused without recompiling by
          * setting the interruptHook to NULL.
          */
         JSInterruptHook hook = f.cx->debugHooks->interruptHook;
         if (hook)
             result = hook(f.cx, f.cx->fp()->script(), pc, Jsvalify(&rval),
                           f.cx->debugHooks->interruptHookData);
+
+        if (result == JSTRAP_CONTINUE)
+            result = Debugger::onSingleStep(f.cx, &rval);
     }
 
     if (result == JSTRAP_CONTINUE && (trapTypes & JSTRAP_TRAP))
         result = Debugger::onTrap(f.cx, &rval);
 
     switch (result) {
       case JSTRAP_THROW:
         f.cx->setPendingException(rval);
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -58,16 +58,17 @@ using namespace js;
 
 /*** Forward declarations ************************************************************************/
 
 extern Class DebuggerFrame_class;
 
 enum {
     JSSLOT_DEBUGFRAME_OWNER,
     JSSLOT_DEBUGFRAME_ARGUMENTS,
+    JSSLOT_DEBUGFRAME_ONSTEP_HANDLER,
     JSSLOT_DEBUGFRAME_COUNT
 };
 
 extern Class DebuggerArguments_class;
 
 enum {
     JSSLOT_DEBUGARGUMENTS_FRAME,
     JSSLOT_DEBUGARGUMENTS_COUNT
@@ -381,23 +382,47 @@ JSObject *
 Debugger::getHook(Hook hook) const
 {
     JS_ASSERT(hook >= 0 && hook < HookCount);
     const Value &v = object->getReservedSlot(JSSLOT_DEBUG_HOOK_START + hook);
     return v.isUndefined() ? NULL : &v.toObject();
 }
 
 bool
-Debugger::hasAnyLiveHooks() const
+Debugger::hasAnyLiveHooks(JSContext *cx) const
 {
-    return enabled && (getHook(OnDebuggerStatement) ||
-                       getHook(OnExceptionUnwind) ||
-                       getHook(OnNewScript) ||
-                       getHook(OnEnterFrame) ||
-                       !JS_CLIST_IS_EMPTY(&breakpoints));
+    if (!enabled)
+        return false;
+
+    if (getHook(OnDebuggerStatement) ||
+        getHook(OnExceptionUnwind) ||
+        getHook(OnNewScript) ||
+        getHook(OnEnterFrame))
+    {
+        return true;
+    }
+
+    /* If any breakpoints are in live scripts, return true. */
+    for (Breakpoint *bp = firstBreakpoint(); bp; bp = bp->nextInDebugger()) {
+        /*
+         * If holder is non-null, examine it to see if the script will be
+         * collected. If holder is null, then bp->site->script is an eval
+         * script on the stack, so it is definitely live.
+         */
+        JSObject *holder = bp->site->getScriptObject();
+        if (!holder || !IsAboutToBeFinalized(cx, holder))
+            return true;
+    }
+
+    for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
+        if (!r.front().value->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined())
+            return true;
+    }
+
+    return false;
 }
 
 void
 Debugger::slowPathOnEnterFrame(JSContext *cx)
 {
     /* Build the list of recipients. */
     AutoValueVector triggered(cx);
     GlobalObject *global = cx->fp()->scopeChain().getGlobal();
@@ -428,18 +453,26 @@ Debugger::slowPathOnLeaveFrame(JSContext
      * FIXME This notifies only current debuggers, so it relies on a hack in
      * Debugger::removeDebuggeeGlobal to make sure only current debuggers have
      * Frame objects with .live === true.
      */
     if (GlobalObject::DebuggerVector *debuggers = global->getDebuggers()) {
         for (Debugger **p = debuggers->begin(); p != debuggers->end(); p++) {
             Debugger *dbg = *p;
             if (FrameMap::Ptr p = dbg->frames.lookup(fp)) {
+                StackFrame *frame = p->key;
                 JSObject *frameobj = p->value;
                 frameobj->setPrivate(NULL);
+
+                /* If this frame had an onStep handler, adjust the script's count. */
+                if (!frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() &&
+                    frame->isScriptFrame()) {
+                    frame->script()->changeStepModeCount(cx, -1);
+                }
+
                 dbg->frames.remove(p);
             }
         }
     }
 
     /*
      * If this is an eval frame, then from the debugger's perspective the
      * script is about to be destroyed. Remove any breakpoints in it.
@@ -873,16 +906,111 @@ Debugger::onTrap(JSContext *cx, Value *v
             return st;
     }
 
     /* By convention, return the true op to the interpreter in vp. */
     vp->setInt32(op);
     return JSTRAP_CONTINUE;
 }
 
+JSTrapStatus
+Debugger::onSingleStep(JSContext *cx, Value *vp)
+{
+    StackFrame *fp = cx->fp();
+
+    /*
+     * We may be stepping over a JSOP_EXCEPTION, that pushes the context's
+     * pending exception for a 'catch' clause to handle. Don't let the
+     * onStep handlers mess with that (other than by returning a resumption
+     * value).
+     */
+    Value exception = UndefinedValue();
+    bool exceptionPending = cx->isExceptionPending();
+    if (exceptionPending) {
+        exception = cx->getPendingException();
+        cx->clearPendingException();
+    }
+
+    /* We should only receive single-step traps for scripted frames. */
+    JS_ASSERT(fp->isScriptFrame());
+
+    /*
+     * Build list of Debugger.Frame instances referring to this frame with
+     * onStep handlers.
+     */
+    AutoObjectVector frames(cx);
+    GlobalObject *global = fp->scopeChain().getGlobal();
+    if (GlobalObject::DebuggerVector *debuggers = global->getDebuggers()) {
+        for (Debugger **d = debuggers->begin(); d != debuggers->end(); d++) {
+            Debugger *dbg = *d;
+            if (FrameMap::Ptr p = dbg->frames.lookup(fp)) {
+                JSObject *frame = p->value;
+                if (!frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined() &&
+                    !frames.append(frame))
+                    return JSTRAP_ERROR;
+            }
+        }
+    }
+
+#ifdef DEBUG
+    /*
+     * Validate the single-step count on this frame's script, to ensure that
+     * we're not receiving traps we didn't ask for. Even when frames is
+     * non-empty (and thus we know this trap was requested), do the check
+     * anyway, to make sure the count has the correct non-zero value.
+     *
+     * The converse --- ensuring that we do receive traps when we should --- can
+     * be done with unit tests.
+     */
+    {
+        uint32 stepperCount = 0;
+        JSScript *trappingScript = fp->script();
+        if (GlobalObject::DebuggerVector *debuggers = global->getDebuggers()) {
+            for (Debugger **p = debuggers->begin(); p != debuggers->end(); p++) {
+                Debugger *dbg = *p;
+                for (FrameMap::Range r = dbg->frames.all(); !r.empty(); r.popFront()) {
+                    StackFrame *frame = r.front().key;
+                    JSObject *frameobj = r.front().value;
+                    if (frame->isScriptFrame() &&
+                        frame->script() == trappingScript &&
+                        !frameobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER).isUndefined())
+                    {
+                        stepperCount++;
+                    }
+                }
+            }
+        }
+        if (trappingScript->compileAndGo)
+            JS_ASSERT(stepperCount == trappingScript->stepModeCount());
+        else
+            JS_ASSERT(stepperCount <= trappingScript->stepModeCount());
+    }
+#endif
+
+    /* Call all the onStep handlers we found. */
+    for (JSObject **p = frames.begin(); p != frames.end(); p++) {
+        JSObject *frame = *p;
+        Debugger *dbg = Debugger::fromChildJSObject(frame);
+        AutoCompartment ac(cx, dbg->object);
+        if (!ac.enter())
+            return JSTRAP_ERROR;
+        const Value &handler = frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
+        Value rval;
+        bool ok = Invoke(cx, ObjectValue(*frame), handler, 0, NULL, &rval);
+        JSTrapStatus st = dbg->parseResumptionValue(ac, ok, rval, vp);
+        if (st != JSTRAP_CONTINUE)
+            return st;
+    }
+
+    vp->setUndefined();
+    if (exceptionPending)
+        cx->setPendingException(exception);
+    return JSTRAP_CONTINUE;
+}
+
 
 /*** Debugger JSObjects **************************************************************************/
 
 void
 Debugger::markKeysInCompartment(JSTracer *tracer, const ObjectWeakMap &map)
 {
     JSCompartment *comp = tracer->context->runtime->gcCurrentCompartment;
     JS_ASSERT(comp);
@@ -954,63 +1082,86 @@ Debugger::markCrossCompartmentDebuggerOb
  * returns false.
  */
 bool
 Debugger::markAllIteratively(GCMarker *trc, JSGCInvocationKind gckind)
 {
     bool markedAny = false;
 
     /*
-     * We must find all Debugger objects in danger of GC. This code is a little
+     * Find all Debugger objects in danger of GC. This code is a little
      * convoluted since the easiest way to find them is via their debuggees.
      */
-    JSRuntime *rt = trc->context->runtime;
+    JSContext *cx = trc->context;
+    JSRuntime *rt = cx->runtime;
     JSCompartment *comp = rt->gcCurrentCompartment;
     for (JSCompartment **c = rt->compartments.begin(); c != rt->compartments.end(); c++) {
         JSCompartment *dc = *c;
 
-        /* If dc is being collected, mark breakpoint handlers in it. */
+        /* If dc is being collected, mark jsdbgapi.h trap closures in it. */
         if (!comp || dc == comp)
-            markedAny = markedAny | dc->markBreakpointsIteratively(trc);
+            markedAny = markedAny | dc->markTrapClosuresIteratively(trc);
 
         /*
          * If this is a single-compartment GC, no compartment can debug itself, so skip
-         * |comp|. If it's a global GC, then search every live compartment.
+         * |comp|. If it's a global GC, then search every compartment.
          */
         if (comp && dc == comp)
             continue;
 
         const GlobalObjectSet &debuggees = dc->getDebuggees();
         for (GlobalObjectSet::Range r = debuggees.all(); !r.empty(); r.popFront()) {
             GlobalObject *global = r.front();
 
             /*
              * Every debuggee has at least one debugger, so in this case
              * getDebuggers can't return NULL.
              */
             const GlobalObject::DebuggerVector *debuggers = global->getDebuggers();
             JS_ASSERT(debuggers);
             for (Debugger **p = debuggers->begin(); p != debuggers->end(); p++) {
                 Debugger *dbg = *p;
-                JSObject *obj = dbg->toJSObject();
 
                 /*
                  * dbg is a Debugger with at least one debuggee. Check three things:
                  *   - dbg is actually in a compartment being GC'd
                  *   - it isn't already marked
                  *   - it actually has hooks that might be called
-                 * IsAboutToBeFinalized covers the first two criteria.
                  */
-                if (IsAboutToBeFinalized(trc->context, obj) && dbg->hasAnyLiveHooks()) {
+                JSObject *dbgobj = dbg->toJSObject();
+                if (comp && comp != dbgobj->compartment())
+                    continue;
+
+                bool dbgMarked = !IsAboutToBeFinalized(cx, dbgobj);
+                if (!dbgMarked && dbg->hasAnyLiveHooks(cx)) {
                     /*
                      * obj could be reachable only via its live, enabled
                      * debugger hooks, which may yet be called.
                      */
-                    MarkObject(trc, *obj, "enabled Debugger");
+                    MarkObject(trc, *dbgobj, "enabled Debugger");
                     markedAny = true;
+                    dbgMarked = true;
+                }
+
+                if (dbgMarked) {
+                    /* Search for breakpoints to mark. */
+                    for (Breakpoint *bp = dbg->firstBreakpoint(); bp; bp = bp->nextInDebugger()) {
+                        JSObject *scriptObject = bp->site->getScriptObject();
+                        if (!scriptObject || !IsAboutToBeFinalized(cx, scriptObject)) {
+                            /*
+                             * The debugger and the script are both live.
+                             * Therefore the breakpoint handler is live.
+                             */
+                            JSObject *handler = bp->getHandler();
+                            if (IsAboutToBeFinalized(cx, handler)) {
+                                MarkObject(trc, *bp->getHandler(), "breakpoint handler");
+                                markedAny = true;
+                            }
+                        }
+                    }
                 }
             }
         }
     }
     return markedAny;
 }
 
 void
@@ -1022,18 +1173,22 @@ Debugger::traceObject(JSTracer *trc, JSO
 
 void
 Debugger::trace(JSTracer *trc)
 {
     if (uncaughtExceptionHook)
         MarkObject(trc, *uncaughtExceptionHook, "hooks");
 
     /*
-     * Mark Debugger.Frame objects that are reachable from JS if we look them up
-     * again (because the corresponding StackFrame is still on the stack).
+     * Mark Debugger.Frame objects. These are all reachable from JS, because the
+     * corresponding StackFrames are still on the stack.
+     *
+     * (Once we support generator frames properly, we will need
+     * weakly-referenced Debugger.Frame objects as well, for suspended generator
+     * frames.)
      */
     for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
         JSObject *frameobj = r.front().value;
         JS_ASSERT(frameobj->getPrivate());
         MarkObject(trc, *frameobj, "live Debugger.Frame");
     }
 
     /* Trace the referent -> Debugger.Object weak map. */
@@ -2645,16 +2800,64 @@ DebuggerFrame_getLive(JSContext *cx, uin
     JSObject *thisobj = CheckThisFrame(cx, args, "get live", false);
     if (!thisobj)
         return false;
     StackFrame *fp = (StackFrame *) thisobj->getPrivate();
     args.rval().setBoolean(!!fp);
     return true;
 }
 
+static bool
+IsValidHook(const Value &v)
+{
+    return v.isUndefined() || (v.isObject() && v.toObject().isCallable());
+}
+
+static JSBool
+DebuggerFrame_getOnStep(JSContext *cx, uintN argc, Value *vp)
+{
+    THIS_FRAME(cx, argc, vp, "get onStep", args, thisobj, fp);
+    (void) fp;  // Silence GCC warning
+    Value handler = thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
+    JS_ASSERT(IsValidHook(handler));
+    args.rval() = handler;
+    return true;
+}
+
+static JSBool
+DebuggerFrame_setOnStep(JSContext *cx, uintN argc, Value *vp)
+{
+    REQUIRE_ARGC("Debugger.Frame.set onStep", 1);
+    THIS_FRAME(cx, argc, vp, "set onStep", args, thisobj, fp);
+    if (!fp->isScriptFrame()) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_NOT_SCRIPT_FRAME);
+        return false;
+    }
+    if (!IsValidHook(args[0])) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NOT_CALLABLE_OR_UNDEFINED);
+        return false;
+    }
+
+    Value prior = thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER);
+    int delta = !args[0].isUndefined() - !prior.isUndefined();
+    if (delta != 0) {
+        /* Try to adjust this frame's script single-step mode count. */
+        AutoCompartment ac(cx, &fp->scopeChain());
+        if (!ac.enter())
+            return false;
+        if (!fp->script()->changeStepModeCount(cx, delta))
+            return false;
+    }
+
+    /* Now that the step mode switch has succeeded, we can install the handler. */
+    thisobj->setReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER, args[0]);
+    args.rval().setUndefined();
+    return true;
+}
+
 namespace js {
 
 JSBool
 EvaluateInScope(JSContext *cx, JSObject *scobj, StackFrame *fp, const jschar *chars,
                 uintN length, const char *filename, uintN lineno, Value *rval)
 {
     assertSameCompartment(cx, scobj, fp);
 
@@ -2790,16 +2993,17 @@ static JSPropertySpec DebuggerFrame_prop
     JS_PSG("constructing", DebuggerFrame_getConstructing, 0),
     JS_PSG("generator", DebuggerFrame_getGenerator, 0),
     JS_PSG("live", DebuggerFrame_getLive, 0),
     JS_PSG("offset", DebuggerFrame_getOffset, 0),
     JS_PSG("older", DebuggerFrame_getOlder, 0),
     JS_PSG("script", DebuggerFrame_getScript, 0),
     JS_PSG("this", DebuggerFrame_getThis, 0),
     JS_PSG("type", DebuggerFrame_getType, 0),
+    JS_PSGS("onStep", DebuggerFrame_getOnStep, DebuggerFrame_setOnStep, 0),
     JS_PS_END
 };
 
 static JSFunctionSpec DebuggerFrame_methods[] = {
     JS_FN("eval", DebuggerFrame_eval, 1, 0),
     JS_FN("evalWithBindings", DebuggerFrame_evalWithBindings, 1, 0),
     JS_FS_END
 };
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -221,17 +221,17 @@ class Debugger {
     static JSBool getDebuggees(JSContext *cx, uintN argc, Value *vp);
     static JSBool getNewestFrame(JSContext *cx, uintN argc, Value *vp);
     static JSBool clearAllBreakpoints(JSContext *cx, uintN argc, Value *vp);
     static JSBool construct(JSContext *cx, uintN argc, Value *vp);
     static JSPropertySpec properties[];
     static JSFunctionSpec methods[];
 
     JSObject *getHook(Hook hook) const;
-    bool hasAnyLiveHooks() const;
+    bool hasAnyLiveHooks(JSContext *cx) const;
 
     static void slowPathOnEnterFrame(JSContext *cx);
     static void slowPathOnLeaveFrame(JSContext *cx);
     static void slowPathOnNewScript(JSContext *cx, JSScript *script, JSObject *obj,
                                     NewScriptKind kind);
     static void slowPathOnDestroyScript(JSScript *script);
 
     static JSTrapStatus dispatchHook(JSContext *cx, js::Value *vp, Hook which);
@@ -283,34 +283,35 @@ class Debugger {
      *   * it is in the middle of dispatching an event (the event dispatching
      *     code roots it in this case); OR
      *   * it is enabled, and it is debugging at least one live compartment,
      *     and at least one of the following is true:
      *       - it has a debugger hook installed
      *       - it has a breakpoint set on a live script
      *       - it has a watchpoint set on a live object.
      *
-     * Debugger::mark handles the last case. If it finds any Debugger objects
-     * that are definitely live but not yet marked, it marks them and returns
-     * true. If not, it returns false.
+     * Debugger::markAllIteratively handles the last case. If it finds any
+     * Debugger objects that are definitely live but not yet marked, it marks
+     * them and returns true. If not, it returns false.
      */
     static void markCrossCompartmentDebuggerObjectReferents(JSTracer *tracer);
     static bool markAllIteratively(GCMarker *trc, JSGCInvocationKind gckind);
     static void sweepAll(JSContext *cx);
     static void detachAllDebuggersFromGlobal(JSContext *cx, GlobalObject *global,
                                              GlobalObjectSet::Enum *compartmentEnum);
 
     static inline void onEnterFrame(JSContext *cx);
     static inline void onLeaveFrame(JSContext *cx);
     static inline JSTrapStatus onDebuggerStatement(JSContext *cx, js::Value *vp);
     static inline JSTrapStatus onExceptionUnwind(JSContext *cx, js::Value *vp);
     static inline void onNewScript(JSContext *cx, JSScript *script, JSObject *obj,
                                    NewScriptKind kind);
     static inline void onDestroyScript(JSScript *script);
     static JSTrapStatus onTrap(JSContext *cx, Value *vp);
+    static JSTrapStatus onSingleStep(JSContext *cx, Value *vp);
 
     /************************************* Functions for use by Debugger.cpp. */
 
     inline bool observesEnterFrame() const;
     inline bool observesNewScript() const;
     inline bool observesScope(JSObject *obj) const;
     inline bool observesFrame(StackFrame *fp) const;
 
@@ -425,25 +426,44 @@ class BreakpointSite {
 
     bool recompile(JSContext *cx, bool forTrap);
 
   public:
     BreakpointSite(JSScript *script, jsbytecode *pc);
     Breakpoint *firstBreakpoint() const;
     bool hasBreakpoint(Breakpoint *bp);
     bool hasTrap() const { return !!trapHandler; }
+    JSObject *getScriptObject() const { return scriptObject; }
 
     bool inc(JSContext *cx);
     void dec(JSContext *cx);
     bool setTrap(JSContext *cx, JSTrapHandler handler, const Value &closure);
     void clearTrap(JSContext *cx, BreakpointSiteMap::Enum *e = NULL,
                    JSTrapHandler *handlerp = NULL, Value *closurep = NULL);
     void destroyIfEmpty(JSRuntime *rt, BreakpointSiteMap::Enum *e);
 };
 
+/*
+ * Each Breakpoint is a member of two linked lists: its debugger's list and its
+ * site's list.
+ *
+ * GC rules:
+ *   - script is live, breakpoint exists, and debugger is enabled
+ *      ==> debugger is live
+ *   - script is live, breakpoint exists, and debugger is live
+ *      ==> retain the breakpoint and the handler object is live
+ *
+ * Debugger::markAllIteratively implements these two rules. It uses
+ * Debugger::hasAnyLiveHooks to check for rule 1.
+ *
+ * Nothing else causes a breakpoint to be retained, so if its script or
+ * debugger is collected, the breakpoint is destroyed during GC sweep phase,
+ * even if the debugger compartment isn't being GC'd. This is implemented in
+ * JSCompartment::sweepBreakpoints.
+ */
 class Breakpoint {
     friend class ::JSCompartment;
     friend class js::Debugger;
 
   public:
     Debugger * const debugger;
     BreakpointSite * const site;
   private:
--- a/mobile/app/mobile.js
+++ b/mobile/app/mobile.js
@@ -67,16 +67,17 @@ pref("toolkit.screen.lock", false);
 
 // From libpref/src/init/all.js, extended to allow a slightly wider zoom range.
 pref("zoom.minPercent", 20);
 pref("zoom.maxPercent", 400);
 pref("toolkit.zoomManager.zoomValues", ".2,.3,.5,.67,.8,.9,1,1.1,1.2,1.33,1.5,1.7,2,2.4,3,4");
 
 // Device pixel to CSS px ratio, in percent. Set to -1 to calculate based on display density.
 pref("browser.viewport.scaleRatio", -1);
+pref("browser.viewport.desktopWidth", 980);
 
 /* allow scrollbars to float above chrome ui */
 pref("ui.scrollbarsCanOverlapContent", 1);
 
 /* use long press to display a context menu */
 pref("ui.click_hold_context_menus", true);
 
 /* cache prefs */
--- a/mobile/chrome/content/AppMenu.js
+++ b/mobile/chrome/content/AppMenu.js
@@ -1,75 +1,122 @@
 var AppMenu = {
   get panel() {
     delete this.panel;
     return this.panel = document.getElementById("appmenu");
   },
 
+  get popup() {
+    delete this.popup;
+    return this.popup = document.getElementById("appmenu-popup");
+  },
+
   shouldShow: function appmenu_shouldShow(aElement) {
     return !aElement.hidden;
   },
 
   overflowMenu : [],
 
   show: function show() {
     let modals = document.getElementsByClassName("modal-block").length;
     if (BrowserUI.activePanel || BrowserUI.isPanelVisible() || modals > 0 || BrowserUI.activeDialog)
       return;
 
-    let shown = 0;
-    let lastShown = null;
-    this.overflowMenu = [];
-    let childrenCount = this.panel.childElementCount;
-    for (let i = 0; i < childrenCount; i++) {
-      if (this.shouldShow(this.panel.children[i])) {
-        if (shown == 6 && this.overflowMenu.length == 0) {
-          // if we are trying to show more than 6 elements fall back to showing a more button
-          lastShown.removeAttribute("show");
-          this.overflowMenu.push(lastShown);
-          this.panel.appendChild(this.createMoreButton());
-        }
-        if (this.overflowMenu.length > 0) {
-          this.overflowMenu.push(this.panel.children[i]);
-        } else {
-          lastShown = this.panel.children[i];
-          lastShown.setAttribute("show", shown);
-          shown++;
-        }
-      }
-    }
-
-    this.panel.setAttribute("count", shown);
-    this.panel.hidden = false;
+    // Figure if we should show a menu-list or a pop-up menu
+    let menuButton = document.getElementById("tool-menu");
+    let listFormat = (getComputedStyle(menuButton).visibility == "visible");
 
     addEventListener("keypress", this, true);
 
-    BrowserUI.lockToolbar();
-    BrowserUI.pushPopup(this, [this.panel, Elements.toolbarContainer]);
+    if (listFormat) {
+      let listbox = document.getElementById("appmenu-popup-commands");
+      while (listbox.firstChild)
+        listbox.removeChild(listbox.firstChild);
+
+      let childrenCount = this.panel.childElementCount;
+      for (let i = 0; i < childrenCount; i++) {
+        let child = this.panel.children[i];
+
+        if (!this.shouldShow(child))
+          continue;
+
+        child.setAttribute("show", true);
+
+        let item = document.createElement("richlistitem");
+        item.setAttribute("class", "appmenu-button");
+        item.onclick = function() { child.click(); }
+
+        let label = document.createElement("label");
+        label.setAttribute("value", child.label);
+        item.appendChild(label);
+
+        listbox.appendChild(item);
+      }
+
+      this.popup.top = menuButton.getBoundingClientRect().bottom;
+
+      let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
+      this.popup.setAttribute(chromeReg.isLocaleRTL("global") ? "left" : "right", 0);
+
+      this.popup.hidden = false;
+      this.popup.anchorTo(menuButton);
+
+      BrowserUI.lockToolbar();
+      BrowserUI.pushPopup(this, [this.popup, menuButton]);
+    } else {
+      let shown = 0;
+      let lastShown = null;
+      this.overflowMenu = [];
+      let childrenCount = this.panel.childElementCount;
+      for (let i = 0; i < childrenCount; i++) {
+        if (this.shouldShow(this.panel.children[i])) {
+          if (shown == 6 && this.overflowMenu.length == 0) {
+            // if we are trying to show more than 6 elements fall back to showing a more button
+            lastShown.removeAttribute("show");
+            this.overflowMenu.push(lastShown);
+            this.panel.appendChild(this.createMoreButton());
+          }
+          if (this.overflowMenu.length > 0) {
+            this.overflowMenu.push(this.panel.children[i]);
+          } else {
+            lastShown = this.panel.children[i];
+            lastShown.setAttribute("show", shown);
+            shown++;
+          }
+        }
+      }
+
+      this.panel.setAttribute("count", shown);
+      this.panel.hidden = false;
+
+      BrowserUI.lockToolbar();
+      BrowserUI.pushPopup(this, [this.panel, Elements.toolbarContainer]);
+    }
   },
 
   hide: function hide() {
     this.panel.hidden = true;
+    this.popup.hidden = true;
     let moreButton = document.getElementById("appmenu-more-button");
     if (moreButton)
       moreButton.parentNode.removeChild(moreButton);
 
     for (let i = 0; i < this.panel.childElementCount; i++) {
       if (this.panel.children[i].hasAttribute("show"))
         this.panel.children[i].removeAttribute("show");
     }
 
     removeEventListener("keypress", this, true);
 
     BrowserUI.unlockToolbar();
     BrowserUI.popPopup(this);
   },
 
   toggle: function toggle() {
-    this.panel.hidden ? this.show() : this.hide();
+    (this.panel.hidden && this.popup.hidden) ? this.show() : this.hide();
   },
 
   handleEvent: function handleEvent(aEvent) {
     this.hide();
   },
 
   showAsList: function showAsList() {
     // allow menu to hide to remove the more button before we show the menulist
--- a/mobile/chrome/content/bindings/browser.js
+++ b/mobile/chrome/content/bindings/browser.js
@@ -26,16 +26,18 @@ let WebProgressListener = {
     };
 
     sendAsyncMessage("Content:StateChange", json);
   },
 
   onProgressChange: function onProgressChange(aWebProgress, aRequest, aCurSelf, aMaxSelf, aCurTotal, aMaxTotal) {
   },
 
+  _firstPaint: false,
+
   onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocationURI) {
     if (content != aWebProgress.DOMWindow)
       return;
 
     let spec = aLocationURI ? aLocationURI.spec : "";
     let location = spec.split("#")[0];
 
     let charset = content.document.characterSet;
@@ -46,20 +48,24 @@ let WebProgressListener = {
       location:        spec,
       canGoBack:       docShell.canGoBack,
       canGoForward:    docShell.canGoForward,
       charset:         charset.toString()
     };
 
     sendAsyncMessage("Content:LocationChange", json);
 
+    this._firstPaint = false;
+    let self = this;
+
     // When a new page is loaded fire a message for the first paint
     addEventListener("MozAfterPaint", function(aEvent) {
       removeEventListener("MozAfterPaint", arguments.callee, true);
 
+      self._firstPaint = true;
       let scrollOffset = ContentScroll.getScrollOffset(content);
       sendAsyncMessage("Browser:FirstPaint", scrollOffset);
     }, true);
   },
 
   onStatusChange: function onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
   },
 
@@ -537,18 +543,21 @@ let ContentScroll =  {
   },
 
   receiveMessage: function(aMessage) {
     let json = aMessage.json;
     switch (aMessage.name) {
       case "Content:SetCacheViewport": {
         // Set resolution for root view
         let rootCwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
-        if (json.id == 1)
+        if (json.id == 1) {
           rootCwu.setResolution(json.scale, json.scale);
+          if (!WebProgressListener._firstPaint)
+            break;
+        }
 
         let displayport = new Rect(json.x, json.y, json.w, json.h);
         if (displayport.isEmpty())
           break;
 
         // Map ID to element
         let element = null;
         try {
@@ -581,17 +590,16 @@ let ContentScroll =  {
         if (json.id == 1) {
           x = Math.round(x * json.scale) / json.scale;
           y = Math.round(y * json.scale) / json.scale;
         }
 
         let win = element.ownerDocument.defaultView;
         let winCwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
         winCwu.setDisplayPortForElement(x, y, displayport.width, displayport.height, element);
-
         break;
       }
 
       case "Content:SetWindowSize": {
         let cwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
         cwu.setCSSViewport(json.width, json.height);
         break;
       }
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -54,19 +54,16 @@ let Cu = Components.utils;
 let Cr = Components.results;
 
 function getBrowser() {
   return Browser.selectedBrowser;
 }
 
 const kBrowserViewZoomLevelPrecision = 10000;
 
-const kDefaultBrowserWidth = 800;
-const kFallbackBrowserWidth = 980;
-
 // allow panning after this timeout on pages with registered touch listeners
 const kTouchTimeout = 300;
 
 const kDefaultMetadata = { autoSize: false, allowZoom: true, autoScale: true };
 
 // Override sizeToContent in the main window. It breaks things (bug 565887)
 window.sizeToContent = function() {
   Cu.reportError("window.sizeToContent is not allowed in this window");
@@ -153,16 +150,23 @@ var Browser = {
                      .getInterface(Ci.nsIDOMWindowUtils),
   controlsScrollbox: null,
   controlsScrollboxScroller: null,
   controlsPosition: null,
   pageScrollbox: null,
   pageScrollboxScroller: null,
   styles: {},
 
+  get defaultBrowserWidth() {
+    delete this.defaultBrowserWidth;
+    var width = Services.prefs.getIntPref("browser.viewport.desktopWidth");
+    this.defaultBrowserWidth = width;
+    return width;
+  },
+
   startup: function startup() {
     var self = this;
 
     try {
       messageManager.loadFrameScript("chrome://browser/content/Util.js", true);
       messageManager.loadFrameScript("chrome://browser/content/forms.js", true);
       messageManager.loadFrameScript("chrome://browser/content/content.js", true);
     } catch (e) {
@@ -373,16 +377,17 @@ var Browser = {
         };
         Services.obs.addObserver(dummyCleanup, "sessionstore-windows-restored", false);
       }
       ss.restoreLastSession(bringFront);
     } else {
       this.addTab(commandURL || this.getHomePage(), true);
     }
 
+    messageManager.addMessageListener("MozScrolledAreaChanged", this);
     messageManager.addMessageListener("Browser:ViewportMetadata", this);
     messageManager.addMessageListener("Browser:CanCaptureMouse:Return", this);
     messageManager.addMessageListener("Browser:FormSubmit", this);
     messageManager.addMessageListener("Browser:KeyPress", this);
     messageManager.addMessageListener("Browser:ZoomToPoint:Return", this);
     messageManager.addMessageListener("Browser:CanUnload:Return", this);
     messageManager.addMessageListener("scroll", this);
     messageManager.addMessageListener("Browser:CertException", this);
@@ -472,16 +477,17 @@ var Browser = {
 
     Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null);
     return true;
   },
 
   shutdown: function shutdown() {
     BrowserUI.uninit();
 
+    messageManager.removeMessageListener("MozScrolledAreaChanged", this);
     messageManager.removeMessageListener("Browser:ViewportMetadata", this);
     messageManager.removeMessageListener("Browser:FormSubmit", this);
     messageManager.removeMessageListener("Browser:KeyPress", this);
     messageManager.removeMessageListener("Browser:ZoomToPoint:Return", this);
     messageManager.removeMessageListener("scroll", this);
     messageManager.removeMessageListener("Browser:CertException", this);
     messageManager.removeMessageListener("Browser:BlockedSite", this);
     messageManager.removeMessageListener("Browser:ErrorPage", this);
@@ -1174,16 +1180,22 @@ var Browser = {
     let dummy = getComputedStyle(document.documentElement, "").width;
   },
 
   receiveMessage: function receiveMessage(aMessage) {
     let json = aMessage.json;
     let browser = aMessage.target;
 
     switch (aMessage.name) {
+      case "MozScrolledAreaChanged": {
+        let tab = this.getTabForBrowser(browser);
+        if (tab)
+          tab.scrolledAreaChanged();
+        break;
+      }
       case "Browser:ViewportMetadata": {
         let tab = this.getTabForBrowser(browser);
         // Some browser such as iframes loaded dynamically into the chrome UI
         // does not have any assigned tab
         if (tab)
           tab.updateViewportMetadata(json);
         break;
       }
@@ -1524,17 +1536,16 @@ Browser.WebProgress.prototype = {
           tab.browser.userTypedValue = "";
           tab.browser.appIcon = { href: null, size:-1 };
 
 #ifdef MOZ_CRASH_REPORTER
           if (CrashReporter.enabled)
             CrashReporter.annotateCrashReport("URL", spec);
 #endif
           this._waitForLoad(tab);
-          tab.useFallbackWidth = false;
         }
 
         let event = document.createEvent("UIEvents");
         event.initUIEvent("URLChanged", true, false, window, locationHasChanged);
         tab.browser.dispatchEvent(event);
         break;
       }
 
@@ -1576,36 +1587,39 @@ Browser.WebProgress.prototype = {
   _documentStop: function _documentStop(aTab) {
     // Make sure the URLbar is in view. If this were the selected tab,
     // _waitForLoad would scroll to top.
     aTab.pageScrollOffset = { x: 0, y: 0 };
   },
 
   _waitForLoad: function _waitForLoad(aTab) {
     let browser = aTab.browser;
-    browser.messageManager.removeMessageListener("MozScrolledAreaChanged", aTab.scrolledAreaChanged);
+
+    aTab._firstPaint = false;
 
     browser.messageManager.addMessageListener("Browser:FirstPaint", function firstPaintListener(aMessage) {
       browser.messageManager.removeMessageListener(aMessage.name, arguments.callee);
+      aTab._firstPaint = true;
 
       // We're about to have new page content, so scroll the content area
       // so the new paints will draw correctly.
       // Background tabs are delayed scrolled to top in _documentStop
       if (getBrowser() == browser) {
         let json = aMessage.json;
         browser.getRootView().scrollTo(Math.floor(json.x * browser.scale),
                                        Math.floor(json.y * browser.scale));
         if (json.x == 0 && json.y == 0)
           Browser.pageScrollboxScroller.scrollTo(0, 0);
+        else
+          Browser.pageScrollboxScroller.scrollTo(0, Number.MAX_VALUE);
       }
 
-      aTab.scrolledAreaChanged();
+      aTab.scrolledAreaChanged(true);
       aTab.updateThumbnail();
 
-      browser.messageManager.addMessageListener("MozScrolledAreaChanged", aTab.scrolledAreaChanged);
       aTab.updateContentCapture();
     });
   }
 };
 
 
 const OPEN_APPTAB = 100; // Hack until we get a real API
 
@@ -2681,34 +2695,31 @@ function Tab(aURI, aParams) {
   this._id = null;
   this._browser = null;
   this._notification = null;
   this._loading = false;
   this._chromeTab = null;
   this._metadata = null;
 
   this.contentMightCaptureMouse = false;
-  this.useFallbackWidth = false;
   this.owner = null;
 
   this.hostChanged = false;
   this.state = null;
 
   // Set to 0 since new tabs that have not been viewed yet are good tabs to
   // toss if app needs more memory.
   this.lastSelected = 0;
 
   // aParams is an object that contains some properties for the initial tab
   // loading like flags, a referrerURI, a charset or even a postData.
   this.create(aURI, aParams || {});
 
   // default tabs to inactive (i.e. no display port)
   this.active = false;
-
-  this.scrolledAreaChanged = this.scrolledAreaChanged.bind(this);
 }
 
 Tab.prototype = {
   get browser() {
     return this._browser;
   },
 
   get notification() {
@@ -2778,18 +2789,18 @@ Tab.prototype = {
       let validW = viewportW > 0;
       let validH = viewportH > 0;
 
       if (validW && !validH) {
         viewportH = viewportW * (screenH / screenW);
       } else if (!validW && validH) {
         viewportW = viewportH * (screenW / screenH);
       } else if (!validW && !validH) {
-        viewportW = this.useFallbackWidth ? kFallbackBrowserWidth : kDefaultBrowserWidth;
-        viewportH = kDefaultBrowserWidth * (screenH / screenW);
+        viewportW = Browser.defaultBrowserWidth;
+        viewportH = Browser.defaultBrowserWidth * (screenH / screenW);
       }
     }
 
     // Make sure the viewport height is not shorter than the window when
     // the page is zoomed out to show its full width.
     if (viewportH * this.clampZoomLevel(this.getPageZoomLevel()) < screenH)
       viewportH = Math.max(viewportH, screenH * (browser.contentDocumentWidth / screenW));
 
@@ -2939,35 +2950,35 @@ Tab.prototype = {
     return rounded || 1.0;
   },
 
   /** Record the initial zoom level when a page first loads. */
   resetZoomLevel: function resetZoomLevel() {
     this._defaultZoomLevel = this._browser.scale;
   },
 
-  scrolledAreaChanged: function scrolledAreaChanged() {
+  scrolledAreaChanged: function scrolledAreaChanged(firstPaint) {
     if (!this._browser)
       return;
 
+    if (firstPaint) {
+      // You only get one shot, do not miss your chance to reflow.
+      this.updateViewportSize();
+    }
+
     this.updateDefaultZoomLevel();
-
-    if (!this.useFallbackWidth && this._browser.contentDocumentWidth > kDefaultBrowserWidth)
-      this.useFallbackWidth = true;
-
-    this.updateViewportSize();
   },
 
   /**
    * Recalculate default zoom level when page size changes, and update zoom
    * level if we are at default.
    */
   updateDefaultZoomLevel: function updateDefaultZoomLevel() {
     let browser = this._browser;
-    if (!browser)
+    if (!browser || !this._firstPaint)
       return;
 
     let isDefault = this.isDefaultZoomLevel();
     this._defaultZoomLevel = this.getDefaultZoomLevel();
     if (isDefault) {
       if (browser.scale != this._defaultZoomLevel) {
         browser.scale = this._defaultZoomLevel;
       } else {
@@ -3189,39 +3200,41 @@ var ViewableAreaObserver = {
     let oldWidth = parseInt(Browser.styles["viewable-width"].width);
 
     let newWidth = this.width;
     let newHeight = this.height;
     if (newHeight == oldHeight && newWidth == oldWidth)
       return;
 
     // Guess if the window has been resize to handle a virtual keyboard
-    let isDueToKeyboard = (newHeight != oldHeight && newWidth == oldWidth);
     this.isKeyboardOpened = (newHeight < oldHeight && newWidth == oldWidth);
 
     Browser.styles["viewable-height"].height = newHeight + "px";
     Browser.styles["viewable-height"].maxHeight = newHeight + "px";
 
     Browser.styles["viewable-width"].width = newWidth + "px";
     Browser.styles["viewable-width"].maxWidth = newWidth + "px";
 
-    let startup = !oldHeight && !oldWidth;
-    if (!isDueToKeyboard) {
+    // Don't update the viewport if screen height is shrinking, which typically
+    // means the keyboard appeared. This helps smooth out the experience of our
+    // form filler.
+    if (newHeight > oldHeight || newWidth != oldWidth) {
+      let startup = !oldHeight && !oldWidth;
       for (let i = Browser.tabs.length - 1; i >= 0; i--) {
         let tab = Browser.tabs[i];
         let oldContentWindowWidth = tab.browser.contentWindowWidth;
         tab.updateViewportSize(); // contentWindowWidth may change here.
-  
+
         // Don't bother updating the zoom level on startup
         if (!startup) {
           // If the viewport width is still the same, the page layout has not
           // changed, so we can keep keep the same content on-screen.
           if (tab.browser.contentWindowWidth == oldContentWindowWidth)
             tab.restoreViewportPosition(oldWidth, newWidth);
-  
+
           tab.updateDefaultZoomLevel();
         }
       }
     }
 
     // setTimeout(callback, 0) to ensure the resize event handler dispatch is finished
     setTimeout(function() {
       let event = document.createEvent("Events");
--- a/mobile/chrome/content/browser.xul
+++ b/mobile/chrome/content/browser.xul
@@ -256,16 +256,17 @@
                              clickSelectsAll="true"/>
                     <hbox id="urlbar-icons" class="urlbar-cap-button" observes="bcast_urlbarState">
                       <toolbarbutton id="tool-reload" oncommand="CommandUpdater.doCommand(event.shiftKey ? 'cmd_forceReload' : 'cmd_reload');"/>
                       <toolbarbutton id="tool-stop" command="cmd_stop"/>
                       <toolbarbutton id="tool-search" command="cmd_opensearch"/>
                     </hbox>
                   </hbox>
                   <toolbarbutton id="tool-star2" class="tool-star button-actionbar" command="cmd_star"/>
+                  <toolbarbutton id="tool-menu" class="button-actionbar" command="cmd_menu"/>
                   <toolbarbutton id="tool-tabs" class="button-actionbar" command="cmd_showTabs"/>
 #ifndef ANDROID
                   <toolbarbutton id="tool-app-close" class="urlbar-button" command="cmd_close"/>
 #endif
                 </toolbar>
               </box>
             </box>
 
@@ -384,16 +385,20 @@
         <separator id="bookmark-form-line" class="prompt-line"/>
         <scrollbox id="bookmark-form" align="start" class="prompt-message" flex="1"/>
         <hbox id="bookmark-form-buttons" pack="center" class="prompt-buttons">
           <button label="&editBookmarkDone.label;" class="prompt-button" oncommand="BookmarkHelper.save();"/>
         </hbox>
       </dialog>
     </box>
 
+    <arrowbox id="appmenu-popup" hidden="true">
+      <richlistbox id="appmenu-popup-commands"/>
+    </arrowbox>
+
     <box id="panel-container" hidden="true" class="window-width window-height panel-dark"
          style="-moz-stack-sizing: ignore" left="10000">
       <box id="panel-container-inner" flex="1" class="panel-dark">
         <box id="panel-controls" class="panel-row-header" oncommand="BrowserUI.switchPane(event.target.getAttribute('linkedpanel'));">
           <toolbarbutton id="tool-preferences" label="&prefsHeader.label;" class="panel-row-button" type="radio" group="1" checked="true" linkedpanel="prefs-container"/>
           <toolbarbutton id="tool-downloads" label="&downloadsHeader.label;" class="panel-row-button" type="radio" group="1" linkedpanel="downloads-container"/>
           <toolbarbutton id="tool-addons" label="&addonsHeader.label;" class="panel-row-button" type="radio" group="1" linkedpanel="addons-container"/>
           <toolbarbutton id="tool-console" label="&consoleHeader.label;" class="panel-row-button" type="radio" group="1" hidden="true" linkedpanel="console-container"/>
--- a/mobile/themes/core/browser.css
+++ b/mobile/themes/core/browser.css
@@ -369,16 +369,20 @@ toolbarbutton.urlbar-button {
 .tool-forward {
   list-style-image: url("chrome://browser/skin/images/forward-default-hdpi.png");
 }
 
 #tool-panel-open {
   list-style-image: url("chrome://browser/skin/images/settings-default-hdpi.png");
 }
 
+#tool-menu {
+  list-style-image: url("chrome://browser/skin/images/menu-hdpi.png");
+}
+
 %ifndef ANDROID
 /* MAEMO (and desktop) only */
 .panel-close {
   min-width: @touch_button_xlarge@ !important; /* 72, not 64 to make up for the negative margin */
   max-width: @touch_button_xlarge@ !important; /* 72, not 64 to make up for the negative margin */
   min-height: @touch_button_xlarge@ !important; /* 72, not 64 to make up for the negative margin */
   list-style-image: url("chrome://browser/skin/images/task-back-hdpi.png");
   background: transparent !important;
index 41e15740d9d2f273b94160b5b5273d341cb848a7..8e66e5ae57e04947712939bd5e9ee08d46aad029
GIT binary patch
literal 268
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O`
z&)mfH)S%SFl*+=BsWw1G>pWc?Ln>}1{rUgj{;&g+S{oyq&J7lCZ|}gSrMq{}o;d5#
z4;h8l!wav7)w2mFxOg)gu&&FHDi4m7xy-oX0TbI~6Wzx$8RvtaypZ_dV)2UMiOUP_
zBTgQitzisP4DK_2Osm=1aP5Jln@B?uqfYB3D{%$!fW@5!CgMHMnl8Fn%;I({KA52-
zbCxxtn6WCU#q{b*)xJ605e2FIRh`C<UdaAl^wB&xa?S$g7i<hnJPc<Kbsk-^s@M(a
Oat2RVKbLh*2~7a8tzxkN
index 41e15740d9d2f273b94160b5b5273d341cb848a7..8e66e5ae57e04947712939bd5e9ee08d46aad029
GIT binary patch
literal 268
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O`
z&)mfH)S%SFl*+=BsWw1G>pWc?Ln>}1{rUgj{;&g+S{oyq&J7lCZ|}gSrMq{}o;d5#
z4;h8l!wav7)w2mFxOg)gu&&FHDi4m7xy-oX0TbI~6Wzx$8RvtaypZ_dV)2UMiOUP_
zBTgQitzisP4DK_2Osm=1aP5Jln@B?uqfYB3D{%$!fW@5!CgMHMnl8Fn%;I({KA52-
zbCxxtn6WCU#q{b*)xJ605e2FIRh`C<UdaAl^wB&xa?S$g7i<hnJPc<Kbsk-^s@M(a
Oat2RVKbLh*2~7a8tzxkN
index 41e15740d9d2f273b94160b5b5273d341cb848a7..8e66e5ae57e04947712939bd5e9ee08d46aad029
GIT binary patch
literal 268
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O`
z&)mfH)S%SFl*+=BsWw1G>pWc?Ln>}1{rUgj{;&g+S{oyq&J7lCZ|}gSrMq{}o;d5#
z4;h8l!wav7)w2mFxOg)gu&&FHDi4m7xy-oX0TbI~6Wzx$8RvtaypZ_dV)2UMiOUP_
zBTgQitzisP4DK_2Osm=1aP5Jln@B?uqfYB3D{%$!fW@5!CgMHMnl8Fn%;I({KA52-
zbCxxtn6WCU#q{b*)xJ605e2FIRh`C<UdaAl^wB&xa?S$g7i<hnJPc<Kbsk-^s@M(a
Oat2RVKbLh*2~7a8tzxkN
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8662b9319c44dbb6fb1a135f1c8b8383a46cac75
GIT binary patch
literal 245
zc%17D@N?(olHy`uVBq!ia0vp^S|H591|*LjJ{b+97>k44ofy`glX(f`uqAoByD<C*
z!3BGlPX>x`7I;J!Gca%qgD@k*tT_@uLG}_)Usv|qY~l=T3=4i%`v8SXJzX3_A`ZWu
z>d4h#z{4{8-~aQkk52AdqF~V)`C@5+#H13VHp6F=uUt-@^oJ|7^zp3SAyc?!ecrSE
z-zNscS(o#(zO0bDy144fi$L2|?q63JR)zGxdv)>6g<mCFT@6c`_0DQAP2!rh?L+1S
llM6cQ1Ef?~w`>jJ`@pRxUUGZQU!VgRJYD@<);T3K0RU?LT%!O0
--- a/mobile/themes/core/jar.mn
+++ b/mobile/themes/core/jar.mn
@@ -71,16 +71,17 @@ chrome.jar:
   skin/images/reload-hdpi.png               (images/reload-hdpi.png)
   skin/images/alert-addons-30.png           (images/alert-addons-30.png)
   skin/images/alert-downloads-30.png        (images/alert-downloads-30.png)
   skin/images/addons-default-hdpi.png         (images/addons-default-hdpi.png)
   skin/images/back-default-hdpi.png           (images/back-default-hdpi.png)
   skin/images/allpages-48.png               (images/allpages-48.png)
   skin/images/history-48.png                (images/history-48.png)
   skin/images/tabs-hdpi.png                 (images/tabs-hdpi.png)
+  skin/images/menu-hdpi.png                 (images/menu-hdpi.png)
   skin/images/bookmark-default-hdpi.png     (images/bookmark-default-hdpi.png)
   skin/images/bookmarks-48.png              (images/bookmarks-48.png)
   skin/images/bookmark-starred-hdpi.png     (images/bookmark-starred-hdpi.png)
   skin/images/panelrow-active-hdpi.png      (images/panelrow-active-hdpi.png)
   skin/images/panelrow-default-hdpi.png     (images/panelrow-default-hdpi.png)
   skin/images/panelrow-selected-hdpi.png    (images/panelrow-selected-hdpi.png)
   skin/images/forward-default-hdpi.png        (images/forward-default-hdpi.png)
   skin/images/downloads-default-hdpi.png      (images/downloads-default-hdpi.png)
@@ -249,16 +250,17 @@ chrome.jar:
   skin/gingerbread/images/play-hdpi.png                 (gingerbread/images/play-hdpi.png)
   skin/gingerbread/images/pause-hdpi.png                (gingerbread/images/pause-hdpi.png)
   skin/gingerbread/images/mute-hdpi.png                 (gingerbread/images/mute-hdpi.png)
   skin/gingerbread/images/unmute-hdpi.png               (gingerbread/images/unmute-hdpi.png)
   skin/gingerbread/images/scrubber-hdpi.png             (gingerbread/images/scrubber-hdpi.png)
   skin/gingerbread/images/handle-start.png              (gingerbread/images/handle-start.png)
   skin/gingerbread/images/handle-end.png                (gingerbread/images/handle-end.png)
   skin/gingerbread/images/tabs-hdpi.png                 (images/tabs-hdpi.png)
+  skin/gingerbread/images/menu-hdpi.png                 (images/menu-hdpi.png)
   skin/gingerbread/images/errorpage-warning.png         (images/errorpage-warning.png)
   skin/gingerbread/images/errorpage-larry-white.png     (images/errorpage-larry-white.png)
   skin/gingerbread/images/errorpage-larry-black.png     (images/errorpage-larry-black.png)
   skin/gingerbread/images/homescreen-blank-hdpi.png     (images/homescreen-blank-hdpi.png)
   skin/gingerbread/images/homescreen-default-hdpi.png   (images/homescreen-default-hdpi.png)
 
 chrome.jar:
 % skin browser classic/1.0 %skin/honeycomb/ os=Android osversion>=3.0
@@ -378,13 +380,14 @@ chrome.jar:
   skin/honeycomb/images/play-hdpi.png                 (honeycomb/images/play-hdpi.png)
   skin/honeycomb/images/pause-hdpi.png                (honeycomb/images/pause-hdpi.png)
   skin/honeycomb/images/mute-hdpi.png                 (honeycomb/images/mute-hdpi.png)
   skin/honeycomb/images/unmute-hdpi.png               (honeycomb/images/unmute-hdpi.png)
   skin/honeycomb/images/scrubber-hdpi.png             (honeycomb/images/scrubber-hdpi.png)
   skin/honeycomb/images/handle-start.png              (images/handle-start.png)
   skin/honeycomb/images/handle-end.png                (images/handle-end.png)
   skin/honeycomb/images/tabs-hdpi.png                 (images/tabs-hdpi.png)
+  skin/honeycomb/images/menu-hdpi.png                 (images/menu-hdpi.png)
   skin/honeycomb/images/errorpage-warning.png         (images/errorpage-warning.png)
   skin/honeycomb/images/errorpage-larry-white.png     (images/errorpage-larry-white.png)
   skin/honeycomb/images/errorpage-larry-black.png     (images/errorpage-larry-black.png)
   skin/honeycomb/images/homescreen-blank-hdpi.png     (images/homescreen-blank-hdpi.png)
   skin/honeycomb/images/homescreen-default-hdpi.png   (images/homescreen-default-hdpi.png)