Repository: CSCfi/rems
Pull request #3349 (2024-11-19 13:08:15)

Low level optimization
Try to improve specifically workflow edit speed.

Major speed-up is from memoization of the new `get-workflow-handlers` and `get-user`. Some further improvements come from low-level optimization like avoiding work or transient data structure.

The missing enrichment step functions are also created for consistency, but also naming the steps for e.g. tracing.

Heaviest steps in enrichment seem to be:
- By far `enrich-events`.
- Then `enrich-workflow-handlers` that uses memoization since now.
- Finally `enrich-workflow-voting` which doesn't make sense, but is slowed by `rems.permissions/blacklist` and particularly `map-permissions`. This slows down other "cheap steps" like `workflow-disable-commands` and `enrich-processing-states`. A low-level optimization or rewrite is perhaps in order.

# Checklist for author

Remove items that aren't applicable, check items that are done.

## Reviewability
- [ ] Link to issue

## Documentation
- [ ] Update changelog if necessary (possibly not needed)

## Testing
- [x] Complex logic is unit tested
- [x] Valuable features are integration / browser / acceptance tested automatically

## Follow-up
- [ ] New tasks are created for pending or remaining tasks



Patch:
diff --git a/src/clj/rems/application/model.clj b/src/clj/rems/application/model.clj
index df446db77..cc6a227ac 100644
--- a/src/clj/rems/application/model.clj
+++ b/src/clj/rems/application/model.clj
@@ -470,34 +470,48 @@
 (defn enrich-forms [forms get-form-template]
   (mapv #(enrich-form % get-form-template) forms))
 
-(defn- set-application-description [application]
+(defn- enrich-application-forms [application get-form-template]
+  (update application
+          :application/forms
+          #(enrich-forms % get-form-template)))
+
+(defn- enrich-application-description [application]
   (let [fields (select [:application/forms ALL :form/fields ALL] application)
         description (->> fields
                          (find-first #(= :description (:field/type %)))
                          :field/value)]
     (assoc application :application/description (str description))))
 
-(defn- enrich-resources [resources get-catalogue-item get-resource]
+(defn- enrich-resources [resources get-catalogue-item get-resource get-config]
   (->> resources
        (map (fn [resource]
               (let [item (get-catalogue-item (:catalogue-item/id resource))
-                    resource-id (:resource-id item) ; this is a bit backwards approach
-                    duo-codes (-> (get-resource resource-id) :resource/duo :duo/codes)]
-                (-> {:catalogue-item/id (:catalogue-item/id resource)
-                     :resource/ext-id (:resource/ext-id resource)
-                     :resource/id (:resource-id item)
-                     :catalogue-item/title (localization-for :title item)
-                     :catalogue-item/infourl (localization-for :infourl item)
-                     ;; TODO: remove unused keys
-                     :catalogue-item/start (:start item)
-                     :catalogue-item/end (:end item)
-                     :catalogue-item/enabled (:enabled item)
-                     :catalogue-item/expired (:expired item)
-                     :catalogue-item/archived (:archived item)}
-                    (assoc-some-in [:resource/duo :duo/codes] (seq (duo/enrich-duo-codes duo-codes)))))))
+                    enriched-resource {:catalogue-item/id (:catalogue-item/id resource)
+                                       :resource/ext-id (:resource/ext-id resource)
+                                       :resource/id (:resource-id item)
+                                       :catalogue-item/title (localization-for :title item)
+                                       :catalogue-item/infourl (localization-for :infourl item)
+                                       ;; TODO: remove unused keys
+                                       :catalogue-item/start (:start item)
+                                       :catalogue-item/end (:end item)
+                                       :catalogue-item/enabled (:enabled item)
+                                       :catalogue-item/expired (:expired item)
+                                       :catalogue-item/archived (:archived item)}]
+
+                (if (:enable-duo (get-config))
+                  (let [resource-id (:resource-id item) ; this is a bit backwards approach
+                        duo-codes (-> (get-resource resource-id) :resource/duo :duo/codes)]
+                    (assoc-some-in enriched-resource [:resource/duo :duo/codes] (seq (duo/enrich-duo-codes duo-codes))))
+
+                  enriched-resource))))
        (sort-by :catalogue-item/id)
        vec))
 
+(defn- enrich-application-resources [application get-catalogue-item get-resource get-config]
+  (update application
+          :application/resources
+          #(enrich-resources % get-catalogue-item get-resource get-config)))
+
 (defn- validate-duo-match [dataset-code query-code resource]
   (let [validity (duo/check-duo-code dataset-code query-code)]
     {:validity validity
@@ -524,18 +538,16 @@
      :resource/id (:resource/id resource)
      :duo/validation (validate-duo-match dataset-code query-code resource)}))
 
-(defn- hide-duos-if-not-enabled [application get-config]
+(defn- enrich-duos [application get-config]
   (if (:enable-duo (get-config))
-    application
     (-> application
+        (update-existing-in [:application/duo :duo/codes] duo/enrich-duo-codes)
+        (update-existing :application/duo assoc-some :duo/matches (seq (get-duo-matches application))))
+
+    (-> application ; hide instead
         (dissoc :application/duo)
         (update :application/resources (partial mapv #(dissoc % :resource/duo))))))
 
-(defn- enrich-duos [application]
-  (-> application
-      (update-existing-in [:application/duo :duo/codes] duo/enrich-duo-codes)
-      (update-existing :application/duo assoc-some :duo/matches (seq (get-duo-matches application)))))
-
 (defn- enrich-workflow-licenses [application get-workflow]
   (let [wf (get-workflow (get-in application [:application/workflow :workflow/id]))]
     (if-some [workflow-licenses (seq (get-in wf [:workflow :licenses]))]
@@ -567,6 +579,11 @@
                            (sort-by :license/id))]
     (merge-lists-by :license/id rich-licenses app-licenses)))
 
+(defn- enrich-application-licenses [application get-license]
+  (update application
+          :application/licenses
+          #(enrich-licenses % get-license)))
+
 (def ^:private sensitive-events #{:application.event/review-requested
                                   :application.event/reviewed
                                   :application.event/reviewer-invited
@@ -625,14 +642,14 @@
     :else
     :visibility/public))
 
-(defn enrich-event [event get-user get-catalogue-item get-resource]
+(defn enrich-event [event get-user get-catalogue-item get-resource get-config]
   (let [event-type (:event/type event)]
     (merge event
            {:event/actor-attributes (get-user (:event/actor event))
             :event/visibility (get-event-visibility event)}
            (case event-type
              :application.event/resources-changed
-             {:application/resources (enrich-resources (:application/resources event) get-catalogue-item get-resource)}
+             {:application/resources (enrich-resources (:application/resources event) get-catalogue-item get-resource get-config)}
 
              :application.event/decision-requested
              {:application/deciders (mapv get-user (:application/deciders event))}
@@ -704,10 +721,15 @@
             :application/members
             enrich-members)))
 
-(defn enrich-workflow-handlers [application get-workflow get-user]
-  (let [workflow (get-workflow (get-in application [:application/workflow :workflow/id]))
+(defn get-workflow-handlers [workflow-id get-workflow get-user]
+  (let [workflow (get-workflow workflow-id)
         handlers (->> (get-in workflow [:workflow :handlers])
-                      (mapv (comp get-user :userid)))
+                      (mapv (comp get-user :userid)))]
+    handlers))
+
+(defn enrich-workflow-handlers [application get-workflow-handlers]
+  (let [workflow-id (get-in application [:application/workflow :workflow/id])
+        handlers (get-workflow-handlers workflow-id)
         active-users (set (map :event/actor (:application/events application)))
         handlers (map (fn [handler]
                         (if (contains? active-users (:userid handler))
@@ -818,11 +840,6 @@
       :else
       #{:handler})))
 
-(defn- enrich-attachments [application get-user]
-  (->> application
-       (transform [:application/attachments ALL] #(update % :attachment/user get-user))
-       (transform [:application/attachments ALL] #(assoc % :attachment/redact-roles (allowed-redact-roles application %)))))
-
 (defn- enrich-workflow-anonymize-handling [application get-workflow]
   (let [workflow-id (get-in application [:application/workflow :workflow/id])
         workflow (get-workflow workflow-id)
@@ -868,6 +885,25 @@
     (-> application
         (assoc :application/invited-members (set members)))))
 
+(defn- enrich-events [application get-user get-catalogue-item get-resource get-config]
+  (let [events (mapv #(enrich-event % get-user get-catalogue-item get-resource get-config)
+                     (:application/events application))]
+    (assoc application :application/events events)))
+
+(defn- enrich-attachments [application get-attachments-for-application get-user]
+  (->> (update application
+               :application/attachments
+               #(merge-lists-by :attachment/id
+                                %
+                                (get-attachments-for-application (getx application :application/id))))
+       (transform [:application/attachments ALL] #(update % :attachment/user get-user))
+       (transform [:application/attachments ALL] #(assoc % :attachment/redact-roles (allowed-redact-roles application %)))))
+
+(defn- enrich-applicant [application get-user]
+  (assoc application
+         :application/applicant
+         (get-user (get-in application [:application/applicant :userid]))))
+
 (defn enrich-with-injections
   [application {:keys [blacklisted?
                        get-attachments-for-application
@@ -878,30 +914,30 @@
                        get-resource
                        get-user
                        get-users-with-role
-                       get-workflow]
+                       get-workflow
+                       cached-get-workflow-handlers]
                 :as _injections}]
   (-> application
-      (update :application/forms enrich-forms get-form-template)
+      (enrich-application-forms get-form-template)
       enrich-answers ; uses enriched form
       enrich-field-visible ; uses enriched answers
-      set-application-description ; uses enriched answers
-      (update :application/resources enrich-resources get-catalogue-item get-resource)
-      (hide-duos-if-not-enabled get-config)
-      enrich-duos ; uses enriched resources
+      enrich-application-description ; uses enriched answers
+      (enrich-application-resources get-catalogue-item get-resource get-config)
+      (enrich-duos get-config) ; uses enriched resources
       (enrich-workflow-licenses get-workflow)
-      (update :application/licenses enrich-licenses get-license)
-      (update :application/events (partial mapv #(enrich-event % get-user get-catalogue-item get-resource)))
-      (assoc :application/applicant (get-user (get-in application [:application/applicant :userid])))
-      (update :application/attachments #(merge-lists-by :attachment/id % (get-attachments-for-application (getx application :application/id))))
+      (enrich-application-licenses get-license)
+      (enrich-events get-user get-catalogue-item get-resource get-config)
+      (enrich-applicant get-user)
       (enrich-user-attributes get-user)
       (enrich-blacklist blacklisted?) ; uses enriched users
-      (enrich-workflow-handlers get-workflow get-user)
+      (enrich-workflow-handlers (or cached-get-workflow-handlers
+                                    #(get-workflow-handlers % get-workflow get-user)))
       (enrich-workflow-anonymize-handling get-workflow)
       (enrich-deadline get-config)
       (enrich-super-users get-users-with-role)
       (enrich-workflow-disable-commands get-config get-workflow)
       (enrich-workflow-voting get-config get-workflow)
-      (enrich-attachments get-user)
+      (enrich-attachments get-attachments-for-application get-user) ; users enriched roles
       (enrich-processing-states get-config get-workflow)
       enrich-invited-members))
 
diff --git a/src/clj/rems/db/applications.clj b/src/clj/rems/db/applications.clj
index d1000bfdd..d64150e9f 100644
--- a/src/clj/rems/db/applications.clj
+++ b/src/clj/rems/db/applications.clj
@@ -1,6 +1,7 @@
 (ns rems.db.applications
   "Query functions for forms and applications."
-  (:require [clojure.java.jdbc :as jdbc]
+  (:require [clojure.core.memoize :refer [memo]]
+            [clojure.java.jdbc :as jdbc]
             [clojure.set :as set]
             [clojure.test :refer [deftest is]]
             [clojure.tools.logging :as log]
@@ -200,24 +201,26 @@
            (group-users-by-role apps)))))
 
 (defn- update-user-roles [updated-users old-roles-by-user old-updated-enriched-apps new-updated-enriched-apps]
-  (into old-roles-by-user
-        (for [userid updated-users
-              :let [old-user-roles (get old-roles-by-user userid {})
-                    old-app-roles (->> old-updated-enriched-apps
-                                       vals
-                                       (eduction (map :application/user-roles)
-                                                 (mapcat #(get % userid)))
-                                       frequencies)
-                    new-app-roles (->> new-updated-enriched-apps
-                                       vals
-                                       (eduction (map :application/user-roles)
-                                                 (mapcat #(get % userid)))
-                                       frequencies)]]
-
-          [userid (as-> old-user-roles roles
-                    (merge-with - roles old-app-roles)
-                    (merge-with + roles new-app-roles)
-                    (filter-vals pos? roles))])))
+  (let [all-old-app-roles (->> old-updated-enriched-apps
+                               vals
+                               (mapv :application/user-roles))
+        all-new-app-roles (->> new-updated-enriched-apps
+                               vals
+                               (mapv :application/user-roles))]
+    (into old-roles-by-user
+          (for [userid updated-users
+                :let [old-user-roles (get old-roles-by-user userid {})
+                      old-app-roles (->> all-old-app-roles
+                                         (mapcat #(get % userid))
+                                         frequencies)
+                      new-app-roles (->> all-new-app-roles
+                                         (mapcat #(get % userid))
+                                         frequencies)]]
+
+            [userid (as-> old-user-roles roles
+                      (merge-with - roles old-app-roles)
+                      (merge-with + roles new-app-roles)
+                      (filter-vals pos? roles))]))))
 
 (deftest update-user-roles-test
   (is (= {"alice" {:applicant 1} "bob" {:applicant 1}}
@@ -256,6 +259,16 @@
   ;; - deleted      - applications that are going away
   (let [updated-app-ids (set (into updated-app-ids deleted-app-ids)) ; let's consider deleted to be automatically an app to update
 
+        ;; temporarily cached injections because a mass update may have a lot of same calls (e.g. handlers of the workflow of many applications)
+        cached-get-user (memo (:get-user fetcher-injections))
+        cached-get-workflow-handlers (memo (fn [workflow-id]
+                                             (model/get-workflow-handlers workflow-id
+                                                                          (:get-workflow fetcher-injections)
+                                                                          cached-get-user)))
+        cached-injections (assoc fetcher-injections
+                                 :get-user cached-get-user
+                                 :cached-get-workflow-handlers cached-get-workflow-handlers)
+
         old-raw-apps (::raw-apps state)
         old-enriched-apps (::enriched-apps state)
         old-updated-enriched-apps (select-keys old-enriched-apps updated-app-ids)
@@ -269,7 +282,7 @@
                        (apply dissoc apps deleted-app-ids)
                        (doall apps))
         new-updated-raw-apps (select-keys new-raw-apps updated-app-ids)
-        new-updated-enriched-apps (doall (map-vals #(model/enrich-with-injections % fetcher-injections) new-updated-raw-apps))
+        new-updated-enriched-apps (doall (map-vals #(model/enrich-with-injections % cached-injections) new-updated-raw-apps))
         new-enriched-apps (as-> new-updated-enriched-apps apps
                             (merge old-enriched-apps apps)
                             (apply dissoc apps deleted-app-ids)
diff --git a/src/clj/rems/db/user_settings.clj b/src/clj/rems/db/user_settings.clj
index 4d27cebc0..a7dd8fd18 100644
--- a/src/clj/rems/db/user_settings.clj
+++ b/src/clj/rems/db/user_settings.clj
@@ -39,22 +39,27 @@
                        (assoc-some :notification-email (:notification-email data)))]
       (coerce-DbUserSettings settings))))
 
+(defn- merge-defaults [settings]
+  (merge (default-settings)
+         settings))
+
 (def user-settings-cache
   (cache/basic {:id ::user-settings-cache
                 :miss-fn (fn [userid]
                            (if-let [settings (db/get-user-settings {:user userid})]
                              (-> settings
                                  first
-                                 parse-user-settings-raw)
+                                 parse-user-settings-raw
+                                 merge-defaults)
                              cache/absent))
                 :reload-fn (fn []
                              (->> (db/get-user-settings {})
                                   (group-by :userid)
-                                  (map-vals parse-user-settings-raw)))}))
+                                  (map-vals parse-user-settings-raw)
+                                  (map-vals merge-defaults)))}))
 
 (defn get-user-settings [userid]
-  (merge (default-settings)
-         (cache/lookup-or-miss! user-settings-cache userid)))
+  (cache/lookup-or-miss! user-settings-cache userid))
 
 (defn validate-new-settings
   "Validates the new settings.
diff --git a/src/clj/rems/email/core.clj b/src/clj/rems/email/core.clj
index f5f9c2e6f..7caf3dad5 100644
--- a/src/clj/rems/email/core.clj
+++ b/src/clj/rems/email/core.clj
@@ -33,7 +33,8 @@
       (template/event-to-emails (rems.application.model/enrich-event event
                                                                      rems.db.users/get-user
                                                                      rems.db.catalogue/get-catalogue-item
-                                                                     rems.db.resource/get-resource)
+                                                                     rems.db.resource/get-resource
+                                                                     (fn get-config [] env))
                                 (rems.db.applications/get-application app-id)))))
 
 (defn generate-event-emails! [new-events]
diff --git a/src/clj/rems/permissions.clj b/src/clj/rems/permissions.clj
index e6cec2d9f..c43c0307c 100644
--- a/src/clj/rems/permissions.clj
+++ b/src/clj/rems/permissions.clj
@@ -4,16 +4,13 @@
             [medley.core :refer [map-vals map-kv-vals]]
             [rems.common.util :refer [conj-set]]))
 
-(defn- give-role-to-user [application role user]
-  (assert (keyword? role) {:role role})
-  (assert (string? user) {:user user})
-  (update-in application [:application/user-roles user] conj-set role))
-
 (defn give-role-to-users [application role users]
-  (reduce (fn [app user]
-            (give-role-to-user app role user))
-          application
-          users))
+  (assert (keyword? role) {:role role})
+  (assoc application :application/user-roles
+         (persistent! (reduce (fn [user-roles user]
+                                (assoc! user-roles user (conj (set (user-roles user)) role)))
+                              (transient (or (:application/user-roles application) {}))
+                              users))))
 
 (defn- dissoc-if-empty [m k]
   (if (empty? (get m k))
@@ -36,22 +33,22 @@
   (testing "give first role"
     (is (= {:application/user-roles {"user" #{:role-1}}}
            (-> {}
-               (give-role-to-user :role-1 "user")))))
+               (give-role-to-users :role-1 ["user"])))))
   (testing "give more roles"
     (is (= {:application/user-roles {"user" #{:role-1 :role-2}}}
            (-> {}
-               (give-role-to-user :role-1 "user")
-               (give-role-to-user :role-2 "user")))))
+               (give-role-to-users :role-1 ["user"])
+               (give-role-to-users :role-2 ["user"])))))
   (testing "remove some roles"
     (is (= {:application/user-roles {"user" #{:role-1}}}
            (-> {}
-               (give-role-to-user :role-1 "user")
-               (give-role-to-user :role-2 "user")
+               (give-role-to-users :role-1 ["user"])
+               (give-role-to-users :role-2 ["user"])
                (remove-role-from-user :role-2 "user")))))
   (testing "remove all roles"
     (is (= {:application/user-roles {}}
            (-> {}
-               (give-role-to-user :role-1 "user")
+               (give-role-to-users :role-1 ["user"])
                (remove-role-from-user :role-1 "user")))))
   (testing "give a role to multiple users"
     (is (= {:application/user-roles {"user-1" #{:role-1}
@@ -60,8 +57,8 @@
                (give-role-to-users :role-1 ["user-1" "user-2"])))))
   (testing "multiple users, get the roles of a single user"
     (let [app (-> {}
-                  (give-role-to-user :role-1 "user-1")
-                  (give-role-to-user :role-2 "user-2"))]
+                  (give-role-to-users :role-1 ["user-1"])
+                  (give-role-to-users :role-2 ["user-2"]))]
       (is (= #{:role-1} (user-roles app "user-1")))
       (is (= #{:role-2} (user-roles app "user-2")))
       (is (= #{:everyone-else} (user-roles app "user-3"))))))
@@ -209,14 +206,14 @@
   (testing "one role"
     (is (= #{:foo}
            (-> {}
-               (give-role-to-user :role-1 "user")
+               (give-role-to-users :role-1 ["user"])
                (update-role-permissions {:role-1 #{:foo}})
                (user-permissions "user")))))
   (testing "multiple roles"
     (is (= #{:foo :bar}
            (-> {}
-               (give-role-to-user :role-1 "user")
-               (give-role-to-user :role-2 "user")
+               (give-role-to-users :role-1 ["user"])
+               (give-role-to-users :role-2 ["user"])
                (update-role-permissions {:role-1 #{:foo}
                                          :role-2 #{:bar}})
                (user-permissions "user"))))))


Test patch:
diff --git a/test/clj/rems/application/test_model.clj b/test/clj/rems/application/test_model.clj
index 54a699b5e..34d9858e4 100644
--- a/test/clj/rems/application/test_model.clj
+++ b/test/clj/rems/application/test_model.clj
@@ -1381,7 +1381,7 @@
                                 :application/id 1
                                 :application/resources [{:catalogue-item/id 10 :resource/ext-id "urn:11"}
                                                         {:catalogue-item/id 20 :resource/ext-id "urn:21"}]}
-                               get-user get-catalogue-item get-resource))))
+                               get-user get-catalogue-item get-resource get-config))))
   (testing "decision-requested"
     (is (= {:event/type :application.event/decision-requested
             :event/time (DateTime. 1)
@@ -1396,7 +1396,7 @@
                                 :event/actor "handler"
                                 :application/id 1
                                 :application/deciders ["decider" "reviewer"]}
-                               get-user get-catalogue-item get-resource))))
+                               get-user get-catalogue-item get-resource get-config))))
   (testing "review-requested"
     (is (= {:event/type :application.event/review-requested
             :event/time (DateTime. 1)
@@ -1411,7 +1411,7 @@
                                 :event/actor "handler"
                                 :application/id 1
                                 :application/reviewers ["decider" "reviewer"]}
-                               get-user get-catalogue-item get-resource))))
+                               get-user get-catalogue-item get-resource get-config))))
   (testing "member-added"
     (is (= {:event/type :application.event/member-added
             :event/time (DateTime. 1)
@@ -1425,7 +1425,7 @@
                                 :event/actor "handler"
                                 :application/id 1
                                 :application/member {:userid "member"}}
-                               get-user get-catalogue-item get-resource))))
+                               get-user get-catalogue-item get-resource get-config))))
 
   (testing "member-removed"
     (is (= {:event/type :application.event/member-removed
@@ -1440,7 +1440,7 @@
                                 :event/actor "handler"
                                 :application/id 1
                                 :application/member {:userid "member"}}
-                               get-user get-catalogue-item get-resource)))))
+                               get-user get-catalogue-item get-resource get-config)))))
 
 (deftest test-enrich-answers
   (testing "draft"
@@ -1501,7 +1501,7 @@
                                                                {:userid "edward"
                                                                 :name "Edward Elric"
                                                                 :handler/active? true}]}}
-           (-> (model/enrich-workflow-handlers application get-workflow get-user)
+           (-> (model/enrich-workflow-handlers application #(model/get-workflow-handlers % get-workflow get-user))
                (select-keys [:application/workflow]))))))
 
 (deftest test-enrich-deadline
diff --git a/test/clj/rems/email/test_template.clj b/test/clj/rems/email/test_template.clj
index a4046a38e..b3508412e 100644
--- a/test/clj/rems/email/test_template.clj
+++ b/test/clj/rems/email/test_template.clj
@@ -124,7 +124,7 @@
                                                              (assert (string? userid))
                                                              {:language lang})]
        (sort-emails (template/event-to-emails
-                     (model/enrich-event event get-user get-catalogue-item get-resource)
+                     (model/enrich-event event get-user get-catalogue-item get-resource get-config)
                      application)))))
   ([base-events event]
    (emails :en base-events event)))
diff --git a/test/clj/rems/test_layers.clj b/test/clj/rems/test_layers.clj
index a8752601a..a7f4685be 100644
--- a/test/clj/rems/test_layers.clj
+++ b/test/clj/rems/test_layers.clj
@@ -210,15 +210,17 @@
                            :when (> sum-time hide-cutoff-ms)]
                        [(sym->id from) (sym->id to) {;;:color (if (and avg (< avg cutoff-ms)) "#gray" "black")
                                                      :weight (or (and min-time (number? min-time)) 0.01)
-                                                     :penwidth (max (or (and min-time (number? min-time) (Math/sqrt min-time)) 0.1)
-                                                                    0.1)
+                                                     :penwidth (min 30
+                                                                    ;;(max (or (and min-time (number? min-time) (Math/sqrt min-time)) 0.1)
+                                                                    (max (or (and sum-time (number? sum-time) (* 4.0 (- (Math/log sum-time) 1))) 0.1)
+                                                                         0.1))
                                                      :label (if (and max-time (< max-time cutoff-ms))
                                                               ""
                                                               [:table {:border 0}
                                                                (when (> c 1) [:tr [:td "count"] [:td c]])
-                                                               (when (> c 1) [:tr [:td "sum"] [:td (format "%.2f ms" sum-time)]])
+                                                               (when (>= c 1) [:tr [:td "sum"] [:td (format "%.2f ms" sum-time)]])
                                                                (when (> c 1) [:tr [:td "min"] [:td (format "%.2f ms" min-time)]])
-                                                               [:tr [:td (if (> c 1) "avg" "")] [:td (format "%.2f ms" avg)]]
+                                                               (when (> c 1) [:tr [:td (if (> c 1) "avg" "")] [:td (format "%.2f ms" avg)]])
                                                                (when (> c 1) [:tr [:td "max"] [:td (format "%.2f ms" max-time)]])
                                                                (cond (= lazy c) [:tr [:td "lazy"] [:td "true"]]
                                                                      (pos? lazy) [:tr [:td "lazy"] [:td (format "%d/%d (%.0f%%)" (int lazy) (int c) (* 100.0 (/ lazy (double c))))]])])}]))
diff --git a/test/clj/rems/test_performance.clj b/test/clj/rems/test_performance.clj
index 946ef856e..fd7bc68d2 100644
--- a/test/clj/rems/test_performance.clj
+++ b/test/clj/rems/test_performance.clj
@@ -117,7 +117,7 @@
                      :event/type :application.event/decided
                      :event/actor (:decider +fake-users+)
                      :application/decision :approved}
-                    (rems.application.model/enrich-event (into +fake-user-data+ fake-handlers) nil nil))]
+                    (rems.application.model/enrich-event (into +fake-user-data+ fake-handlers) nil nil (fn [] {})))]
     {:application application
      :event decided}))