Repository: CSCfi/rems
Pull request #3308 (2024-05-23 17:19:36)

Faster translations 2
on top of #3305

- memoized resource compiler
- optimize missing tr key case (a lot faster)
- added more perf test namespace tools 

Performance tests that find every translation key in dict for every language, then runs according function for every key:
- if translation contains vector arguments, `text-format` with generated vector values
- if translation contains map arguments, `text-format` with single map with generated values
- else, `text` (no arguments)

mean value is extracted from performance test after cache warmup

no memoize, no fn optimization
---
| total translations | mean         | empty cache size | warm cache size | final cache size |
| ------------------ | ------------ | ---------------- | --------------- | ---------------- |
| 1885               | 10,148342 ms | 88 B             | 890,6 KiB       | 890,6 KiB        |

no memoize, fn optimization
---
| total translations | mean        | empty cache size | warm cache size | final cache size |
| ------------------ | ----------- | ---------------- | --------------- | ---------------- |
| 1885               | 4,606966 ms | 88 B             | 890,6 KiB       | 890,6 KiB        |

memoize, fn optimization
---
| total translations | mean        | empty cache size | warm cache size | final cache size |
| ------------------ | ----------- | ---------------- | --------------- | ---------------- |
| 1885               | 2,722741 ms | 88 B             | 1,2 MiB         | 1,2 MiB          |

# Checklist for author

Remove items that aren't applicable, check items that are done.

## Reviewability
- [x] Note if PR is on top of other PR



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd4ee6484..47b45ae7b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,9 +11,6 @@ Changes since v2.37
 ### Changes
 - The performance has been improved when there are many handlers. (#3283)
 - The error message "shake" animation has been removed. (#3298)
-- If REMS is trying to send email, and if the address has an obvious problem,
-  REMS will not try sending it again, but gives up immediately. This should
-  avoid having many hopeless retries, if there is a typo in an email address.
 
 ## v2.37 "Laivapojankatu" 2024-05-16
 
diff --git a/project.clj b/project.clj
index c32674faf..daf9e836b 100644
--- a/project.clj
+++ b/project.clj
@@ -115,6 +115,7 @@
    :project/dev {:dependencies [[binaryage/devtools "1.0.7"]
                                 [com.clojure-goes-fast/clj-async-profiler "1.2.0"] ; also check extra :jvm-opts https://github.com/clojure-goes-fast/clj-async-profiler?tab=readme-ov-file#tuning-for-better-accuracy
                                 [com.clojure-goes-fast/clj-memory-meter "0.3.0"]
+                                [com.clojure-goes-fast/clj-async-profiler "1.2.0"]
                                 [criterium "0.4.6"]
                                 [lambdaisland/kaocha "1.87.1366"]
                                 [lambdaisland/kaocha-junit-xml "1.17.101"]
diff --git a/src/clj/rems/email/core.clj b/src/clj/rems/email/core.clj
index 5a8fbc442..9142f5553 100644
--- a/src/clj/rems/email/core.clj
+++ b/src/clj/rems/email/core.clj
@@ -150,30 +150,19 @@
             (log/warn e "failed sending email:" (pr-str email))
             (str "failed sending email: " e)))))))
 
-(defn handle-send-error! [email error]
-  (if (str/includes? error "javax.mail.internet.AddressException") ; e.g. illegal character in address
-    ;; give up immediately
-    (let [email (outbox/attempt-failed-fatally! email error)]
-      (log/warn "all attempts to send email " (:outbox/id email) "failed")
-      ::giving-up-after-fatal-error)
-
-    ;; maybe keep trying
-    (let [email (outbox/attempt-failed! email error)]
-      (if (:outbox/next-attempt email)
-        ::failed-but-will-retry-again
-
-        (do
-          (log/warn "all attempts to send email " (:outbox/id email) "failed")
-          ::giving-up-after-all-attempts)))))
-
 (defn try-send-emails! []
   (log/debug "Trying to send emails")
   (let [due-emails (outbox/get-due-entries :email)]
     (log/debug (str "Emails due: " (count due-emails)))
     (doseq [email due-emails]
       (if-let [error (send-email! (:outbox/email email))]
-        (handle-send-error! email error)
-
+        (if (str/includes? error "javax.mail.internet.AddressException") ; e.g. illegal character in address
+          (let [email (outbox/attempt-failed-fatally! email error)]
+            (when (not (:outbox/next-attempt email))
+              (log/warn "all attempts to send email " (:outbox/id email) "failed")))
+          (let [email (outbox/attempt-failed! email error)]
+            (when (not (:outbox/next-attempt email))
+              (log/warn "all attempts to send email " (:outbox/id email) "failed"))))
         (outbox/attempt-succeeded! (:outbox/id email))))))
 
 (mount/defstate email-poller
diff --git a/src/cljc/rems/tempura.cljc b/src/cljc/rems/tempura.cljc
index 7f184bd17..25851ce75 100644
--- a/src/cljc/rems/tempura.cljc
+++ b/src/cljc/rems/tempura.cljc
@@ -1,6 +1,5 @@
 (ns rems.tempura
-  (:require [better-cond.core :as b]
-            [clojure.string]
+  (:require [clojure.string]
             [clojure.test :refer [deftest is testing]]
             [clojure.walk]
             [taoensso.tempura]))
@@ -38,23 +37,30 @@
             :resource-keys [:x :y]}
            (replace-map-args [:div {:aria-label "argument x is %:x%, argument y is %:y%"} "{:x %:x% :y %:y%}"])))))
 
-(defn find-map-params [resource]
-  (b/cond
-    :let [extract-args #(->> (re-seq +map-args+ %)
-                             (map second))]
+(defn- string-seq [x]
+  (filter string? (tree-seq vector? seq x)))
 
-    (string? resource) (set (extract-args resource))
-    (vector? resource) (set (->> (flatten resource)
-                                 (filter string?)
-                                 (mapcat extract-args)))
-    nil))
+(defn find-vec-params [x]
+  (mapcat #(re-seq +vector-args+ %) (string-seq x)))
 
-(deftest test-find-map-params
-  (is (= #{"x" "long.ns/y" "z"}
-         (find-map-params "arg %:x%, arg %:long.ns/y%, args %:x% %:long.ns/y% %:z%")
-         (find-map-params [:div "arg %:x%" [:span "arg %:long.ns/y%"] [:span "args %:x% %:long.ns/y% %:z%"]])))
-  (is (nil? (find-map-params (constantly "arg %:x%, arg %:long.ns/y%, args %:x% %:long.ns/y% %:z%"))))
-  (is (nil? (find-map-params nil))))
+(defn find-map-params [x]
+  (->> (string-seq x)
+       (mapcat #(re-seq +map-args+ %))
+       (map (comp keyword second))))
+
+(deftest test-find-params
+  (testing "vec params"
+    (is (= ["%1" "%2" "%1" "%2" "%3"]
+           (find-vec-params "arg %1, arg %2, args %1 %2 %3")
+           (find-vec-params [:div "arg %1" [:span "arg %2"] [:span "args %1 %2 %3"]])))
+    (is (= [] (find-vec-params (constantly "arg %1, arg %2, args %1 %2 %3"))))
+    (is (= [] (find-vec-params nil))))
+  (testing "map params"
+    (is (= [:x :long.ns/y :x :long.ns/y :z]
+           (find-map-params "arg %:x%, arg %:long.ns/y%, args %:x% %:long.ns/y% %:z%")
+           (find-map-params [:div "arg %:x%" [:span "arg %:long.ns/y%"] [:span "args %:x% %:long.ns/y% %:z%"]])))
+    (is (= [] (find-map-params (constantly "arg %:x%, arg %:long.ns/y%, args %:x% %:long.ns/y% %:z%"))))
+    (is (= [] (find-map-params nil)))))
 
 (def ^:private get-default-resource-compiler (:resource-compiler taoensso.tempura/default-tr-opts))
 
@@ -78,19 +84,19 @@
                           :resource-keys resource-keys
                           :vargs vargs}))))))
 
-
 (defn- get-resource-compiler [resource]
   (if (seq (find-map-params resource))
     (get-map-compiler resource)
     (get-vec-compiler resource)))
 
 (def default-tr-opts {:cache-dict? :fn-local
-                      :resource-compiler get-resource-compiler})
+                      :cache-locales? :fn-local})
 
 (defn get-cached-tr [translations & [opts]]
   (taoensso.tempura/new-tr-fn (-> default-tr-opts
                                   (merge opts)
-                                  (assoc :dict translations))))
+                                  (assoc :dict translations
+                                         :resource-compiler (memoize get-resource-compiler)))))
 
 (deftest test-get-cached-tr
   (let [dict {:en
diff --git a/src/cljc/rems/text.cljc b/src/cljc/rems/text.cljc
index 72ddcc146..b4f24406c 100644
--- a/src/cljc/rems/text.cljc
+++ b/src/cljc/rems/text.cljc
@@ -63,8 +63,7 @@
 
    `(text-format-map :key {:a 1 :b 2} [:b :a])`"
   ([k arg-map] (text-format k arg-map))
-  ([k arg-map arg-vec] (apply text-format k arg-map (for [k arg-vec]
-                                                      (get arg-map k)))))
+  ([k arg-map arg-vec] (apply text-format k arg-map (mapv arg-map arg-vec))))
 
 (defn text-no-fallback
   "Return the tempura translation for a given key. Additional fallback
@@ -82,11 +81,13 @@
   "Return the tempura translation for a given key. Additional fallback
   keys can be given."
   [& ks]
-  #?(:clj (tr (conj (vec ks) (text-format :t/missing (vec ks))))
+  #?(:clj (or (tr ks) ; optimization: fetch missing translation only when needed
+              (text-format :t/missing ks))
      ;; NB: we can't call the text-no-fallback here as in CLJS
      ;; we can both call this as function or use as a React component
      :cljs (try
-             (tr (conj (vec ks) (text-format :t/missing (vec ks))))
+             (or (tr ks)
+                 (text-format :t/missing ks))
              (catch js/Object e
                ;; fail gracefully if the re-frame state is incomplete
                (.error js/console e)


Test patch:
diff --git a/src/clj/rems/service/test_data.clj b/src/clj/rems/service/test_data.clj
index 482d5ccf4..0793d5312 100644
--- a/src/clj/rems/service/test_data.clj
+++ b/src/clj/rems/service/test_data.clj
@@ -15,6 +15,7 @@
             [rems.db.test-data-users :refer :all]
             [rems.db.users :as users]
             [rems.db.workflow :as workflow]
+            [rems.db.user-settings :as user-settings]
             [rems.service.form :as form]
             [rems.service.organizations :as organizations]
             [rems.testing-util :refer [with-user]])
@@ -562,18 +563,24 @@
       (finally
         (.shutdownNow executor)))))
 
-(def ^:private vocabulary (-> "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut non diam vel erat dapibus facilisis vel vitae nunc. Curabitur at fermentum lorem. Cras et bibendum ante. Etiam convallis erat justo. Phasellus cursus molestie vehicula. Etiam molestie tellus vitae consectetur dignissim. Pellentesque euismod hendrerit mi sed tincidunt. Integer quis lorem ut ipsum egestas hendrerit. Aenean est nunc, mattis euismod erat in, sodales rutrum mauris. Praesent sit amet risus quis felis congue ultricies. Nulla facilisi. Sed mollis justo id tristique volutpat.\n\nPhasellus augue mi, facilisis ac velit et, pharetra tristique nunc. Pellentesque eget arcu quam. Curabitur dictum nulla varius hendrerit varius. Proin vulputate, ex lacinia commodo varius, ipsum velit viverra est, eget molestie dui nisi non eros. Nulla lobortis odio a magna mollis placerat. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer consectetur libero ut gravida ullamcorper. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec aliquam feugiat mollis. Quisque massa lacus, efficitur vel justo vel, elementum mollis magna. Maecenas at sem sem. Praesent sed ex mattis, egestas dui non, volutpat lorem. Nulla tempor, nisi rutrum accumsan varius, tellus elit faucibus nulla, vel mattis lacus justo at ante. Sed ut mollis ex, sed tincidunt ex.\n\nMauris laoreet nibh eget erat tincidunt pharetra. Aenean sagittis maximus consectetur. Curabitur interdum nibh sed tincidunt finibus. Sed blandit nec lorem at iaculis. Morbi non augue nec tortor hendrerit mollis ut non arcu. Suspendisse maximus nec ligula a efficitur. Etiam ultrices rhoncus leo quis dapibus. Integer vel rhoncus est. Integer blandit varius auctor. Vestibulum suscipit suscipit risus, sit amet venenatis lacus iaculis a. Duis eu turpis sit amet nibh sagittis convallis at quis ligula. Sed eget justo quis risus iaculis lacinia vitae a justo. In hac habitasse platea dictumst. Maecenas euismod et lorem vel viverra.\n\nDonec bibendum nec ipsum in volutpat. Vivamus in elit venenatis, venenatis libero ac, ultrices dolor. Morbi quis odio in neque consequat rutrum. Suspendisse quis sapien id sapien fermentum dignissim. Nam eu est vel risus volutpat mollis sed quis eros. Proin leo nulla, dictum id hendrerit vitae, scelerisque in elit. Proin consectetur sodales arcu ac tristique. Suspendisse ut elementum ligula, at rhoncus mauris. Aliquam lacinia at diam eget mattis. Phasellus quam leo, hendrerit sit amet mi eget, porttitor aliquet velit. Proin turpis ante, consequat in enim nec, tempus consequat magna. Vestibulum fringilla ac turpis nec malesuada. Proin id lectus iaculis, suscipit erat at, volutpat turpis. In quis faucibus elit, ut maximus nibh. Sed egestas egestas dolor.\n\nNulla varius orci quam, id auctor enim ultrices nec. Morbi et tellus ac metus sodales convallis sed vehicula neque. Pellentesque rhoncus mattis massa a bibendum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce tincidunt nulla non aliquet facilisis. Praesent nisl nisi, finibus id odio sed, consectetur feugiat mauris. Suspendisse sed lacinia ligula. Duis vitae nisl leo. Donec erat arcu, feugiat sit amet sagittis ac, scelerisque nec est. Pellentesque finibus mauris nulla, in maximus sapien pharetra vitae. Sed leo elit, consequat eu aliquam vitae, feugiat ut eros. Pellentesque dictum feugiat odio sed commodo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin neque quam, varius vel libero sit amet, rhoncus sollicitudin ex. In a dui non neque malesuada pellentesque.\n\nProin tincidunt nisl non commodo faucibus. Sed porttitor arcu neque, vitae bibendum sapien placerat nec. Integer eget tristique orci. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec eu molestie eros. Nunc iaculis rhoncus enim, vel mattis felis fringilla condimentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean ac augue nulla. Phasellus vitae nulla lobortis, mattis magna ac, gravida ipsum. Aenean ornare non nunc non luctus. Aenean lacinia lectus nec velit finibus egestas vel ut ipsum. Cras hendrerit rhoncus erat, vel maximus nunc.\n\nPraesent quis imperdiet quam. Praesent ligula tellus, consectetur sed lacus eu, malesuada condimentum tellus. Donec et diam hendrerit, dictum diam quis, aliquet purus. Suspendisse pulvinar neque at efficitur iaculis. Nulla erat orci, euismod id velit sed, dictum hendrerit arcu. Nulla aliquam molestie aliquam. Duis et semper nisi, eget commodo arcu. Praesent rhoncus, nulla id sodales eleifend, ante ipsum pellentesque augue, id iaculis sem est vitae est. Phasellus cursus diam a lorem vestibulum sodales. Nullam lacinia tortor vel tellus commodo, sit amet sodales quam malesuada.\n\nNulla tempor lectus vel arcu feugiat, vel dapibus ex dapibus. Maecenas purus justo, aliquet et sem sit amet, tincidunt venenatis dui. Nulla eget purus id sapien elementum rutrum eu vel libero. Cras non accumsan justo posuere.\n\n"
-                              str/lower-case
-                              (str/split #"[( \n)+]")
-                              distinct
-                              sort
-                              rest))
+(def vocabulary (-> "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut non diam vel erat dapibus facilisis vel vitae nunc. Curabitur at fermentum lorem. Cras et bibendum ante. Etiam convallis erat justo. Phasellus cursus molestie vehicula. Etiam molestie tellus vitae consectetur dignissim. Pellentesque euismod hendrerit mi sed tincidunt. Integer quis lorem ut ipsum egestas hendrerit. Aenean est nunc, mattis euismod erat in, sodales rutrum mauris. Praesent sit amet risus quis felis congue ultricies. Nulla facilisi. Sed mollis justo id tristique volutpat.\n\nPhasellus augue mi, facilisis ac velit et, pharetra tristique nunc. Pellentesque eget arcu quam. Curabitur dictum nulla varius hendrerit varius. Proin vulputate, ex lacinia commodo varius, ipsum velit viverra est, eget molestie dui nisi non eros. Nulla lobortis odio a magna mollis placerat. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer consectetur libero ut gravida ullamcorper. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec aliquam feugiat mollis. Quisque massa lacus, efficitur vel justo vel, elementum mollis magna. Maecenas at sem sem. Praesent sed ex mattis, egestas dui non, volutpat lorem. Nulla tempor, nisi rutrum accumsan varius, tellus elit faucibus nulla, vel mattis lacus justo at ante. Sed ut mollis ex, sed tincidunt ex.\n\nMauris laoreet nibh eget erat tincidunt pharetra. Aenean sagittis maximus consectetur. Curabitur interdum nibh sed tincidunt finibus. Sed blandit nec lorem at iaculis. Morbi non augue nec tortor hendrerit mollis ut non arcu. Suspendisse maximus nec ligula a efficitur. Etiam ultrices rhoncus leo quis dapibus. Integer vel rhoncus est. Integer blandit varius auctor. Vestibulum suscipit suscipit risus, sit amet venenatis lacus iaculis a. Duis eu turpis sit amet nibh sagittis convallis at quis ligula. Sed eget justo quis risus iaculis lacinia vitae a justo. In hac habitasse platea dictumst. Maecenas euismod et lorem vel viverra.\n\nDonec bibendum nec ipsum in volutpat. Vivamus in elit venenatis, venenatis libero ac, ultrices dolor. Morbi quis odio in neque consequat rutrum. Suspendisse quis sapien id sapien fermentum dignissim. Nam eu est vel risus volutpat mollis sed quis eros. Proin leo nulla, dictum id hendrerit vitae, scelerisque in elit. Proin consectetur sodales arcu ac tristique. Suspendisse ut elementum ligula, at rhoncus mauris. Aliquam lacinia at diam eget mattis. Phasellus quam leo, hendrerit sit amet mi eget, porttitor aliquet velit. Proin turpis ante, consequat in enim nec, tempus consequat magna. Vestibulum fringilla ac turpis nec malesuada. Proin id lectus iaculis, suscipit erat at, volutpat turpis. In quis faucibus elit, ut maximus nibh. Sed egestas egestas dolor.\n\nNulla varius orci quam, id auctor enim ultrices nec. Morbi et tellus ac metus sodales convallis sed vehicula neque. Pellentesque rhoncus mattis massa a bibendum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce tincidunt nulla non aliquet facilisis. Praesent nisl nisi, finibus id odio sed, consectetur feugiat mauris. Suspendisse sed lacinia ligula. Duis vitae nisl leo. Donec erat arcu, feugiat sit amet sagittis ac, scelerisque nec est. Pellentesque finibus mauris nulla, in maximus sapien pharetra vitae. Sed leo elit, consequat eu aliquam vitae, feugiat ut eros. Pellentesque dictum feugiat odio sed commodo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin neque quam, varius vel libero sit amet, rhoncus sollicitudin ex. In a dui non neque malesuada pellentesque.\n\nProin tincidunt nisl non commodo faucibus. Sed porttitor arcu neque, vitae bibendum sapien placerat nec. Integer eget tristique orci. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec eu molestie eros. Nunc iaculis rhoncus enim, vel mattis felis fringilla condimentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean ac augue nulla. Phasellus vitae nulla lobortis, mattis magna ac, gravida ipsum. Aenean ornare non nunc non luctus. Aenean lacinia lectus nec velit finibus egestas vel ut ipsum. Cras hendrerit rhoncus erat, vel maximus nunc.\n\nPraesent quis imperdiet quam. Praesent ligula tellus, consectetur sed lacus eu, malesuada condimentum tellus. Donec et diam hendrerit, dictum diam quis, aliquet purus. Suspendisse pulvinar neque at efficitur iaculis. Nulla erat orci, euismod id velit sed, dictum hendrerit arcu. Nulla aliquam molestie aliquam. Duis et semper nisi, eget commodo arcu. Praesent rhoncus, nulla id sodales eleifend, ante ipsum pellentesque augue, id iaculis sem est vitae est. Phasellus cursus diam a lorem vestibulum sodales. Nullam lacinia tortor vel tellus commodo, sit amet sodales quam malesuada.\n\nNulla tempor lectus vel arcu feugiat, vel dapibus ex dapibus. Maecenas purus justo, aliquet et sem sit amet, tincidunt venenatis dui. Nulla eget purus id sapien elementum rutrum eu vel libero. Cras non accumsan justo posuere.\n\n"
+                    str/lower-case
+                    (str/split #"[( \n)+]")
+                    distinct
+                    sort
+                    rest))
 
-(defn- random-long-string [& [n]]
+(defn random-long-string [& [n]]
   (str (str/join " " (repeatedly (or n 1000) #(rand-nth vocabulary)))
        ;; prevent string interning, just to be sure
        (UUID/randomUUID)))
 
+(def words (->> vocabulary
+                (remove #(re-find #"[.,;\n]+" %))
+                (filter #(> (count %) 3))))
+
+(defn random-word [] (rand-nth words))
+
 (defn create-performance-test-data! []
   (log/info "Creating performance test data")
   (let [resource-count 1000
@@ -584,19 +591,17 @@
         owner (+fake-users+ :owner)
 
         ;; create handlers
-        names (->> vocabulary
-                   (remove #(re-find #"[.,;\n]+" %))
-                   (filter #(> (count %) 3))
-                   (mapv str/capitalize))
         handlers-data (for [i (range handler-count)
-                            :let [first-name (rand-nth names)
-                                  last-name (rand-nth names)]]
+                            :let [first-name (random-word)
+                                  last-name (random-word)]]
                         {:userid (str "perf-test-handler-" i)
-                         :name (str first-name " " last-name)
-                         :email (str (str/lower-case first-name) "." (str/lower-case last-name) "@perftester.org")})
+                         :name (str (str/capitalize first-name) " " (str/capitalize last-name))
+                         :email (str first-name "." last-name "@perftester.org")})
 
         _ (doseq [handler handlers-data]
-            (test-helpers/create-user! handler))
+            (test-helpers/create-user! handler)
+            (user-settings/update-user-settings! (:userid handler)
+                                                 {:language (rand-nth [:en :fi :sv])}))
 
         handlers (concat [(+fake-users+ :approver1)
                           (+fake-users+ :approver2)]
diff --git a/test/clj/rems/email/test_core.clj b/test/clj/rems/email/test_core.clj
index 6e7a5c2ac..fe56316a2 100644
--- a/test/clj/rems/email/test_core.clj
+++ b/test/clj/rems/email/test_core.clj
@@ -2,8 +2,7 @@
   (:require [clojure.test :refer :all]
             [mount.core :as mount]
             [rems.config]
-            [rems.email.core :refer [send-email! handle-send-error!]]
-            [clj-time.core :as time])
+            [rems.email.core :refer [send-email!]])
   (:import [com.icegreen.greenmail.util GreenMail ServerSetup]))
 
 (use-fixtures
@@ -22,9 +21,7 @@
                                          :mail-from "rems@rems.rems")
                   postal.core/send-message (fn [_host message] (reset! message-atom message))
                   rems.db.users/get-user (constantly {:email "user@example.com"})
-                  rems.db.user-settings/get-user-settings (constantly {})
-                  rems.db.outbox/attempt-failed! (fn [email error])
-                  rems.db.outbox/attempt-failed-fatally! (fn [email error])]
+                  rems.db.user-settings/get-user-settings (constantly {})]
 
       (testing "don't send if there is no body (mail toggled off)"
         (is (nil? (send-email! {:to "foo@example.com" :subject "ding" :body nil})))
@@ -71,38 +68,16 @@
         (reset! message-atom nil))
 
       (testing "invalid email address"
-        (let [email {:to "fake email" :subject "x" :body "y"}
-              entry {:outbox/email email}
-              error (send-email! email)]
-          (is (= "failed address validation: Invalid address \"fake email\": javax.mail.internet.AddressException: Local address contains control or whitespace in string ``fake email''"
-                 error))
-          (is (nil? @message-atom))
-          (is (= :rems.email.core/giving-up-after-fatal-error (handle-send-error! entry error)))
-          (reset! message-atom nil)))
-
-      (testing "invalid email address"
-        (let [email {:to "foo.bar;@example.com" :subject "x" :body "y"}
-              entry {:outbox/email email}
-              error (send-email! email)]
-          (is (= "failed address validation: Invalid address \"foo.bar;@example.com\": javax.mail.internet.AddressException: Illegal semicolon, not in group in string ``foo.bar;@example.com'' at position 7"
-                 error))
-          (is (nil? @message-atom))
-          (is (= :rems.email.core/giving-up-after-fatal-error (handle-send-error! entry error)))
-          (reset! message-atom nil)))
+        (is (= "failed address validation: Invalid address \"fake email\": javax.mail.internet.AddressException: Local address contains control or whitespace in string ``fake email''"
+               (send-email! {:to "fake email" :subject "x" :body "y"})))
+        (is (nil? @message-atom))
+        (reset! message-atom nil))
 
       (testing "failed send"
         (with-redefs [postal.core/send-message (fn [_host _message]
                                                  (throw (Throwable. "dummy exception")))]
-          (let [email {:to-user "user" :subject "x" :body "y"}
-                entry {:outbox/email email}
-                error (send-email! email)]
-            (is (= "failed sending email: java.lang.Throwable: dummy exception"
-                   error))
-            (is (nil? @message-atom))
-            ;; NB: we could test retry but that is for outbox tests
-            ;; our dummy does not have next attempt
-            (is (= :rems.email.core/giving-up-after-all-attempts (handle-send-error! entry error)))
-            (reset! message-atom nil))))
+          (is (= "failed sending email: java.lang.Throwable: dummy exception"
+                 (send-email! {:to-user "user" :subject "x" :body "y"})))))
 
       (testing "SMTP not configured"
         (with-redefs [rems.config/env (assoc rems.config/env :smtp-host nil)]
diff --git a/test/clj/rems/test_performance.clj b/test/clj/rems/test_performance.clj
index aad5d25e0..af5eb959b 100644
--- a/test/clj/rems/test_performance.clj
+++ b/test/clj/rems/test_performance.clj
@@ -5,6 +5,7 @@
             [medley.core :refer [map-vals]]
             [mount.core :as mount]
             [rems.application.model]
+            [rems.common.util :refer [recursive-keys to-keyword]]
             [rems.config]
             [rems.db.applications :as applications]
             [rems.db.events :as events]
@@ -13,6 +14,7 @@
             [rems.email.template]
             [rems.locales]
             [rems.markdown]
+            [rems.service.test-data :as test-data]
             [rems.service.todos :as todos]
             [rems.tempura]
             [rems.text])
@@ -122,7 +124,7 @@
                           (mount/start #'rems.config/env ; :enable-handler-emails is enabled by default
                                        #'rems.locales/translations)
                           (rems.text/reset-cached-tr!))
-        get-benchmark-fn (fn [handler-count]
+        get-benchmark-fn (fn get-benchmark-event-to-emails [handler-count]
                            (let [{:keys [application event]} (get-template-app-and-event handler-count)]
                              (fn benchmark-event-to-emails []
                                (doall (rems.email.template/event-to-emails event application)))))
@@ -130,7 +132,8 @@
 
     ;; Execution time mean : 9,400171 ms (2019 Macbook Pro with 2,3 GHz 8-Core Intel Core i9)
     ;; translations cache size 882,5 KiB
-    (with-redefs [rems.db.user-settings/get-user-settings (fn [& _] {:language (rand-nth [:en :fi :sv])})]
+    (with-redefs [rems.db.user-settings/get-user-settings (fn wrapped-get-user-settings [& _]
+                                                            {:language (rand-nth [:en :fi :sv])})]
       (doall
        (for [n benchmark-handlers
              :let [test-event-to-emails (get-benchmark-fn n)
@@ -146,8 +149,11 @@
     ;; translations are cached using taoensso.tempura/new-tr-fn because (big & nested) dictionary compilation is expensive.
     ;; Execution time mean : 870,499348 ms (2019 Macbook Pro with 2,3 GHz 8-Core Intel Core i9)
     (let [get-cached-tr rems.tempura/get-cached-tr]
-      (with-redefs [rems.db.user-settings/get-user-settings (fn [& _] {:language (rand-nth [:en :fi :sv])})
-                    rems.tempura/get-cached-tr #(get-cached-tr % {:cache-dict? false})]
+      (with-redefs [rems.db.user-settings/get-user-settings (fn wrapped-get-user-settings [& _]
+                                                              {:language (rand-nth [:en :fi :sv])})
+                    rems.tempura/get-cached-tr (fn wrapped-get-cached-tr [translations & _]
+                                                 (get-cached-tr translations {:cache-dict? false
+                                                                              :cache-locales? false}))]
         (doall
          (for [n benchmark-handlers
                :let [test-event-to-emails (get-benchmark-fn n)
@@ -158,11 +164,13 @@
            (swap! all-stats update :disabled (fnil conj []) (merge stats {:handler-count n}))))))
     @all-stats))
 
+(defn- format-criterium-time [x]
+  (apply criterium/format-value x (criterium/scale-time x)))
+
 (defn- print-template-performance-tables [all-stats]
-  (let [format-time #(apply criterium/format-value % (criterium/scale-time %))
-        mean (comp format-time :mean)
-        low (comp format-time :low)
-        high (comp format-time :high)]
+  (let [mean (comp format-criterium-time :mean)
+        low (comp format-criterium-time :low)
+        high (comp format-criterium-time :high)]
     (when (:cached all-stats)
       (println "")
       (println "event-to-emails, cache")
@@ -182,15 +190,121 @@
                                (mapv (juxt :handler-count mean low high)))})]
         (println row)))))
 
+(defn- get-cache-sizes []
+  (doall
+   (for [s ['rems.db.applications/all-applications-cache
+            'rems.db.events/event-cache
+            'rems.db.user-settings/user-settings-cache
+            'rems.db.category/categories-cache
+            'rems.db.catalogue/cached
+            'rems.db.user-mappings/user-mappings-by-value
+            'rems.ext.duo/code-by-id
+            'rems.ext.mondo/code-by-id
+            'rems.ext.mondo/codes-dag
+            'rems.service.dependencies/dependencies-cache
+            'rems.text/cached-tr]
+         :let [size (mm/measure (-> s requiring-resolve var-get)
+                                {:bytes true})]]
+     {:name (str s)
+      :bytes size
+      :size ((var-get #'mm/convert-to-human-readable) size)})))
+
+(defn- get-all-translations []
+  (let [key-paths (recursive-keys rems.locales/translations)
+        vec-arg-translations (for [ks key-paths
+                                   :let [v (get-in rems.locales/translations ks)
+                                         args (rems.tempura/find-vec-params v)]
+                                   :when (seq args)]
+                               {:ks ks :args (distinct args)})
+        map-arg-translations (for [ks key-paths
+                                   :let [v (get-in rems.locales/translations ks)
+                                         args (rems.tempura/find-map-params v)]
+                                   :when (seq args)]
+                               {:ks ks :args (distinct args)})
+        no-arg-translations (for [ks (->> key-paths
+                                          (remove (set (map :ks vec-arg-translations)))
+                                          (remove (set (map :ks map-arg-translations))))]
+                              {:ks ks})]
+    {:vec-args vec-arg-translations
+     :map-args map-arg-translations
+     :no-args no-arg-translations}))
+
+(defn- benchmark-all-translations []
+  (let [all-translations (get-all-translations)
+        vec-args (group-by :lang (for [t (:vec-args all-translations)]
+                                   {:lang (first (:ks t))
+                                    :key (to-keyword (rest (:ks t)))
+                                    :fn-args (mapv (fn [& _] (test-data/random-word)) (:args t))}))
+        map-args (group-by :lang (for [t (:map-args all-translations)]
+                                   {:lang (first (:ks t))
+                                    :key (to-keyword (rest (:ks t)))
+                                    :fn-args [(reduce #(assoc %1 %2 (test-data/random-word)) {} (:args t))]}))
+        no-args (group-by :lang (for [t (:no-args all-translations)]
+                                  {:lang (first (:ks t))
+                                   :key (to-keyword (rest (:ks t)))}))
+        text #(rems.text/text (:key %))
+        text-format #(apply rems.text/text-format (:key %) (:fn-args %))
+        tr-all (fn []
+                 (doseq [lang [:en :fi :sv]]
+                   (rems.text/with-language lang
+                     (mapv text (get no-args lang))
+                     (mapv text-format (get vec-args lang))
+                     (mapv text-format (get map-args lang)))))]
+
+    (mount/start #'rems.config/env ; :enable-handler-emails is enabled by default
+                 #'rems.locales/translations)
+    (rems.text/reset-cached-tr!)
+
+    (let [empty-cache-size (mm/measure rems.text/cached-tr)
+          _ (doall (tr-all)) ; run once to warm cache
+          warm-cache-size (mm/measure rems.text/cached-tr)
+          all (run-benchmark {:name "all translations" :benchmark tr-all})
+          final-cache-size (mm/measure rems.text/cached-tr)]
+
+      {:all all
+       :empty-cache-size empty-cache-size
+       :warm-cache-size warm-cache-size
+       :final-cache-size final-cache-size
+       :translations-count (->> (vals all-translations)
+                                (map count)
+                                (reduce + 0))})))
+
 (comment
   (benchmark-get-events)
   (benchmark-get-all-applications)
   (benchmark-get-application)
 
   ;; handler count increases email count linearly
-  (def stats (benchmark-template-performance {:benchmark-handlers [1 10 50 100 200]}))
+  (def template-stats (benchmark-template-performance {:benchmark-handlers [1 10 50 100 200]}))
+  ;; additional table formatting
+  (print-template-performance-tables template-stats)
+
+  (def translations-stats (benchmark-all-translations))
   ;; additional table formatting
-  (print-template-performance-tables stats)
+  (do (println "")
+      (println "translations")
+      (println "---")
+      (doseq [row (rems.markdown/markdown-table
+                   {:header ["total translations" "mean" "empty cache size" "warm cache size" "final cache size"]
+                    :rows (->> [translations-stats]
+                               (mapv (juxt :translations-count
+                                           (comp format-criterium-time :mean :all)
+                                           :empty-cache-size
+                                           :warm-cache-size
+                                           :final-cache-size)))})]
+        (println row)))
+
+  (def cache-stats (get-cache-sizes))
+  ;; additional table formatting
+  (do (println "")
+      (println "cache sizes")
+      (println "---")
+      (doseq [row (rems.markdown/markdown-table
+                   {:header ["cache" "size"]
+                    :rows (->> cache-stats
+                               (sort-by :bytes >)
+                               (mapv (juxt :name :size)))})]
+        (println row)))
 
   (prof/clear-results)
   (prof/serve-ui 8080))