Permalink
Browse files

Merge pull request #3473 from circleci/om-instrumentation

Om instrumentation
  • Loading branch information...
dwwoelfel committed Oct 12, 2014
2 parents 99e3b26 + 209a664 commit 2f58976e57000c448a22440e69573c4f0b7c581b
Showing with 141 additions and 15 deletions.
  1. +1 −0 frontend/components/app.cljs
  2. +18 −13 frontend/core.cljs
  3. +122 −2 frontend/instrumentation.cljs
@@ -29,6 +29,7 @@
[frontend.components.landing :as landing]
[frontend.components.org-settings :as org-settings]
[frontend.components.common :as common]
+ [frontend.instrumentation :as instrumentation]
[frontend.state :as state]
[frontend.utils :as utils :include-macros true]
[frontend.utils.seq :refer [dissoc-in]]
View
@@ -18,7 +18,7 @@
[frontend.controllers.ws :as ws-con]
[frontend.controllers.errors :as errors-con]
[frontend.env :as env]
- [frontend.instrumentation :refer [wrap-api-instrumentation]]
+ [frontend.instrumentation :as instrumentation :refer [wrap-api-instrumentation]]
[frontend.state :as state]
[goog.events]
[om.core :as om :include-macros true]
@@ -174,22 +174,25 @@
mya))
-(defn install-om [state container comms]
+(defn install-om [state container comms instrument?]
(om/root
- app/app
- state
- {:target container
- :shared {:comms comms
- :timer-atom (setup-timer-atom)
- :_app-state-do-not-use state}}))
+ app/app
+ state
+ (merge {:target container
+ :shared {:comms comms
+ :timer-atom (setup-timer-atom)
+ :_app-state-do-not-use state}}
+ (when instrument?
+ {:instrument (fn [f cursor m]
+ (om/build* f cursor (assoc m :descriptor instrumentation/instrumentation-methods)))}))))
(defn find-top-level-node []
(sel1 :body))
(defn find-app-container [top-level-node]
(sel1 top-level-node "#om-app"))
-(defn main [state top-level-node history-imp]
+(defn main [state top-level-node history-imp instrument?]
(let [comms (:comms @state)
container (find-app-container top-level-node)
uri-path (.getPath utils/parsed-uri)
@@ -201,7 +204,7 @@
ws-tap (chan)
errors-tap (chan)]
(routes/define-routes! state)
- (install-om state container comms)
+ (install-om state container comms instrument?)
(async/tap (:controls-mult comms) controls-tap)
(async/tap (:nav-mult comms) nav-tap)
@@ -245,11 +248,13 @@
(mixpanel/set-existing-user)
(let [state (app-state)
top-level-node (find-top-level-node)
- history-imp (history/new-history-imp top-level-node)]
+ history-imp (history/new-history-imp top-level-node)
+ instrument? (or (env/development?) (get-in @state [:current-user :admin]))]
;; globally define the state so that we can get to it for debugging
(def debug-state state)
+ (when instrument? (instrumentation/setup-component-stats!))
(browser-settings/setup! state)
- (main state top-level-node history-imp)
+ (main state top-level-node history-imp instrument?)
(if-let [error-status (get-in @state [:render-context :status])]
;; error codes from the server get passed as :status in the render-context
(put! (get-in @state [:comms :nav]) [:error {:status error-status}])
@@ -277,7 +282,7 @@
(println "value for" test-name "is now" (get-in @debug-state test-path))))
(defn reinstall-om! []
- (install-om debug-state (find-app-container (find-top-level-node)) (:comms @debug-state)))
+ (install-om debug-state (find-app-container (find-top-level-node)) (:comms @debug-state) true))
(defn refresh-css! []
(let [is-app-css? #(re-matches #"/assets/css/app.*?\.css(?:\.less)?" (dommy/attr % :href))
@@ -1,6 +1,14 @@
(ns frontend.instrumentation
- (:require [frontend.state :as state]
- [frontend.utils :as utils :include-macros true]))
+ (:require [cljs-time.core :as time]
+ [cljs-time.format :as time-format]
+ [dommy.core :as dommy]
+ [frontend.components.key-queue :as keyq]
+ [frontend.datetime :as datetime]
+ [frontend.state :as state]
+ [frontend.utils :as utils :include-macros true]
+ [om.core :as om :include-macros true]
+ [om.dom :as dom :include-macros true])
+ (:use-macros [dommy.macros :only [node sel sel1]]))
(defn wrap-api-instrumentation [handler api-data]
(fn [state]
@@ -19,3 +27,115 @@
(catch :default e
(utils/merror e)
state)))))
+
+;; map of react-id to component render stats, e.g.
+;; {"0.1.1" {:last-will-update <time 3pm> :display-name "App" :last-did-update <time 3pm> :render-ms [10 39 20 40]}}
+(def component-stats (atom {}))
+
+(defn react-id [x]
+ (let [id (.-_rootNodeID x)]
+ (assert id)
+ id))
+
+(defn wrap-will-update
+ "Tracks last call time of componentWillUpdate for each component, then calls
+ the original componentWillUpdate."
+ [f]
+ (fn [next-props next-state]
+ (this-as this
+ (swap! component-stats update-in [(react-id this)]
+ merge {:display-name ((aget this "getDisplayName"))
+ :last-will-update (time/now)})
+ (.call f this next-props next-state))))
+
+(defn wrap-did-update
+ "Tracks last call time of componentDidUpdate for each component and updates
+ the render times (using start time provided by wrap-will-update), then
+ calls the original componentDidUpdate."
+ [f]
+ (fn [prev-props prev-state]
+ (this-as this
+ (swap! component-stats update-in [(react-id this)]
+ (fn [stats]
+ (let [now (time/now)]
+ (-> stats
+ (assoc :last-did-update now)
+ (update-in [:render-ms] (fnil conj [])
+ (if (time/after? now (:last-will-update stats))
+ (time/in-millis (time/interval (:last-will-update stats) now))
+ 0))))))
+ (.call f this prev-props prev-state))))
+
+(def instrumentation-methods
+ (om/specify-state-methods!
+ (-> om/pure-methods
+ (update-in [:componentWillUpdate] wrap-will-update)
+ (update-in [:componentDidUpdate] wrap-did-update)
+ (clj->js))))
+
+(defn avg [coll]
+ (/ (reduce + coll)
+ (count coll)))
+
+(defn std-dev [coll]
+ (let [a (avg coll)]
+ (Math/sqrt (avg (map #(Math/pow (- % a) 2) coll)))))
+
+(defn stats-view [data owner]
+ (om/component
+ (dom/figure nil
+ (om/build keyq/KeyboardHandler {}
+ {:opts {:keymap (atom {["ctrl+k"] #(om/transact! data (constantly {}))
+ ["ctrl+j"] #(om/update-state! owner :shown? not)})}})
+
+ (when (om/get-state owner :shown?)
+ (let [stats (map (fn [[display-name renders]]
+ (let [times (mapcat :render-ms renders)]
+ {:display-name (or display-name "Unknown")
+ :render-count (count times)
+ :last-will-update (last (sort (map :last-will-update renders)))
+ :last-render-ms (last (:render-ms (last (sort-by :last-did-update renders))))
+ :average-render-ms (when (seq times) (int (avg times)))
+ :max-render-ms (when (seq times) (apply max times))
+ :min-render-ms (when (seq times) (apply min times))
+ :std-dev (when (seq times) (int (std-dev times)))}))
+ (reduce (fn [acc [react-id data]]
+ (update-in acc [(:display-name data)] (fnil conj []) data))
+ {} data))]
+ (dom/div #js {:className "admin-stats"}
+ (dom/table nil
+ (dom/caption nil "Component render stats, sorted by last update. Clicks go right through like it's not there. Ctrl+j to toggle, Ctrl+k to clear.")
+ (dom/thead nil
+ (dom/tr nil
+ (dom/th nil "component")
+ (dom/th #js {:className "number"} "count")
+ (dom/th #js {:className "number"} "last-update")
+ (dom/th #js {:className "number"} "last-ms")
+ (dom/th #js {:className "number"} "average-ms")
+ (dom/th #js {:className "number"} "max-ms")
+ (dom/th #js {:className "number"} "min-ms")
+ (dom/th #js {:className "number"} "std-ms")))
+ (apply dom/tbody nil
+ (for [{:keys [display-name last-will-update average-render-ms
+ max-render-ms min-render-ms std-dev render-count
+ last-render-ms] :as stat} (reverse (sort-by :last-will-update stats))]
+ (dom/tr nil
+ (dom/td nil display-name)
+ (dom/td #js {:className "number" } render-count)
+ (dom/td #js {:className "number" }
+ (when last-will-update
+ (time-format/unparse (time-format/formatters :hour-minute-second)
+ last-will-update)))
+ (dom/td #js {:className "number" } last-render-ms)
+ (dom/td #js {:className "number" } average-render-ms)
+ (dom/td #js {:className "number" } max-render-ms)
+ (dom/td #js {:className "number" } min-render-ms)
+ (dom/td #js {:className "number" } std-dev)))))))))))
+
+(defn setup-component-stats! []
+ (let [stats-node (node [:div.om-instrumentation])]
+ (dommy/append! (sel1 :body) stats-node)
+ (om/root
+ stats-view
+ component-stats
+ {:target stats-node})))

0 comments on commit 2f58976

Please sign in to comment.