Move stats code to new layer (with configurable list of timers)

Bug: skia:
Change-Id: I3ca5c8c7047309983018339ec7b71b9aea5ee786
Reviewed-on: https://skia-review.googlesource.com/86921
Reviewed-by: Jim Van Verth <jvanverth@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 96c7623..3f1ce7f 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1846,6 +1846,7 @@
         "tools/viewer/ImageSlide.cpp",
         "tools/viewer/SKPSlide.cpp",
         "tools/viewer/SampleSlide.cpp",
+        "tools/viewer/StatsLayer.cpp",
         "tools/viewer/Viewer.cpp",
       ]
       libs = []
diff --git a/tools/sk_app/Window.cpp b/tools/sk_app/Window.cpp
index 6713c4c..29e4864 100644
--- a/tools/sk_app/Window.cpp
+++ b/tools/sk_app/Window.cpp
@@ -20,61 +20,49 @@
     fWindowContext = nullptr;
 }
 
-void Window::onBackendCreated() {
+void Window::visitLayers(std::function<void(Layer*)> visitor) {
     for (int i = 0; i < fLayers.count(); ++i) {
-        fLayers[i]->onBackendCreated();
+        if (fLayers[i]->fActive) {
+            visitor(fLayers[i]);
+        }
     }
 }
 
+bool Window::signalLayers(std::function<bool(Layer*)> visitor) {
+    for (int i = fLayers.count() - 1; i >= 0; --i) {
+        if (fLayers[i]->fActive && visitor(fLayers[i])) {
+            return true;
+        }
+    }
+    return false;
+}
+
+void Window::onBackendCreated() {
+    this->visitLayers([](Layer* layer) { layer->onBackendCreated(); });
+}
+
 bool Window::onChar(SkUnichar c, uint32_t modifiers) {
-    for (int i = fLayers.count() - 1; i >= 0; --i) {
-        if (fLayers[i]->onChar(c, modifiers)) {
-            return true;
-        }
-    }
-    return false;
+    return this->signalLayers([=](Layer* layer) { return layer->onChar(c, modifiers); });
 }
 
 bool Window::onKey(Key key, InputState state, uint32_t modifiers) {
-    for (int i = fLayers.count() - 1; i >= 0; --i) {
-        if (fLayers[i]->onKey(key, state, modifiers)) {
-            return true;
-        }
-    }
-    return false;
+    return this->signalLayers([=](Layer* layer) { return layer->onKey(key, state, modifiers); });
 }
 
 bool Window::onMouse(int x, int y, InputState state, uint32_t modifiers) {
-    for (int i = fLayers.count() - 1; i >= 0; --i) {
-        if (fLayers[i]->onMouse(x, y, state, modifiers)) {
-            return true;
-        }
-    }
-    return false;
+    return this->signalLayers([=](Layer* layer) { return layer->onMouse(x, y, state, modifiers); });
 }
 
 bool Window::onMouseWheel(float delta, uint32_t modifiers) {
-    for (int i = fLayers.count() - 1; i >= 0; --i) {
-        if (fLayers[i]->onMouseWheel(delta, modifiers)) {
-            return true;
-        }
-    }
-    return false;
+    return this->signalLayers([=](Layer* layer) { return layer->onMouseWheel(delta, modifiers); });
 }
 
 bool Window::onTouch(intptr_t owner, InputState state, float x, float y) {
-    for (int i = fLayers.count() - 1; i >= 0; --i) {
-        if (fLayers[i]->onTouch(owner, state, x, y)) {
-            return true;
-        }
-    }
-    return false;
+    return this->signalLayers([=](Layer* layer) { return layer->onTouch(owner, state, x, y); });
 }
 
 void Window::onUIStateChanged(const SkString& stateName, const SkString& stateValue) {
-    for (int i = 0; i < fLayers.count(); ++i) {
-        fLayers[i]->onUIStateChanged(stateName, stateValue);
-    }
+    this->visitLayers([=](Layer* layer) { layer->onUIStateChanged(stateName, stateValue); });
 }
 
 void Window::onPaint() {
@@ -87,12 +75,8 @@
         // draw into the canvas of this surface
         SkCanvas* canvas = backbuffer->getCanvas();
 
-        for (int i = 0; i < fLayers.count(); ++i) {
-            fLayers[i]->onPrePaint();
-        }
-        for (int i = 0; i < fLayers.count(); ++i) {
-            fLayers[i]->onPaint(canvas);
-        }
+        this->visitLayers([](Layer* layer) { layer->onPrePaint(); });
+        this->visitLayers([=](Layer* layer) { layer->onPaint(canvas); });
 
         canvas->flush();
 
diff --git a/tools/sk_app/Window.h b/tools/sk_app/Window.h
index b541e24..927d8a9 100644
--- a/tools/sk_app/Window.h
+++ b/tools/sk_app/Window.h
@@ -132,8 +132,12 @@
 
     class Layer {
     public:
+        Layer() : fActive(true) {}
         virtual ~Layer() = default;
 
+        bool getActive() { return fActive; }
+        void setActive(bool active) { fActive = active; }
+
         // return value of 'true' means 'I have handled this event'
         virtual void onBackendCreated() {}
         virtual void onAttach(Window* window) {}
@@ -145,6 +149,10 @@
         virtual void onUIStateChanged(const SkString& stateName, const SkString& stateValue) {}
         virtual void onPrePaint() {}
         virtual void onPaint(SkCanvas*) {}
+
+    private:
+        friend class Window;
+        bool fActive;
     };
 
     void pushLayer(Layer* layer) {
@@ -189,6 +197,9 @@
     void markInvalProcessed();
 
     bool fIsContentInvalidated = false;  // use this to avoid duplicate invalidate events
+
+    void visitLayers(std::function<void(Layer*)> visitor);
+    bool signalLayers(std::function<bool(Layer*)> visitor);
 };
 
 }   // namespace sk_app
diff --git a/tools/viewer/StatsLayer.cpp b/tools/viewer/StatsLayer.cpp
new file mode 100644
index 0000000..75226fd
--- /dev/null
+++ b/tools/viewer/StatsLayer.cpp
@@ -0,0 +1,139 @@
+/*
+* Copyright 2017 Google Inc.
+*
+* Use of this source code is governed by a BSD-style license that can be
+* found in the LICENSE file.
+*/
+
+#include "StatsLayer.h"
+
+#include "SkCanvas.h"
+#include "SkString.h"
+#include "SkTime.h"
+
+StatsLayer::StatsLayer()
+    : fCurrentMeasurement(0)
+    , fCumulativeMeasurementTime(0)
+    , fCumulativeMeasurementCount(0) {}
+
+void StatsLayer::resetMeasurements() {
+    for (int i = 0; i < fTimers.count(); ++i) {
+        memset(fTimers[i].fTimes, 0, sizeof(fTimers[i].fTimes));
+    }
+    fCurrentMeasurement = 0;
+    fCumulativeMeasurementTime = 0;
+    fCumulativeMeasurementCount = 0;
+}
+
+StatsLayer::Timer StatsLayer::addTimer(const char* label, SkColor color, SkColor labelColor) {
+    Timer newTimer = fTimers.count();
+    TimerData& newData = fTimers.push_back();
+    memset(newData.fTimes, 0, sizeof(newData.fTimes));
+    newData.fLabel = label;
+    newData.fColor = color;
+    newData.fLabelColor = labelColor ? labelColor : color;
+    return newTimer;
+}
+
+void StatsLayer::beginTiming(Timer timer) {
+    fTimers[timer].fTimes[fCurrentMeasurement] -= SkTime::GetMSecs();
+}
+
+void StatsLayer::endTiming(Timer timer) {
+    fTimers[timer].fTimes[fCurrentMeasurement] += SkTime::GetMSecs();
+}
+
+double StatsLayer::getLastTime(Timer timer) {
+    int idx = (fCurrentMeasurement + (kMeasurementCount - 1)) & (kMeasurementCount - 1);
+    return fTimers[timer].fTimes[idx];
+}
+
+void StatsLayer::onPaint(SkCanvas* canvas) {
+    // Advance our timing bookkeeping
+    for (int i = 0; i < fTimers.count(); ++i) {
+        fCumulativeMeasurementTime += fTimers[i].fTimes[fCurrentMeasurement];
+    }
+    fCumulativeMeasurementCount++;
+    fCurrentMeasurement = (fCurrentMeasurement + 1) & (kMeasurementCount - 1);
+    SkASSERT(fCurrentMeasurement < kMeasurementCount);
+    for (int i = 0; i < fTimers.count(); ++i) {
+        fTimers[i].fTimes[fCurrentMeasurement] = 0;
+    }
+
+    // Now draw everything
+    static const float kPixelPerMS = 2.0f;
+    static const int kDisplayWidth = 192;
+    static const int kGraphHeight = 100;
+    static const int kTextHeight = 60;
+    static const int kDisplayHeight = kGraphHeight + kTextHeight;
+    static const int kDisplayPadding = 10;
+    static const int kGraphPadding = 3;
+    static const SkScalar kBaseMS = 1000.f / 60.f;  // ms/frame to hit 60 fps
+
+    SkISize canvasSize = canvas->getBaseLayerSize();
+    SkRect rect = SkRect::MakeXYWH(SkIntToScalar(canvasSize.fWidth-kDisplayWidth-kDisplayPadding),
+                                   SkIntToScalar(kDisplayPadding),
+                                   SkIntToScalar(kDisplayWidth), SkIntToScalar(kDisplayHeight));
+    SkPaint paint;
+    canvas->save();
+
+    paint.setColor(SK_ColorBLACK);
+    canvas->drawRect(rect, paint);
+    // draw the 16ms line
+    paint.setColor(SK_ColorLTGRAY);
+    canvas->drawLine(rect.fLeft, rect.fBottom - kBaseMS*kPixelPerMS,
+                     rect.fRight, rect.fBottom - kBaseMS*kPixelPerMS, paint);
+    paint.setColor(SK_ColorRED);
+    paint.setStyle(SkPaint::kStroke_Style);
+    canvas->drawRect(rect, paint);
+    paint.setStyle(SkPaint::kFill_Style);
+
+    int x = SkScalarTruncToInt(rect.fLeft) + kGraphPadding;
+    const int xStep = 3;
+    int i = fCurrentMeasurement;
+    double ms = 0;
+    SkTDArray<double> sumTimes;
+    sumTimes.setCount(fTimers.count());
+    memset(sumTimes.begin(), 0, sumTimes.count() * sizeof(double));
+    int count = 0;
+    do {
+        int startY = SkScalarTruncToInt(rect.fBottom);
+        double inc = 0;
+        for (int timer = 0; timer < fTimers.count(); ++timer) {
+            int height = (int)(fTimers[timer].fTimes[i] * kPixelPerMS + 0.5);
+            int endY = SkTMax(startY - height, kDisplayPadding + kTextHeight);
+            paint.setColor(fTimers[timer].fColor);
+            canvas->drawLine(SkIntToScalar(x), SkIntToScalar(startY),
+                             SkIntToScalar(x), SkIntToScalar(endY), paint);
+            startY = endY;
+            inc += fTimers[timer].fTimes[i];
+            sumTimes[timer] += fTimers[timer].fTimes[i];
+        }
+
+        if (inc > 0) {
+            ms += inc;
+            ++count;
+        }
+
+        i++;
+        i &= (kMeasurementCount - 1);  // fast mod
+        x += xStep;
+    } while (i != fCurrentMeasurement);
+
+    paint.setTextSize(16);
+    SkString mainString;
+    mainString.appendf("%4.3f ms -> %4.3f ms", ms / SkTMax(1, count),
+                  fCumulativeMeasurementTime / SkTMax(1, fCumulativeMeasurementCount));
+    paint.setColor(SK_ColorWHITE);
+    canvas->drawString(mainString.c_str(), rect.fLeft + 3, rect.fTop + 14, paint);
+
+    for (int timer = 0; timer < fTimers.count(); ++timer) {
+        SkString str;
+        str.appendf("%s: %4.3f ms", fTimers[timer].fLabel.c_str(),
+                    sumTimes[timer] / SkTMax(1, count));
+        paint.setColor(fTimers[timer].fLabelColor);
+        canvas->drawString(str, rect.fLeft + 3, rect.fTop + 28 + (14 * timer), paint);
+    }
+
+    canvas->restore();
+}
diff --git a/tools/viewer/StatsLayer.h b/tools/viewer/StatsLayer.h
new file mode 100644
index 0000000..a99bd43
--- /dev/null
+++ b/tools/viewer/StatsLayer.h
@@ -0,0 +1,43 @@
+/*
+* Copyright 2017 Google Inc.
+*
+* Use of this source code is governed by a BSD-style license that can be
+* found in the LICENSE file.
+*/
+
+#ifndef StatsLayer_DEFINED
+#define StatsLayer_DEFINED
+
+#include "SkColor.h"
+#include "SkString.h"
+#include "sk_app/Window.h"
+
+class StatsLayer : public sk_app::Window::Layer {
+public:
+    StatsLayer();
+    void resetMeasurements();
+
+    typedef int Timer;
+
+    Timer addTimer(const char* label, SkColor color, SkColor labelColor = 0);
+    void beginTiming(Timer);
+    void endTiming(Timer);
+    double getLastTime(Timer);
+
+    void onPaint(SkCanvas* canvas) override;
+
+private:
+    static const int kMeasurementCount = 1 << 6;  // should be power of 2 for fast mod
+    struct TimerData {
+        double fTimes[kMeasurementCount];
+        SkString fLabel;
+        SkColor fColor;
+        SkColor fLabelColor;
+    };
+    SkTArray<TimerData> fTimers;
+    int fCurrentMeasurement;
+    double fCumulativeMeasurementTime;
+    int fCumulativeMeasurementCount;
+};
+
+#endif
diff --git a/tools/viewer/Viewer.cpp b/tools/viewer/Viewer.cpp
index edf37cd..0baa409 100644
--- a/tools/viewer/Viewer.cpp
+++ b/tools/viewer/Viewer.cpp
@@ -30,7 +30,6 @@
 #include "SkSurface.h"
 #include "SkTaskGroup.h"
 #include "SkThreadedBMPDevice.h"
-#include "SkTime.h"
 
 #include "imgui.h"
 
@@ -171,11 +170,7 @@
 const char* kRefreshStateName = "Refresh";
 
 Viewer::Viewer(int argc, char** argv, void* platformData)
-    : fCurrentMeasurement(0)
-    , fCumulativeMeasurementTime(0)
-    , fCumulativeMeasurementCount(0)
-    , fDisplayStats(false)
-    , fRefresh(false)
+    : fRefresh(false)
     , fSaveToSKP(false)
     , fShowImGuiDebugWindow(false)
     , fShowSlidePicker(false)
@@ -204,10 +199,6 @@
     gPathRendererNames[GpuPathRenderers::kTessellating] = "Tessellating";
     gPathRendererNames[GpuPathRenderers::kNone] = "Software masks";
 
-    memset(fPaintTimes, 0, sizeof(fPaintTimes));
-    memset(fFlushTimes, 0, sizeof(fFlushTimes));
-    memset(fAnimateTimes, 0, sizeof(fAnimateTimes));
-
     SkDebugf("Command line arguments: ");
     for (int i = 1; i < argc; ++i) {
         SkDebugf("%s ", argv[i]);
@@ -230,9 +221,16 @@
     SetCtxOptionsFromCommonFlags(&displayParams.fGrContextOptions);
     fWindow->setRequestedDisplayParams(displayParams);
 
+    // Configure timers
+    fStatsLayer.setActive(false);
+    fAnimateTimer = fStatsLayer.addTimer("Animate", SK_ColorMAGENTA, 0xffff66ff);
+    fPaintTimer = fStatsLayer.addTimer("Paint", SK_ColorGREEN);
+    fFlushTimer = fStatsLayer.addTimer("Flush", SK_ColorRED, 0xffff6666);
+
     // register callbacks
     fCommands.attach(fWindow);
     fWindow->pushLayer(this);
+    fWindow->pushLayer(&fStatsLayer);
     fWindow->pushLayer(&fImGuiLayer);
 
     // add key-bindings
@@ -261,11 +259,11 @@
         fWindow->inval();
     });
     fCommands.addCommand('s', "Overlays", "Toggle stats display", [this]() {
-        this->fDisplayStats = !this->fDisplayStats;
+        fStatsLayer.setActive(!fStatsLayer.getActive());
         fWindow->inval();
     });
     fCommands.addCommand('0', "Overlays", "Reset stats", [this]() {
-        this->resetMeasurements();
+        fStatsLayer.resetMeasurements();
         this->updateTitle();
         fWindow->inval();
     });
@@ -593,15 +591,6 @@
     }
 }
 
-void Viewer::resetMeasurements() {
-    memset(fPaintTimes, 0, sizeof(fPaintTimes));
-    memset(fFlushTimes, 0, sizeof(fFlushTimes));
-    memset(fAnimateTimes, 0, sizeof(fAnimateTimes));
-    fCurrentMeasurement = 0;
-    fCumulativeMeasurementTime = 0;
-    fCumulativeMeasurementCount = 0;
-}
-
 void Viewer::setupCurrentSlide(int previousSlide) {
     if (fCurrentSlide == previousSlide) {
         return; // no change; do nothing
@@ -632,7 +621,7 @@
         fSlides[previousSlide]->unload();
     }
 
-    this->resetMeasurements();
+    fStatsLayer.resetMeasurements();
 
     fWindow->inval();
 }
@@ -673,6 +662,7 @@
     // re-register callbacks
     fCommands.attach(fWindow);
     fWindow->pushLayer(this);
+    fWindow->pushLayer(&fStatsLayer);
     fWindow->pushLayer(&fImGuiLayer);
 
     // Don't allow the window to re-attach. If we're in MSAA mode, the params we grabbed above
@@ -788,15 +778,15 @@
     slideCanvas->clear(SK_ColorWHITE);
     slideCanvas->concat(computeMatrix());
     // Time the painting logic of the slide
-    double startTime = SkTime::GetMSecs();
+    fStatsLayer.beginTiming(fPaintTimer);
     fSlides[fCurrentSlide]->draw(slideCanvas);
-    fPaintTimes[fCurrentMeasurement] = SkTime::GetMSecs() - startTime;
+    fStatsLayer.endTiming(fPaintTimer);
     slideCanvas->restoreToCount(count);
 
     // Force a flush so we can time that, too
-    startTime = SkTime::GetMSecs();
+    fStatsLayer.beginTiming(fFlushTimer);
     slideCanvas->flush();
-    fFlushTimes[fCurrentMeasurement] = SkTime::GetMSecs() - startTime;
+    fStatsLayer.endTiming(fFlushTimer);
 
     // If we rendered offscreen, snap an image and push the results to the window's canvas
     if (offscreenSurface) {
@@ -816,7 +806,7 @@
     this->updateTitle();
     this->updateUIState();
     this->setupCurrentSlide(-1);
-    this->resetMeasurements();
+    fStatsLayer.resetMeasurements();
     fWindow->show();
     fWindow->inval();
 }
@@ -824,18 +814,6 @@
 void Viewer::onPaint(SkCanvas* canvas) {
     this->drawSlide(canvas);
 
-    // Advance our timing bookkeeping
-    fCumulativeMeasurementTime += fAnimateTimes[fCurrentMeasurement] +
-                                  fPaintTimes[fCurrentMeasurement] +
-                                  fFlushTimes[fCurrentMeasurement];
-    fCumulativeMeasurementCount++;
-    fCurrentMeasurement = (fCurrentMeasurement + 1) & (kMeasurementCount - 1);
-    SkASSERT(fCurrentMeasurement < kMeasurementCount);
-
-    // Draw any overlays or UI that we don't want timed
-    if (fDisplayStats) {
-        drawStats(canvas);
-    }
     fCommands.drawHelp(canvas);
 
     this->drawImGui();
@@ -893,102 +871,6 @@
     return true;
 }
 
-void Viewer::drawStats(SkCanvas* canvas) {
-    static const float kPixelPerMS = 2.0f;
-    static const int kDisplayWidth = 192;
-    static const int kGraphHeight = 100;
-    static const int kTextHeight = 60;
-    static const int kDisplayHeight = kGraphHeight + kTextHeight;
-    static const int kDisplayPadding = 10;
-    static const int kGraphPadding = 3;
-    static const SkScalar kBaseMS = 1000.f / 60.f;  // ms/frame to hit 60 fps
-
-    SkISize canvasSize = canvas->getBaseLayerSize();
-    SkRect rect = SkRect::MakeXYWH(SkIntToScalar(canvasSize.fWidth-kDisplayWidth-kDisplayPadding),
-                                   SkIntToScalar(kDisplayPadding),
-                                   SkIntToScalar(kDisplayWidth), SkIntToScalar(kDisplayHeight));
-    SkPaint paint;
-    canvas->save();
-
-    paint.setColor(SK_ColorBLACK);
-    canvas->drawRect(rect, paint);
-    // draw the 16ms line
-    paint.setColor(SK_ColorLTGRAY);
-    canvas->drawLine(rect.fLeft, rect.fBottom - kBaseMS*kPixelPerMS,
-                     rect.fRight, rect.fBottom - kBaseMS*kPixelPerMS, paint);
-    paint.setColor(SK_ColorRED);
-    paint.setStyle(SkPaint::kStroke_Style);
-    canvas->drawRect(rect, paint);
-    paint.setStyle(SkPaint::kFill_Style);
-
-    int x = SkScalarTruncToInt(rect.fLeft) + kGraphPadding;
-    const int xStep = 3;
-    int i = fCurrentMeasurement;
-    double ms = 0;
-    double animateMS = 0;
-    double paintMS = 0;
-    double flushMS = 0;
-    int count = 0;
-    do {
-        // Round to nearest values
-        int animateHeight = (int)(fAnimateTimes[i] * kPixelPerMS + 0.5);
-        int paintHeight = (int)(fPaintTimes[i] * kPixelPerMS + 0.5);
-        int flushHeight = (int)(fFlushTimes[i] * kPixelPerMS + 0.5);
-        int startY = SkScalarTruncToInt(rect.fBottom);
-        int endY = SkTMax(startY - flushHeight, kDisplayPadding + kTextHeight);
-        paint.setColor(SK_ColorRED);
-        canvas->drawLine(SkIntToScalar(x), SkIntToScalar(startY),
-                         SkIntToScalar(x), SkIntToScalar(endY), paint);
-        startY = endY;
-        endY = SkTMax(startY - paintHeight, kDisplayPadding + kTextHeight);
-        paint.setColor(SK_ColorGREEN);
-        canvas->drawLine(SkIntToScalar(x), SkIntToScalar(startY),
-                         SkIntToScalar(x), SkIntToScalar(endY), paint);
-        startY = endY;
-        endY = SkTMax(startY - animateHeight, kDisplayPadding + kTextHeight);
-        paint.setColor(SK_ColorMAGENTA);
-        canvas->drawLine(SkIntToScalar(x), SkIntToScalar(startY),
-                         SkIntToScalar(x), SkIntToScalar(endY), paint);
-
-        double inc = fAnimateTimes[i] + fPaintTimes[i] + fFlushTimes[i];
-        if (inc > 0) {
-            ms += inc;
-            animateMS += fAnimateTimes[i];
-            paintMS += fPaintTimes[i];
-            flushMS += fFlushTimes[i];
-            ++count;
-        }
-
-        i++;
-        i &= (kMeasurementCount - 1);  // fast mod
-        x += xStep;
-    } while (i != fCurrentMeasurement);
-
-    paint.setTextSize(16);
-    SkString mainString;
-    mainString.appendf("%4.3f ms -> %4.3f ms", ms / SkTMax(1, count),
-                  fCumulativeMeasurementTime / SkTMax(1, fCumulativeMeasurementCount));
-    paint.setColor(SK_ColorWHITE);
-    canvas->drawString(mainString.c_str(), rect.fLeft+3, rect.fTop + 14, paint);
-
-    SkString animateString;
-    animateString.appendf("Animate: %4.3f ms", animateMS / SkTMax(1, count));
-    paint.setColor(0xffff66ff);    // pure magenta is hard to read
-    canvas->drawString(animateString.c_str(), rect.fLeft+3, rect.fTop + 28, paint);
-
-    SkString paintString;
-    paintString.appendf("Paint: %4.3f ms", paintMS / SkTMax(1, count));
-    paint.setColor(SK_ColorGREEN);
-    canvas->drawString(paintString.c_str(), rect.fLeft+3, rect.fTop + 42, paint);
-
-    SkString flushString;
-    flushString.appendf("Flush: %4.3f ms", flushMS / SkTMax(1, count));
-    paint.setColor(0xffff6666);    // pure red is hard to read
-    canvas->drawString(flushString.c_str(), rect.fLeft+3, rect.fTop + 56, paint);
-
-    canvas->restore();
-}
-
 static ImVec2 ImGui_DragPrimary(const char* label, float* x, float* y,
                                 const ImVec2& pos, const ImVec2& size) {
     // Transform primaries ([0, 0] - [0.8, 0.9]) to screen coords (including Y-flip)
@@ -1282,13 +1164,13 @@
     }
     fDeferredActions.reset();
 
-    double startTime = SkTime::GetMSecs();
+    fStatsLayer.beginTiming(fAnimateTimer);
     fAnimTimer.updateTime();
     bool animateWantsInval = fSlides[fCurrentSlide]->animate(fAnimTimer);
-    fAnimateTimes[fCurrentMeasurement] = SkTime::GetMSecs() - startTime;
+    fStatsLayer.endTiming(fAnimateTimer);
 
     ImGuiIO& io = ImGui::GetIO();
-    if (animateWantsInval || fDisplayStats || fRefresh || io.MetricsActiveWindows) {
+    if (animateWantsInval || fStatsLayer.getActive() || fRefresh || io.MetricsActiveWindows) {
         fWindow->inval();
     }
 }
@@ -1378,12 +1260,12 @@
     // FPS state
     Json::Value fpsState(Json::objectValue);
     fpsState[kName] = kFpsStateName;
-    int idx = (fCurrentMeasurement + (kMeasurementCount - 1)) & (kMeasurementCount - 1);
+    double animTime = fStatsLayer.getLastTime(fAnimateTimer);
+    double paintTime = fStatsLayer.getLastTime(fPaintTimer);
+    double flushTime = fStatsLayer.getLastTime(fFlushTimer);
     fpsState[kValue] = SkStringPrintf("%8.3lf ms\n\nA %8.3lf\nP %8.3lf\nF%8.3lf",
-                                      fAnimateTimes[idx] + fPaintTimes[idx] + fFlushTimes[idx],
-                                      fAnimateTimes[idx],
-                                      fPaintTimes[idx],
-                                      fFlushTimes[idx]).c_str();
+                                      animTime + paintTime + flushTime,
+                                      animTime, paintTime, flushTime).c_str();
     fpsState[kOptions] = Json::Value(Json::arrayValue);
 
     Json::Value state(Json::arrayValue);
diff --git a/tools/viewer/Viewer.h b/tools/viewer/Viewer.h
index 2ab3de5..7d4980c 100644
--- a/tools/viewer/Viewer.h
+++ b/tools/viewer/Viewer.h
@@ -18,6 +18,7 @@
 #include "SkJSONCPP.h"
 #include "SkTouchGesture.h"
 #include "Slide.h"
+#include "StatsLayer.h"
 
 class SkCanvas;
 
@@ -51,12 +52,10 @@
     void setStartupSlide();
     void setupCurrentSlide(int previousSlide);
     void listNames();
-    void resetMeasurements();
 
     void updateUIState();
 
     void drawSlide(SkCanvas* canvs);
-    void drawStats(SkCanvas* canvas);
     void drawImGui();
 
     void changeZoomLevel(float delta);
@@ -68,19 +67,15 @@
 
     sk_app::Window*        fWindow;
 
-    static const int kMeasurementCount = 1 << 6;  // should be power of 2 for fast mod
-    double fPaintTimes[kMeasurementCount];
-    double fFlushTimes[kMeasurementCount];
-    double fAnimateTimes[kMeasurementCount];
-    int fCurrentMeasurement;
-    double fCumulativeMeasurementTime;
-    int fCumulativeMeasurementCount;
+    StatsLayer             fStatsLayer;
+    StatsLayer::Timer      fPaintTimer;
+    StatsLayer::Timer      fFlushTimer;
+    StatsLayer::Timer      fAnimateTimer;
 
     SkAnimTimer            fAnimTimer;
     SkTArray<sk_sp<Slide>> fSlides;
     int                    fCurrentSlide;
 
-    bool                   fDisplayStats;
     bool                   fRefresh; // whether to continuously refresh for measuring render time
 
     bool                   fSaveToSKP;