penpot/common/test/common_tests/transit_test.cljc
Luke Lau 55360c52f5
✨ Cache the transit reader when decoding strings (#12704)
* ✨ Cache the transit reader when decoding strings

On the cljs side, we create a new transit reader on every call to decode-str.
This turns out to have a fair bit overhead in both transit-cljs and transit-js,
since it has to merge the handlers, call clj->js, allocate a new ReadCache,
etc.

By reusing it the measured decode time goes from ~58us -> ~41us, so about 1.4x
faster. This particularly helps for some operations like whenever a component
is edited and every object needs to be decoded, see
https://github.com/penpot/penpot/issues/9344, although they could probably be
made more efficient in other ways.

The reader usually clears its read cache after a successful read, but whenever
it throws it hangs around. So in this case we need to reset the cache
ourselves, otherwise the next read will be from a clobbered cache.

The tests were written by Claude Sonnet 5.5, and should cover the clobbering
behaviour above.

Signed-off-by: Luke Lau <luke@igalia.com>

* Keep one transit reader per type and rebuild it on add-handlers!

---------

Signed-off-by: Luke Lau <luke@igalia.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-11-09 17:34:24 +01:00

46 lines
1.9 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns common-tests.transit-test
(:require
[app.common.transit :as tr]
[clojure.test :as t]))
;; A handler that always fails, to make a read throw after the reader has
;; already written to its read cache.
#?(:cljs
(tr/add-handlers!
{:id "test/throws"
:rfn (fn [_] (throw (js/Error. "invalid representation")))}))
(t/deftest decode-str-round-trip
(let [data {:a 1 :b [:c "d"] :e #{1 2}}]
(t/is (= data (-> data tr/encode-str tr/decode-str)))
(t/is (= data (-> data (tr/encode-str {:type :json-verbose})
(tr/decode-str {:type :json-verbose}))))))
;; `decode-str` reuses the readers across calls, but `add-handlers!` rebuilds
;; them, so a handler registered after the first read still applies.
#?(:cljs
(t/deftest decode-str-uses-handlers-added-after-the-first-read
(t/is (= [:x] (tr/decode-str "[\"~:x\"]")))
(tr/add-handlers! {:id "test/late" :rfn (fn [v] (str "late:" v))})
(t/is (= "late:x" (tr/decode-str "[\"~#test/late\",\"x\"]")))))
;; In ClojureScript `decode-str` reuses the same reader across calls, and a
;; read that throws leaves that reader's read cache dirty. The next message
;; would then resolve its cache codes ("^0", "^1") to entries of the failed
;; one if the cache is not cleared.
#?(:cljs
(t/deftest decode-str-after-a-failed-read
(let [good "[\"~:aaaa\",\"~:bbbb\",\"^0\",\"^1\"]"
bad "[\"~:cccc\",[\"~#test/throws\",1]]"
expected [:aaaa :bbbb :aaaa :bbbb]]
(t/is (= expected (tr/decode-str good)))
(t/is (thrown? js/Error (tr/decode-str bad)))
(t/is (= expected (tr/decode-str good)))
(t/is (= expected (tr/decode-str good))))))