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 208825 75c93e9a7c970d66c8c1af12d400fd45e1871e96
parent 208824 41bad654774b1ba60c91dfe0a5246f416c72fe68
child 208826 2374287a24ab3d60373eea2d4349c5b8129be182
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerslongsonr
bugs932762
milestone35.0a1
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