Bug 932762, part 2 - Make SVG elements cache their Moz2D path data to speed up rendering, hit-testing, clipPath clipping, bbox calculations and animation/text along a path. r=longsonr
authorJonathan Watt <jwatt@jwatt.org>
Sat, 04 Oct 2014 12:13:30 +0100
changeset 232040 75c93e9a7c970d66c8c1af12d400fd45e1871e96
parent 232039 41bad654774b1ba60c91dfe0a5246f416c72fe68
child 232041 2374287a24ab3d60373eea2d4349c5b8129be182
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslongsonr
bugs932762
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 932762, part 2 - Make SVG elements cache their Moz2D path data to speed up rendering, hit-testing, clipPath clipping, bbox calculations and animation/text along a path. r=longsonr
content/svg/content/src/DOMSVGPathSeg.cpp
content/svg/content/src/DOMSVGPathSeg.h
content/svg/content/src/DOMSVGPathSegList.cpp
content/svg/content/src/SVGMotionSMILAnimationFunction.cpp
content/svg/content/src/SVGPathData.cpp
content/svg/content/src/SVGPathData.h
content/svg/content/src/SVGPathElement.cpp
content/svg/content/src/SVGPathElement.h
content/svg/content/src/nsSVGElement.cpp
content/svg/content/src/nsSVGElement.h
content/svg/content/src/nsSVGPathGeometryElement.cpp
content/svg/content/src/nsSVGPathGeometryElement.h
layout/reftests/svg/dynamic-fill-rule-01-ref.svg
layout/reftests/svg/dynamic-fill-rule-01.svg
layout/reftests/svg/reftest.list
layout/svg/SVGTextFrame.cpp
layout/svg/nsSVGClipPathFrame.cpp
layout/svg/nsSVGPathGeometryFrame.cpp
layout/svg/nsSVGUtils.cpp
layout/svg/nsSVGUtils.h
modules/libpref/init/all.js
--- a/content/svg/content/src/DOMSVGPathSeg.cpp
+++ b/content/svg/content/src/DOMSVGPathSeg.cpp
@@ -175,17 +175,16 @@ DOMSVGPathSeg::IndexIsValid()
       return;                                                                 \
     }                                                                         \
     if (HasOwner()) {                                                         \
       if (InternalItem()[1+index] == float(a##propName)) {                    \
         return;                                                               \
       }                                                                       \
       AutoChangePathSegNotifier notifier(this);                               \
       InternalItem()[1+index] = float(a##propName);                           \
-      InvalidateCachedList();                                                 \
     } else {                                                                  \
       mArgs[index] = float(a##propName);                                      \
     }                                                                         \
   }
 
 // For float, the normal type of arguments
 #define IMPL_FLOAT_PROP(segName, propName, index) \
   IMPL_PROP_WITH_TYPE(segName, propName, index, float)
--- a/content/svg/content/src/DOMSVGPathSeg.h
+++ b/content/svg/content/src/DOMSVGPathSeg.h
@@ -204,20 +204,16 @@ protected:
    *
    * To simplify the code we just have this one method for obtaining both
    * baseVal and animVal internal items. This means that animVal items don't
    * get const protection, but then our setter methods guard against changing
    * animVal items.
    */
   float* InternalItem();
 
-  void InvalidateCachedList() {
-    mList->InternalList().mCachedPath = nullptr;
-  }
-
   virtual float* PtrToMemberArgs() = 0;
 
 #ifdef DEBUG
   bool IndexIsValid();
 #endif
 
   nsRefPtr<DOMSVGPathSegList> mList;
 
--- a/content/svg/content/src/DOMSVGPathSegList.cpp
+++ b/content/svg/content/src/DOMSVGPathSegList.cpp
@@ -378,17 +378,16 @@ DOMSVGPathSegList::InsertItemBefore(DOMS
   AutoChangePathSegListNotifier notifier(this);
   // Now that we know we're inserting, keep animVal list in sync as necessary.
   MaybeInsertNullInAnimValListAt(aIndex, internalIndex, argCount);
 
   float segAsRaw[1 + NS_SVG_PATH_SEG_MAX_ARGS];
   domItem->ToSVGPathSegEncodedData(segAsRaw);
 
   InternalList().mData.InsertElementsAt(internalIndex, segAsRaw, 1 + argCount);
-  InternalList().mCachedPath = nullptr;
   mItems.InsertElementAt(aIndex, ItemProxy(domItem.get(), internalIndex));
 
   // This MUST come after the insertion into InternalList(), or else under the
   // insertion into InternalList() the values read from domItem would be bad
   // data from InternalList() itself!:
   domItem->InsertingIntoList(this, aIndex, IsAnimValList());
 
   UpdateListIndicesFromIndex(aIndex + 1, argCount + 1);
@@ -435,17 +434,16 @@ DOMSVGPathSegList::ReplaceItem(DOMSVGPat
   int32_t newArgCount = SVGPathSegUtils::ArgCountForType(domItem->Type());
 
   float segAsRaw[1 + NS_SVG_PATH_SEG_MAX_ARGS];
   domItem->ToSVGPathSegEncodedData(segAsRaw);
 
   bool ok = !!InternalList().mData.ReplaceElementsAt(
                   internalIndex, 1 + oldArgCount,
                   segAsRaw, 1 + newArgCount);
-  InternalList().mCachedPath = nullptr;
   if (!ok) {
     aError.Throw(NS_ERROR_OUT_OF_MEMORY);
     return nullptr;
   }
   ItemAt(aIndex) = domItem;
 
   // This MUST come after the ToSVGPathSegEncodedData call, otherwise that call
   // would end up reading bad data from InternalList()!
@@ -490,17 +488,16 @@ DOMSVGPathSegList::RemoveItem(uint32_t a
   int32_t argCount = SVGPathSegUtils::ArgCountForType(segType);
 
   // Now that we know we're removing, keep animVal list in sync as necessary.
   // Do this *before* touching InternalList() so the removed item can get its
   // internal value.
   MaybeRemoveItemFromAnimValListAt(aIndex, argCount);
 
   InternalList().mData.RemoveElementsAt(internalIndex, 1 + argCount);
-  InternalList().mCachedPath = nullptr;
   mItems.RemoveElementAt(aIndex);
 
   UpdateListIndicesFromIndex(aIndex, -(argCount + 1));
 
   return result.forget();
 }
 
 already_AddRefed<DOMSVGPathSeg>
--- a/content/svg/content/src/SVGMotionSMILAnimationFunction.cpp
+++ b/content/svg/content/src/SVGMotionSMILAnimationFunction.cpp
@@ -222,17 +222,17 @@ SVGMotionSMILAnimationFunction::
   if (pathElem) {
     const SVGPathData &path = pathElem->GetAnimPathSegList()->GetAnimValue();
     // Path data must contain of at least one path segment (if the path data
     // doesn't begin with a valid "M", then it's invalid).
     if (path.Length()) {
       bool ok =
         path.GetDistancesFromOriginToEndsOfVisibleSegments(&mPathVertices);
       if (ok && mPathVertices.Length()) {
-        mPath = pathElem->GetPathForLengthOrPositionMeasuring();
+        mPath = pathElem->GetOrBuildPathForMeasuring();
       }
     }
   }
 }
 
 void
 SVGMotionSMILAnimationFunction::RebuildPathAndVerticesFromPathAttr()
 {
@@ -247,17 +247,17 @@ SVGMotionSMILAnimationFunction::RebuildP
   // accept all segments up to the first invalid token. Instead we must
   // explicitly check that the parse produces at least one path segment (if
   // the path data doesn't begin with a valid "M", then it's invalid).
   pathParser.Parse();
   if (!path.Length()) {
     return;
   }
 
-  mPath = path.ToPathForLengthOrPositionMeasuring();
+  mPath = path.BuildPathForMeasuring();
   bool ok = path.GetDistancesFromOriginToEndsOfVisibleSegments(&mPathVertices);
   if (!ok || !mPathVertices.Length()) {
     mPath = nullptr;
   }
 }
 
 // Helper to regenerate our path representation & its list of vertices
 void
--- a/content/svg/content/src/SVGPathData.cpp
+++ b/content/svg/content/src/SVGPathData.cpp
@@ -32,17 +32,16 @@ static bool IsMoveto(uint16_t aSegType)
 
 nsresult
 SVGPathData::CopyFrom(const SVGPathData& rhs)
 {
   if (!mData.SetCapacity(rhs.mData.Length())) {
     // Yes, we do want fallible alloc here
     return NS_ERROR_OUT_OF_MEMORY;
   }
-  mCachedPath = nullptr;
   mData = rhs.mData;
   return NS_OK;
 }
 
 void
 SVGPathData::GetValueAsString(nsAString& aValue) const
 {
   // we need this function in DidChangePathSegList
@@ -67,30 +66,28 @@ SVGPathData::GetValueAsString(nsAString&
 
 nsresult
 SVGPathData::SetValueFromString(const nsAString& aValue)
 {
   // We don't use a temp variable since the spec says to parse everything up to
   // the first error. We still return any error though so that callers know if
   // there's a problem.
 
-  mCachedPath = nullptr;
   nsSVGPathDataParser pathParser(aValue, this);
   return pathParser.Parse() ? NS_OK : NS_ERROR_DOM_SYNTAX_ERR;
 }
 
 nsresult
 SVGPathData::AppendSeg(uint32_t aType, ...)
 {
   uint32_t oldLength = mData.Length();
   uint32_t newLength = oldLength + 1 + SVGPathSegUtils::ArgCountForType(aType);
   if (!mData.SetLength(newLength)) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
-  mCachedPath = nullptr;
 
   mData[oldLength] = SVGPathSegUtils::EncodeType(aType);
   va_list args;
   va_start(args, aType);
   for (uint32_t i = oldLength + 1; i < newLength; ++i) {
     // NOTE! 'float' is promoted to 'double' when passed through '...'!
     mData[i] = float(va_arg(args, double));
   }
@@ -505,35 +502,31 @@ SVGPathData::BuildPath(PathBuilder* buil
                     "prevSegType should be left at the final segType");
 
   MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT;
 
   return builder->Finish();
 }
 
 TemporaryRef<Path>
-SVGPathData::ToPathForLengthOrPositionMeasuring() const
+SVGPathData::BuildPathForMeasuring() const
 {
   // Since the path that we return will not be used for painting it doesn't
   // matter what we pass to CreatePathBuilder as aFillRule. Hawever, we do want
   // to pass something other than NS_STYLE_STROKE_LINECAP_SQUARE as
   // aStrokeLineCap to avoid the insertion of extra little lines (by
   // ApproximateZeroLengthSubpathSquareCaps), in which case the value that we
   // pass as aStrokeWidth doesn't matter (since it's only used to determine the
   // length of those extra little lines).
 
-  if (!mCachedPath) {
-    RefPtr<DrawTarget> drawTarget =
-      gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
-    RefPtr<PathBuilder> builder =
-      drawTarget->CreatePathBuilder(FillRule::FILL_WINDING);
-    mCachedPath = BuildPath(builder, NS_STYLE_STROKE_LINECAP_BUTT, 0);
-  }
-
-  return mCachedPath;
+  RefPtr<DrawTarget> drawTarget =
+    gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
+  RefPtr<PathBuilder> builder =
+    drawTarget->CreatePathBuilder(FillRule::FILL_WINDING);
+  return BuildPath(builder, NS_STYLE_STROKE_LINECAP_BUTT, 0);
 }
 
 static double
 AngleOfVector(const Point& aVector)
 {
   // C99 says about atan2 "A domain error may occur if both arguments are
   // zero" and "On a domain error, the function returns an implementation-
   // defined value". In the case of atan2 the implementation-defined value
--- a/content/svg/content/src/SVGPathData.h
+++ b/content/svg/content/src/SVGPathData.h
@@ -158,17 +158,17 @@ public:
    */
   bool GetDistancesFromOriginToEndsOfVisibleSegments(FallibleTArray<double> *aArray) const;
 
   /**
    * This returns a path without the extra little line segments that
    * ApproximateZeroLengthSubpathSquareCaps can insert if we have square-caps.
    * See the comment for that function for more info on that.
    */
-  TemporaryRef<Path> ToPathForLengthOrPositionMeasuring() const;
+  TemporaryRef<Path> BuildPathForMeasuring() const;
 
   TemporaryRef<Path> BuildPath(PathBuilder* aBuilder,
                                uint8_t aCapStyle,
                                Float aStrokeWidth) const;
 
   const_iterator begin() const { return mData.Elements(); }
   const_iterator end() const { return mData.Elements() + mData.Length(); }
 
@@ -188,51 +188,47 @@ protected:
 
   /**
    * This may fail on OOM if the internal capacity needs to be increased, in
    * which case the list will be left unmodified.
    */
   nsresult CopyFrom(const SVGPathData& rhs);
 
   float& operator[](uint32_t aIndex) {
-    mCachedPath = nullptr;
     return mData[aIndex];
   }
 
   /**
    * This may fail (return false) on OOM if the internal capacity is being
    * increased, in which case the list will be left unmodified.
    */
   bool SetLength(uint32_t aLength) {
-    mCachedPath = nullptr;
     return mData.SetLength(aLength);
   }
 
   nsresult SetValueFromString(const nsAString& aValue);
 
   void Clear() {
-    mCachedPath = nullptr;
     mData.Clear();
   }
 
   // Our DOM wrappers have direct access to our mData, so they directly
   // manipulate it rather than us implementing:
   //
   // * InsertItem(uint32_t aDataIndex, uint32_t aType, const float *aArgs);
   // * ReplaceItem(uint32_t aDataIndex, uint32_t aType, const float *aArgs);
   // * RemoveItem(uint32_t aDataIndex);
   // * bool AppendItem(uint32_t aType, const float *aArgs);
 
   nsresult AppendSeg(uint32_t aType, ...); // variable number of float args
 
-  iterator begin() { mCachedPath = nullptr; return mData.Elements(); }
-  iterator end() { mCachedPath = nullptr; return mData.Elements() + mData.Length(); }
+  iterator begin() { return mData.Elements(); }
+  iterator end() { return mData.Elements() + mData.Length(); }
 
   FallibleTArray<float> mData;
-  mutable RefPtr<gfx::Path> mCachedPath;
 };
 
 
 /**
  * This SVGPathData subclass is for SVGPathSegListSMILType which needs to
  * have write access to the lists it works with.
  *
  * Instances of this class do not have DOM wrappers that need to be kept in
--- a/content/svg/content/src/SVGPathElement.cpp
+++ b/content/svg/content/src/SVGPathElement.cpp
@@ -65,24 +65,24 @@ already_AddRefed<SVGAnimatedNumber>
 SVGPathElement::PathLength()
 {
   return mPathLength.ToDOMAnimatedNumber(this);
 }
 
 float
 SVGPathElement::GetTotalLength()
 {
-  RefPtr<Path> flat = GetPathForLengthOrPositionMeasuring();
+  RefPtr<Path> flat = GetOrBuildPathForMeasuring();
   return flat ? flat->ComputeLength() : 0.f;
 }
 
 already_AddRefed<nsISVGPoint>
 SVGPathElement::GetPointAtLength(float distance, ErrorResult& rv)
 {
-  RefPtr<Path> path = GetPathForLengthOrPositionMeasuring();
+  RefPtr<Path> path = GetOrBuildPathForMeasuring();
   if (!path) {
     rv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   float totalLength = path->ComputeLength();
   if (mPathLength.IsExplicitlySet()) {
     float pathLength = mPathLength.GetAnimValue();
@@ -300,19 +300,19 @@ SVGPathElement::IsAttributeMapped(const 
     sMarkersMap
   };
 
   return FindAttributeDependence(name, map) ||
     SVGPathElementBase::IsAttributeMapped(name);
 }
 
 TemporaryRef<Path>
-SVGPathElement::GetPathForLengthOrPositionMeasuring()
+SVGPathElement::GetOrBuildPathForMeasuring()
 {
-  return mD.GetAnimValue().ToPathForLengthOrPositionMeasuring();
+  return mD.GetAnimValue().BuildPathForMeasuring();
 }
 
 //----------------------------------------------------------------------
 // nsSVGPathGeometryElement methods
 
 bool
 SVGPathElement::AttributeDefinesGeometry(const nsIAtom *aName)
 {
@@ -335,17 +335,17 @@ SVGPathElement::GetMarkPoints(nsTArray<n
 float
 SVGPathElement::GetPathLengthScale(PathLengthScaleForType aFor)
 {
   NS_ABORT_IF_FALSE(aFor == eForTextPath || aFor == eForStroking,
                     "Unknown enum");
   if (mPathLength.IsExplicitlySet()) {
     float authorsPathLengthEstimate = mPathLength.GetAnimValue();
     if (authorsPathLengthEstimate > 0) {
-      RefPtr<Path> path = GetPathForLengthOrPositionMeasuring();
+      RefPtr<Path> path = GetOrBuildPathForMeasuring();
       if (!path) {
         // The path is empty or invalid so its length must be zero and
         // we know that 0 / authorsPathLengthEstimate = 0.
         return 0.0;
       }
       if (aFor == eForTextPath) {
         // For textPath, a transform on the referenced path affects the
         // textPath layout, so when calculating the actual path length
--- a/content/svg/content/src/SVGPathElement.h
+++ b/content/svg/content/src/SVGPathElement.h
@@ -52,18 +52,17 @@ public:
   virtual void GetMarkPoints(nsTArray<nsSVGMark> *aMarks) MOZ_OVERRIDE;
   virtual TemporaryRef<Path> BuildPath(PathBuilder* aBuilder) MOZ_OVERRIDE;
 
   /**
    * This returns a path without the extra little line segments that
    * ApproximateZeroLengthSubpathSquareCaps can insert if we have square-caps.
    * See the comment for that function for more info on that.
    */
-  virtual TemporaryRef<Path>
-    GetPathForLengthOrPositionMeasuring() MOZ_OVERRIDE;
+  virtual TemporaryRef<Path> GetOrBuildPathForMeasuring() MOZ_OVERRIDE;
 
   // nsIContent interface
   virtual nsresult Clone(mozilla::dom::NodeInfo *aNodeInfo, nsINode **aResult) const MOZ_OVERRIDE;
 
   virtual SVGAnimatedPathSegList* GetAnimPathSegList() MOZ_OVERRIDE {
     return &mD;
   }
 
--- a/content/svg/content/src/nsSVGElement.cpp
+++ b/content/svg/content/src/nsSVGElement.cpp
@@ -10,16 +10,17 @@
 #include "nsSVGElement.h"
 
 #include "mozilla/dom/SVGSVGElement.h"
 #include "mozilla/dom/SVGTests.h"
 #include "nsContentUtils.h"
 #include "nsICSSDeclaration.h"
 #include "nsIDocument.h"
 #include "nsIDOMMutationEvent.h"
+#include "nsSVGPathGeometryElement.h"
 #include "mozilla/InternalMutationEvent.h"
 #include "nsError.h"
 #include "nsIPresShell.h"
 #include "nsGkAtoms.h"
 #include "mozilla/css/StyleRule.h"
 #include "nsRuleWalker.h"
 #include "mozilla/css/Declaration.h"
 #include "nsCSSProps.h"
@@ -1600,16 +1601,18 @@ nsSVGElement::DidChangeLength(uint8_t aA
 
   DidChangeValue(*info.mLengthInfo[aAttrEnum].mName, aEmptyOrOldValue,
                  newValue);
 }
 
 void
 nsSVGElement::DidAnimateLength(uint8_t aAttrEnum)
 {
+  ClearAnyCachedPath();
+
   nsIFrame* frame = GetPrimaryFrame();
 
   if (frame) {
     LengthAttributesInfo info = GetLengthInfo();
     frame->AttributeChanged(kNameSpaceID_None,
                             *info.mLengthInfo[aAttrEnum].mName,
                             nsIDOMMutationEvent::MODIFICATION);
   }
@@ -1845,16 +1848,18 @@ nsSVGElement::DidChangePointList(const n
 }
 
 void
 nsSVGElement::DidAnimatePointList()
 {
   NS_ABORT_IF_FALSE(GetPointListAttrName(),
                     "Animating non-existent path data?");
 
+  ClearAnyCachedPath();
+
   nsIFrame* frame = GetPrimaryFrame();
 
   if (frame) {
     frame->AttributeChanged(kNameSpaceID_None,
                             GetPointListAttrName(),
                             nsIDOMMutationEvent::MODIFICATION);
   }
 }
@@ -1880,16 +1885,18 @@ nsSVGElement::DidChangePathSegList(const
 }
 
 void
 nsSVGElement::DidAnimatePathSegList()
 {
   NS_ABORT_IF_FALSE(GetPathDataAttrName(),
                     "Animating non-existent path data?");
 
+  ClearAnyCachedPath();
+
   nsIFrame* frame = GetPrimaryFrame();
 
   if (frame) {
     frame->AttributeChanged(kNameSpaceID_None,
                             GetPathDataAttrName(),
                             nsIDOMMutationEvent::MODIFICATION);
   }
 }
--- a/content/svg/content/src/nsSVGElement.h
+++ b/content/svg/content/src/nsSVGElement.h
@@ -28,16 +28,17 @@
 class nsSVGAngle;
 class nsSVGBoolean;
 class nsSVGEnum;
 class nsSVGInteger;
 class nsSVGIntegerPair;
 class nsSVGLength2;
 class nsSVGNumber2;
 class nsSVGNumberPair;
+class nsSVGPathGeometryElement;
 class nsSVGString;
 class nsSVGViewBox;
 
 namespace mozilla {
 namespace dom {
 class CSSValue;
 class SVGSVGElement;
 
@@ -308,16 +309,17 @@ public:
   const nsAttrValue* GetAnimatedClassName() const
   {
     if (!mClassAttribute.IsAnimated()) {
       return nullptr;
     }
     return mClassAnimAttr;
   }
 
+  virtual void ClearAnyCachedPath() {}
   virtual nsIDOMNode* AsDOMNode() MOZ_FINAL MOZ_OVERRIDE { return this; }
   virtual bool IsTransformable() { return false; }
 
   // WebIDL
   mozilla::dom::SVGSVGElement* GetOwnerSVGElement();
   nsSVGElement* GetViewportElement();
   already_AddRefed<mozilla::dom::SVGAnimatedString> ClassName();
 protected:
--- a/content/svg/content/src/nsSVGPathGeometryElement.cpp
+++ b/content/svg/content/src/nsSVGPathGeometryElement.cpp
@@ -3,30 +3,44 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsSVGPathGeometryElement.h"
 
 #include "gfxPlatform.h"
 #include "mozilla/gfx/2D.h"
 #include "nsComputedDOMStyle.h"
+#include "nsSVGUtils.h"
 #include "nsSVGLength2.h"
 #include "SVGContentUtils.h"
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 
 //----------------------------------------------------------------------
 // Implementation
 
 nsSVGPathGeometryElement::nsSVGPathGeometryElement(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo)
   : nsSVGPathGeometryElementBase(aNodeInfo)
 {
 }
 
+nsresult
+nsSVGPathGeometryElement::AfterSetAttr(int32_t aNamespaceID, nsIAtom* aName,
+                                       const nsAttrValue* aValue, bool aNotify)
+{
+  if (mCachedPath &&
+      aNamespaceID == kNameSpaceID_None &&
+      AttributeDefinesGeometry(aName)) {
+    mCachedPath = nullptr;
+  }
+  return nsSVGPathGeometryElementBase::AfterSetAttr(aNamespaceID, aName,
+                                                    aValue, aNotify);
+}
+
 bool
 nsSVGPathGeometryElement::AttributeDefinesGeometry(const nsIAtom *aName)
 {
   // Check for nsSVGLength2 attribute
   LengthAttributesInfo info = GetLengthInfo();
   for (uint32_t i = 0; i < info.mLengthCount; i++) {
     if (aName == *info.mLengthInfo[i].mName) {
       return true;
@@ -56,17 +70,40 @@ nsSVGPathGeometryElement::IsMarkable()
 }
 
 void
 nsSVGPathGeometryElement::GetMarkPoints(nsTArray<nsSVGMark> *aMarks)
 {
 }
 
 TemporaryRef<Path>
-nsSVGPathGeometryElement::GetPathForLengthOrPositionMeasuring()
+nsSVGPathGeometryElement::GetOrBuildPath(const DrawTarget& aDrawTarget,
+                                         FillRule aFillRule)
+{
+  // We only cache the path if it matches the backend used for screen painting:
+  bool cacheable  = aDrawTarget.GetBackendType() ==
+                      gfxPlatform::GetPlatform()->GetContentBackend();
+
+  // Checking for and returning mCachedPath before checking the pref means
+  // that the pref is only live on page reload (or app restart for SVG in
+  // chrome). The benefit is that we avoid causing a CPU memory cache miss by
+  // looking at the global variable that the pref's stored in.
+  if (cacheable && mCachedPath) {
+    return mCachedPath;
+  }
+  RefPtr<PathBuilder> builder = aDrawTarget.CreatePathBuilder(aFillRule);
+  RefPtr<Path> path = BuildPath(builder);
+  if (cacheable && NS_SVGPathCachingEnabled()) {
+    mCachedPath = path;
+  }
+  return path.forget();
+}
+
+TemporaryRef<Path>
+nsSVGPathGeometryElement::GetOrBuildPathForMeasuring()
 {
   return nullptr;
 }
 
 FillRule
 nsSVGPathGeometryElement::GetFillRule()
 {
   FillRule fillRule = FillRule::FILL_WINDING; // Equivalent to NS_STYLE_FILL_RULE_NONZERO
--- a/content/svg/content/src/nsSVGPathGeometryElement.h
+++ b/content/svg/content/src/nsSVGPathGeometryElement.h
@@ -26,24 +26,36 @@ struct nsSVGMark {
     x(aX), y(aY), angle(aAngle), type(aType) {}
 };
 
 typedef mozilla::dom::SVGGraphicsElement nsSVGPathGeometryElementBase;
 
 class nsSVGPathGeometryElement : public nsSVGPathGeometryElementBase
 {
 protected:
+  typedef mozilla::gfx::DrawTarget DrawTarget;
   typedef mozilla::gfx::FillRule FillRule;
   typedef mozilla::gfx::Float Float;
   typedef mozilla::gfx::Path Path;
   typedef mozilla::gfx::PathBuilder PathBuilder;
 
 public:
   explicit nsSVGPathGeometryElement(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo);
 
+  virtual nsresult AfterSetAttr(int32_t aNamespaceID, nsIAtom* aName,
+                                const nsAttrValue* aValue, bool aNotify) MOZ_OVERRIDE;
+
+  /**
+   * Causes this element to discard any Path object that GetOrBuildPath may
+   * have cached.
+   */
+  virtual void ClearAnyCachedPath() MOZ_OVERRIDE MOZ_FINAL {
+    mCachedPath = nullptr;
+  }
+
   virtual bool AttributeDefinesGeometry(const nsIAtom *aName);
 
   /**
    * Returns true if this element's geometry depends on the width or height of its
    * coordinate context (typically the viewport established by its nearest <svg>
    * ancestor). In other words, returns true if one of the attributes for which
    * AttributeDefinesGeometry returns true has a percentage value.
    *
@@ -52,22 +64,49 @@ public:
    */
   bool GeometryDependsOnCoordCtx();
 
   virtual bool IsMarkable();
   virtual void GetMarkPoints(nsTArray<nsSVGMark> *aMarks);
 
   /**
    * Returns a Path that can be used to paint, hit-test or calculate bounds for
+   * this element. May return nullptr if there is no [valid] path. The path
+   * that is created may be cached and returned on subsequent calls.
+   */
+  virtual mozilla::TemporaryRef<Path> GetOrBuildPath(const DrawTarget& aDrawTarget,
+                                                     FillRule fillRule);
+
+  /**
+   * The same as GetOrBuildPath, but bypasses the cache (neither returns any
+   * previously cached Path, nor caches the Path that in does return).
    * this element. May return nullptr if there is no [valid] path.
    */
-  virtual mozilla::TemporaryRef<Path> BuildPath(PathBuilder* aBuilder = nullptr) = 0;
+  virtual mozilla::TemporaryRef<Path> BuildPath(PathBuilder* aBuilder) = 0;
 
-  virtual mozilla::TemporaryRef<Path> GetPathForLengthOrPositionMeasuring();
+  /**
+   * Returns a Path that can be used to measure the length of this elements
+   * path, or to find the position at a given distance along it.
+   *
+   * This is currently equivalent to calling GetOrBuildPath, but it may not be
+   * in the future. The reason for this function to be separate from
+   * GetOrBuildPath is because SVGPathData::BuildPath inserts small lines into
+   * the path if zero length subpaths are encountered, in order to implement
+   * the SVG specifications requirements that zero length subpaths should
+   * render circles/squares if stroke-linecap is round/square, respectively.
+   * In principle these inserted lines could interfere with path measurement,
+   * so we keep callers that are looking to do measurement separate in case we
+   * run into problems with the inserted lines negatively affecting measuring
+   * for content.
+   */
+  virtual mozilla::TemporaryRef<Path> GetOrBuildPathForMeasuring();
 
   /**
    * Returns the current computed value of the CSS property 'fill-rule' for
    * this element.
    */
   FillRule GetFillRule();
+
+protected:
+  mutable mozilla::RefPtr<Path> mCachedPath;
 };
 
 #endif
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/dynamic-fill-rule-01-ref.svg
@@ -0,0 +1,20 @@
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
+  <title>Reference for dynamic changes to fill-rule</title>
+  <!-- From https://bugzilla.mozilla.org/show_bug.cgi?id=932762 -->
+
+  <!-- for p1 -->
+  <rect x="50" y="100" width="150" height="50"/>
+  <rect x="100" y="50" width="50" height="150"/>
+
+  <!-- for p2 -->
+  <rect x="250" y="100" width="50" height="50"/>
+  <rect x="350" y="100" width="50" height="50"/>
+  <rect x="300" y="50" width="50" height="50"/>
+  <rect x="300" y="150" width="50" height="50"/>
+
+</svg>
+
new file mode 100644
--- /dev/null
+++ b/layout/reftests/svg/dynamic-fill-rule-01.svg
@@ -0,0 +1,25 @@
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+  <title>Testcase for dynamic changes to fill-rule</title>
+  <!-- From https://bugzilla.mozilla.org/show_bug.cgi?id=932762 -->
+  <script>
+
+function doTest() {
+  document.getElementById("p1").removeAttribute("style");
+  document.getElementById("p2").setAttribute("style", "fill-rule: evenodd;");
+  document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("MozReftestInvalidate", doTest, false);
+window.setTimeout(doTest, 4000); // fallback for running outside reftest
+
+  </script>
+  <path id="p1" style="fill-rule: evenodd;"
+        d="M100,50 l0,150 50,0 0,-100 -100,0 0,50 150,0 0,-50 -50,0 0,-50 z"/>
+  <path id="p2"
+        d="M300,50 l0,150 50,0 0,-100 -100,0 0,50 150,0 0,-50 -50,0 0,-50 z"/>
+</svg>
+
--- a/layout/reftests/svg/reftest.list
+++ b/layout/reftests/svg/reftest.list
@@ -73,16 +73,17 @@ fuzzy-if(/^Windows\x20NT\x206\.[12]/.tes
 == dynamic-conditions-11.svg pass.svg
 == dynamic-conditions-12.svg pass.svg
 == dynamic-conditions-13.svg about:blank
 fuzzy-if(Android,4,87) == dynamic-clipPath-01.svg pass.svg
 == dynamic-clipPath-02.svg pass.svg
 == dynamic-feFlood-01.svg pass.svg
 asserts(0-1) == dynamic-feImage-01.svg pass.svg # intermittent assertions (bug 886080)
 == dynamic-fill-01.svg dynamic-fill-01-ref.svg
+== dynamic-fill-rule-01.svg dynamic-fill-rule-01-ref.svg
 fuzzy-if(d2d,1,10000) == dynamic-filter-contents-01a.svg dynamic-filter-contents-01-ref.svg
 fuzzy-if(d2d,1,10000) == dynamic-filter-contents-01b.svg dynamic-filter-contents-01-ref.svg
 == dynamic-gradient-contents-01.svg pass.svg
 == dynamic-gradient-contents-02.svg pass.svg
 == dynamic-inner-svg-01.svg pass.svg
 == dynamic-link-style-01.svg pass.svg
 == dynamic-marker-01.svg pass.svg
 == dynamic-marker-02.svg dynamic-marker-02-ref.svg
--- a/layout/svg/SVGTextFrame.cpp
+++ b/layout/svg/SVGTextFrame.cpp
@@ -4800,17 +4800,17 @@ SVGTextFrame::GetTextPath(nsIFrame* aTex
 
   if (!pathFrame) {
     return nullptr;
   }
 
   nsSVGPathGeometryElement *element =
     static_cast<nsSVGPathGeometryElement*>(pathFrame->GetContent());
 
-  RefPtr<Path> path = element->GetPathForLengthOrPositionMeasuring();
+  RefPtr<Path> path = element->GetOrBuildPathForMeasuring();
   if (!path) {
     return nullptr;
   }
 
   gfxMatrix matrix = element->PrependLocalTransformsTo(gfxMatrix());
   if (!matrix.IsIdentity()) {
     RefPtr<PathBuilder> builder =
       path->TransformedCopyToBuilder(ToMatrix(matrix));
--- a/layout/svg/nsSVGClipPathFrame.cpp
+++ b/layout/svg/nsSVGClipPathFrame.cpp
@@ -61,20 +61,18 @@ nsSVGClipPathFrame::ApplyClipOrPaintClip
           static_cast<nsSVGPathGeometryElement*>(pathFrame->GetContent());
         gfxMatrix toChildsUserSpace = pathElement->
           PrependLocalTransformsTo(mMatrixForChildren,
                                    nsSVGElement::eUserSpaceToParent);
         gfxMatrix newMatrix =
           gfx->CurrentMatrix().PreMultiply(toChildsUserSpace).NudgeToIntegers();
         if (!newMatrix.IsSingular()) {
           gfx->SetMatrix(newMatrix);
-          RefPtr<PathBuilder> builder =
-            gfx->GetDrawTarget()->CreatePathBuilder(
-              nsSVGUtils::ToFillRule(pathFrame->StyleSVG()->mClipRule));
-          clipPath = pathElement->BuildPath(builder);
+          clipPath = pathElement->GetOrBuildPath(*gfx->GetDrawTarget(),
+                                                 nsSVGUtils::ToFillRule(pathFrame->StyleSVG()->mClipRule));
         }
       }
     }
     gfx->NewPath();
     if (clipPath) {
       gfx->SetPath(clipPath);
       // gfxContext::Clip() resets the FillRule on any Path set by
       // gfxContext::SetPath() call to the contexts own current FillRule.
--- a/layout/svg/nsSVGPathGeometryFrame.cpp
+++ b/layout/svg/nsSVGPathGeometryFrame.cpp
@@ -153,16 +153,35 @@ nsSVGPathGeometryFrame::DidSetStyleConte
     float oldOpacity = aOldStyleContext->PeekStyleDisplay()->mOpacity;
     float newOpacity = StyleDisplay()->mOpacity;
     if (newOpacity != oldOpacity &&
         nsSVGUtils::CanOptimizeOpacity(this)) {
       // nsIFrame::BuildDisplayListForStackingContext() is not going to create an
       // nsDisplayOpacity display list item, so DLBI won't invalidate for us.
       InvalidateFrame();
     }
+
+    nsSVGPathGeometryElement* element =
+      static_cast<nsSVGPathGeometryElement*>(mContent);
+
+    if (aOldStyleContext->PeekStyleSVG()) {
+      if ((StyleSVG()->mStrokeLinecap !=
+             aOldStyleContext->PeekStyleSVG()->mStrokeLinecap) &&
+          element->Tag() == nsGkAtoms::path) {
+        // If the stroke-linecap changes to or from "butt" then our element
+        // needs to update its cached Moz2D Path, since SVGPathData::BuildPath
+        // decides whether or not to insert little lines into the path for zero
+        // length subpaths base on that property.
+        element->ClearAnyCachedPath();
+      } else if (StyleSVG()->mFillRule !=
+                   aOldStyleContext->PeekStyleSVG()->mFillRule) {
+        // Moz2D Path objects are fill-rule specific.
+        element->ClearAnyCachedPath();
+      }
+    }
   }
 }
 
 nsIAtom *
 nsSVGPathGeometryFrame::GetType() const
 {
   return nsGkAtoms::svgPathGeometryFrame;
 }
@@ -283,19 +302,17 @@ nsSVGPathGeometryFrame::GetFrameForPoint
   nsSVGPathGeometryElement* content =
     static_cast<nsSVGPathGeometryElement*>(mContent);
 
   // Using ScreenReferenceDrawTarget() opens us to Moz2D backend specific hit-
   // testing bugs. Maybe we should use a BackendType::CAIRO DT for hit-testing
   // so that we get more consistent/backwards compatible results?
   RefPtr<DrawTarget> drawTarget =
     gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
-  RefPtr<PathBuilder> builder =
-    drawTarget->CreatePathBuilder(fillRule);
-  RefPtr<Path> path = content->BuildPath(builder);
+  RefPtr<Path> path = content->GetOrBuildPath(*drawTarget, fillRule);
   if (!path) {
     return nullptr; // no path, so we don't paint anything that can be hit
   }
 
   if (hitTestFlags & SVG_HIT_TEST_FILL) {
     isHit = path->ContainsPoint(ToPoint(aPoint), Matrix());
   }
   if (!isHit && (hitTestFlags & SVG_HIT_TEST_STROKE)) {
@@ -405,16 +422,17 @@ nsSVGPathGeometryFrame::NotifySVGChanged
   if (aFlags & COORD_CONTEXT_CHANGED) {
     // Stroke currently contributes to our mRect, which is why we have to take
     // account of stroke-width here. Note that we do not need to take account
     // of stroke-dashoffset since, although that can have a percentage value
     // that is resolved against our coordinate context, it does not affect our
     // mRect.
     if (static_cast<nsSVGPathGeometryElement*>(mContent)->GeometryDependsOnCoordCtx() ||
         StyleSVG()->mStrokeWidth.HasPercent()) {
+      static_cast<nsSVGPathGeometryElement*>(mContent)->ClearAnyCachedPath();
       nsSVGUtils::ScheduleReflowSVG(this);
     }
   }
 
   if ((aFlags & TRANSFORM_CHANGED) &&
       StyleSVGReset()->mVectorEffect ==
         NS_STYLE_VECTOR_EFFECT_NON_SCALING_STROKE) {
     // Stroke currently contributes to our mRect, and our stroke depends on
@@ -449,26 +467,25 @@ nsSVGPathGeometryFrame::GetBBoxContribut
   tmpDT = gfxPlatform::GetPlatform()->
     CreateDrawTargetForSurface(refSurf, IntSize(1, 1));
 #else
   tmpDT = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
 #endif
 
   FillRule fillRule = StyleSVG()->mFillRule == NS_STYLE_FILL_RULE_NONZERO
                         ? FillRule::FILL_WINDING : FillRule::FILL_EVEN_ODD;
-  RefPtr<PathBuilder> builder = tmpDT->CreatePathBuilder(fillRule);
-  RefPtr<Path> pathInUserSpace = element->BuildPath(builder);
+  RefPtr<Path> pathInUserSpace = element->GetOrBuildPath(*tmpDT, fillRule);
   if (!pathInUserSpace) {
     return bbox;
   }
   RefPtr<Path> pathInBBoxSpace;
   if (aToBBoxUserspace.IsIdentity()) {
     pathInBBoxSpace = pathInUserSpace;
   } else {
-    builder =
+    RefPtr<PathBuilder> builder =
       pathInUserSpace->TransformedCopyToBuilder(aToBBoxUserspace, fillRule);
     pathInBBoxSpace = builder->Finish();
     if (!pathInBBoxSpace) {
       return bbox;
     }
   }
 
   // Be careful when replacing the following logic to get the fill and stroke
@@ -658,23 +675,20 @@ nsSVGPathGeometryFrame::Render(gfxContex
   MOZ_ASSERT(renderMode == SVGAutoRenderState::NORMAL ||
              renderMode == SVGAutoRenderState::CLIP_MASK,
              "Unknown render mode");
 
   FillRule fillRule =
     nsSVGUtils::ToFillRule(renderMode == SVGAutoRenderState::NORMAL ?
                              StyleSVG()->mFillRule : StyleSVG()->mClipRule);
 
-  RefPtr<PathBuilder> builder = drawTarget->CreatePathBuilder(fillRule);
-  if (!builder) {
-    return;
-  }
+  nsSVGPathGeometryElement* element =
+    static_cast<nsSVGPathGeometryElement*>(mContent);
 
-  RefPtr<Path> path =
-    static_cast<nsSVGPathGeometryElement*>(mContent)->BuildPath(builder);
+  RefPtr<Path> path = element->GetOrBuildPath(*drawTarget, fillRule);
   if (!path) {
     return;
   }
 
   AntialiasMode aaMode =
     (StyleSVG()->mShapeRendering == NS_STYLE_SHAPE_RENDERING_OPTIMIZESPEED ||
      StyleSVG()->mShapeRendering == NS_STYLE_SHAPE_RENDERING_CRISPEDGES) ?
     AntialiasMode::NONE : AntialiasMode::SUBPIXEL;
@@ -710,17 +724,18 @@ nsSVGPathGeometryFrame::Render(gfxContex
     gfxMatrix userToOuterSVG;
     if (nsSVGUtils::GetNonScalingStrokeTransform(this, &userToOuterSVG)) {
       // We need to transform the path back into the appropriate ancestor
       // coordinate system, and paint it it that coordinate system, in order
       // for non-scaled stroke to paint correctly.
       gfxMatrix outerSVGToUser = userToOuterSVG;
       outerSVGToUser.Invert();
       aContext->Multiply(outerSVGToUser);
-      builder = path->TransformedCopyToBuilder(ToMatrix(userToOuterSVG), fillRule);
+      RefPtr<PathBuilder> builder =
+        path->TransformedCopyToBuilder(ToMatrix(userToOuterSVG), fillRule);
       path = builder->Finish();
     }
     GeneralPattern strokePattern;
     nsSVGUtils::MakeStrokePatternFor(this, aContext, &strokePattern, contextPaint);
     if (strokePattern.GetPattern()) {
       SVGContentUtils::AutoStrokeOptions strokeOptions;
       SVGContentUtils::GetStrokeOptions(&strokeOptions,
                                         static_cast<nsSVGElement*>(mContent),
--- a/layout/svg/nsSVGUtils.cpp
+++ b/layout/svg/nsSVGUtils.cpp
@@ -52,21 +52,28 @@
 #include "nsTextFrame.h"
 #include "SVGContentUtils.h"
 #include "mozilla/unused.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 using namespace mozilla::gfx;
 
+static bool sSVGPathCachingEnabled;
 static bool sSVGDisplayListHitTestingEnabled;
 static bool sSVGDisplayListPaintingEnabled;
 static bool sSVGNewGetBBoxEnabled;
 
 bool
+NS_SVGPathCachingEnabled()
+{
+  return sSVGPathCachingEnabled;
+}
+
+bool
 NS_SVGDisplayListHitTestingEnabled()
 {
   return sSVGDisplayListHitTestingEnabled;
 }
 
 bool
 NS_SVGDisplayListPaintingEnabled()
 {
@@ -132,16 +139,19 @@ SVGAutoRenderState::IsPaintingToWindow(D
     return static_cast<SVGAutoRenderState*>(state)->mPaintingToWindow;
   }
   return false;
 }
 
 void
 nsSVGUtils::Init()
 {
+  Preferences::AddBoolVarCache(&sSVGPathCachingEnabled,
+                               "svg.path-caching.enabled");
+
   Preferences::AddBoolVarCache(&sSVGDisplayListHitTestingEnabled,
                                "svg.display-lists.hit-testing.enabled");
 
   Preferences::AddBoolVarCache(&sSVGDisplayListPaintingEnabled,
                                "svg.display-lists.painting.enabled");
 
   Preferences::AddBoolVarCache(&sSVGNewGetBBoxEnabled,
                                "svg.new-getBBox.enabled");
--- a/layout/svg/nsSVGUtils.h
+++ b/layout/svg/nsSVGUtils.h
@@ -73,16 +73,17 @@ class SourceSurface;
 // In fact Macs can't even manage that
 #define NS_SVG_OFFSCREEN_MAX_DIMENSION 4096
 
 #define SVG_HIT_TEST_FILL        0x01
 #define SVG_HIT_TEST_STROKE      0x02
 #define SVG_HIT_TEST_CHECK_MRECT 0x04
 
 
+bool NS_SVGPathCachingEnabled();
 bool NS_SVGDisplayListHitTestingEnabled();
 bool NS_SVGDisplayListPaintingEnabled();
 bool NS_SVGNewGetBBoxEnabled();
 
 /**
  * Sometimes we need to distinguish between an empty box and a box
  * that contains an element that has no size e.g. a point at the origin.
  */
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2286,16 +2286,19 @@ pref("dom.ipc.plugins.flash.subprocess.c
 pref("dom.ipc.plugins.reportCrashURL", true);
 
 // How long we wait before unloading an idle plugin process.
 // Defaults to 30 seconds.
 pref("dom.ipc.plugins.unloadTimeoutSecs", 30);
 
 pref("dom.ipc.processCount", 1);
 
+// Enable caching of Moz2D Path objects for SVG geometry elements
+pref("svg.path-caching.enabled", true);
+
 // Enable the use of display-lists for SVG hit-testing and painting.
 pref("svg.display-lists.hit-testing.enabled", true);
 pref("svg.display-lists.painting.enabled", true);
 
 // Is support for the SVG 2 paint-order property enabled?
 pref("svg.paint-order.enabled", true);
 
 // Is support for the new marker features from SVG 2 enabled?  Currently