4
votes

I want to calculate the rate of events, for example, key press events, and display it. I also want to start rate calculation only after first event happened. Another requirement is that rate calculation should stop after certain amount of key press events. My current approach to that is

(defn keypress-chan
  "Returns a channel that receives keys"
  []
  (let [out (chan) handler (fn [e] (put! out (.-key e)))]
    (js/document.addEventListener "keypress" handler false)
    out))

(defn tick
  "Pushes to `out` 1000 ms later"
  [out]
  (go
    (<! (timeout 1000))
    (>! out 1)))

(defn get-rate
  "Calculates keypress per minute rate"
  [ticks pressed]
  (quot (* pressed 60) ticks))

(defn complete?
  [count]
  (>= count 100))

(defn main-loop []
  (let [key-chan (keypress-chan) ticker-chan (chan)]
    (go-loop [pressed 0 ticks 0]
      (let [[value ch] (alts! [key-chan ticker-chan])]
        (if (= ch key-chan)
          (do
            ;; Start timer to calculate keypress rate
            (if (zero? pressed)
              (tick ticker-chan))

            ;; Do something with `value`, i. e. the key being pressed

            (when-not (complete?)
              (recur (inc pressed) ticks)))

          (let [speed (get-rate ticks pressed)]
            (swap! app-state assoc :rate rate)
            (tick ticker-chan)
            (recur pressed (inc ticks))))))))

So, this works, but the code in main-loop seems ugly to me. I've been thinking of having a "trigger" that fires only when first value appears in the key-chan and starts the timer. Then another trigger stops the timer when necessary. But I'm new to Clojure and don't know well how to work with async, so I couldn't create anything better than code above.

How this loop could be improved? Can I move timer to a function and start the timer by the first value in the key-chan? How can I stop that timer later?

1
I would store the timestamp for each keypress in a sorted list, then compute a moving average rate over the past 60 seconds. To start, I wouldn't worry about discarding old values; just pull out all values within 60 sec of the current time, count them, and that is your average rate/min. - Alan Thompson
@AlanThompson, thanks for the idea! Will try it. - coquin

1 Answers

0
votes

I came up with the following solution.

(ns ratecalc.core
  (:require [cljs.core.async :refer [chan <! go-loop]])
  (:import [goog.date DateTime]))

(defn calc-rate [input-chan do-fun max-count]
  (go-loop [count 0
            start-time nil]
    (<! input-chan)
    (let [inc-count (inc count)
          now (.getTime (DateTime.))
          start-time* (if-not start-time now start-time)
          rate-per-ms (/ inc-count (- now start-time*))]
      (do-fun rate-per-ms)
      (if (= count max-count)
        rate-per-ms
        (recur inc-count start-time*)))))

The calc-rate function calculates the number of messages on the channel per millisecond. You can pass it a function to do some side effect like printing the current average. The last argument limits it calculation to a maximum number of messages. Here is an example of how to use it:

(calc-rate (keypress-chan) println 10)