(add-classpath "file:///Users/ragge/Projects/clojure/redis-clojure/redis-clojure.jar")

(ns benchmarks.clojure
  (:use clojure.contrib.pprint)
  (:require redis))

(defstruct benchmark-options
  :host
  :port
  :db
  :clients
  :requests
  :key-size
  :keyspace-size
  :data-size)


(defstruct client
  :id
  :request-times
  :requests-performed
  :requests-per-second)

(defstruct result
  :options
  :clients
  :total-time
  :requests)


(defmacro defbenchmark [name & body]
  (let [benchmark-name (symbol (str name "-benchmark"))]
    `(def ~(with-meta benchmark-name {:benchmark true})
          (fn ~benchmark-name
            [client# options# result#]
            (redis/with-server
             {:host (options# :host)
              :port (options# :port)
              :db   (options# :db)}
             (let [requests# (:requests options#)
                   requests-done# (:requests result#)]
               (loop [requests-performed# 0 request-times# []]
                 (if (>= @requests-done# requests#)
                   (assoc client#
                     :request-times request-times#
                     :requests-performed requests-performed#)
                   (do
                     (let [start# (System/nanoTime)]
                       ~@body
                       (let [end# (System/nanoTime)
                             elapsed# (/ (float (- end# start#)) 1000000.0)]
                         (dosync
                          (commute requests-done# inc))
                         (recur (inc requests-performed#)
                                (conj request-times# elapsed#)))))))))))))

(defbenchmark ping
  (redis/ping))

(defbenchmark get
  (redis/get (str "key-" (rand-int 1000))))

(defbenchmark set
  (redis/set (str "key-" (rand-int 1000)) "abc"))

(defbenchmark exists-set-and-get
  (let [key (str "key-" (rand-int 100))]
    (redis/exists key)
    (redis/set    key "blahongaa!")
    (redis/get    key)))


(def *default-options* (struct-map benchmark-options
                         :host "127.0.0.1"
                         :port 6379
                         :db 15
                         :clients 1
                         :requests 10000))

(defn create-clients [options]
  (for [id (range (:clients options))]
    (agent (struct client id))))

(defn create-result [options clients]
  (let [result (struct result options clients 0 (ref 0))]
    result))


(defn requests-by-ms [clients]
  (let [all-times (apply concat (map #(:request-times (deref %)) clients))
        all-times-in-ms (map #(int (/ % 1)) all-times)]
    (sort
     (reduce
      (fn [m time]
        (if (m time)
          (assoc m time (inc (m time)))
          (assoc m time 1)))
      {} all-times-in-ms))))

(defn report-request-times [clients requests]
  (let [requests-dist (map #(let [perc (* 100 (/ (last %) requests))]
                             (conj % perc)) (requests-by-ms clients))]
    (loop [items requests-dist
           seen 0]
      (if-not (empty? items)
        (do 
          (let [item (first items)
                seen (+ seen (last item))]
            (println (format "%.2f%% < %d ms" (float seen) (inc (first item))))
            (recur (rest items) seen)))))))

(defn report-client-rps [client]
  (let [{:keys [id requests-performed request-times]} @client]
    (when (< 0 requests-performed)
      (let [total-time (apply + request-times)
            requests-per-second (/ (float requests-performed)
                                   total-time)]
        (println total-time)
        (println (format "Client %d: %f rps" id (float requests-per-second)))))))

(defn report-result [result]
  (let [{:keys [clients options]} result
        name (:name result)
        time (:total-time result)
        time-in-seconds (/ time 1000)
        requests (deref (:requests result)) 
        requests-per-second (/ requests time-in-seconds)
        ]
    (do
      (println (format "====== %s =====\n" name))
      (println (format "   %d requests completed in %f seconds\n" requests time-in-seconds))
      (println (format "   %d parallel clients\n" (:clients options)))
      (report-request-times clients requests)
      ;(dorun (map report-client-rps clients))
      (println (format "%f requests per second\n\n" requests-per-second))
      )
    )
  )



(defn run-benchmark [fn options]
  (let [clients (create-clients options)
        result (create-result options clients)
        start (System/nanoTime)]
    (dorun
     (map #(send-off % fn options result) clients))
    (apply await clients)
    (let [elapsed (/ (double (- (System/nanoTime) start)) 1000000.0)]
      (dorun
       (map #(when (agent-errors %)
               (pprint (agent-errors %))) clients))
      (assoc result
        :name (str fn)
        :options options
        :clients clients
        :total-time elapsed))))

(defn find-all-benchmarks [ns]
  (filter #(:benchmark (meta %))
          (vals (ns-map ns))))

(defn run-and-report [fn options]
  (let [result (run-benchmark fn options)]
    (report-result result)))

(defn run-all-benchmarks [ns]
  (let [benchmarks (find-all-benchmarks ns)]
    (dorun
     (map #(run-and-report % *default-options*) benchmarks))))


;(run-all-benchmarks)

;(report-result (run-benchmark ping-benchmark *default-options*))
;(run-benchmark get-benchmark *default-options*)