Skip to content

Commit 527ff7d

Browse files
[debug] Use more compact locals map construction
1 parent 75eee35 commit 527ff7d

File tree

6 files changed

+154
-48
lines changed

6 files changed

+154
-48
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
* [#941](https://github.com/clojure-emacs/cider-nrepl/pull/941): Stop vendoring Fipp dependency.
66
* [#941](https://github.com/clojure-emacs/cider-nrepl/pull/941): Default to orchard.pp printer when Fipp/Puget/Zprint is selected but not found on classpath.
7+
* [#943](https://github.com/clojure-emacs/cider-nrepl/pull/943): Debug: reduce insrumentation bytecode footprint.
78

89
## 0.55.7 (2025-04-29)
910

project.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,5 @@
138138
:eastwood [:test
139139
{:plugins [[jonase/eastwood "1.4.3"]]
140140
:eastwood {:config-files ["eastwood.clj"]
141-
:exclude-namespaces [cider.nrepl.middleware.test-filter-tests]}}]})
141+
:exclude-namespaces [cider.nrepl.middleware.debug-test
142+
cider.nrepl.middleware.test-filter-tests]}}]})

src/cider/nrepl/middleware/DebugSupport.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,26 @@ public static long doBreak(Object coor, long val, Object locals, Object STATE__)
3030
public static double doBreak(Object coor, double val, Object locals, Object STATE__) {
3131
return (double)doBreak(coor, Numbers.num(val), locals, STATE__);
3232
}
33+
34+
// The purpose of the following assoc methods is to build a locals map.
35+
// Chaining such assoc calls is more bytecode-compact than a single
36+
// RT.mapUniqueKeys(...) because the latter constructs an array (load each
37+
// key and value onto the stack, save them into the array) and boxes
38+
// primitives (invocations of Numbers.num). Additionally, in this custom
39+
// method we turn string keys into symbols, which means we don't have to
40+
// generate symbols at the callsite (in the instrumented method). This saves
41+
// bytecode because LDC of a constant string is more compact than
42+
// ALOAD+GETFIELD of an interned symbol.
43+
44+
public static IPersistentMap assoc(Object map, Object key, Object value) {
45+
return ((IPersistentMap)map).assoc(Symbol.intern(null, (String)key), value);
46+
}
47+
48+
public static IPersistentMap assoc(Object map, Object key, long value) {
49+
return assoc(map, key, Numbers.num(value));
50+
}
51+
52+
public static IPersistentMap assoc(Object map, Object key, double value) {
53+
return assoc(map, key, Numbers.num(value));
54+
}
3355
}

src/cider/nrepl/middleware/debug.clj

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,13 @@
147147
(remove (fn [[k]] (re-find #"__" (name k))) locals))
148148

149149
;;; Politely borrowed from clj-debugger.
150-
(defn sanitize-env
151-
"Turn a macro's &env into a map usable for binding."
150+
(defn locals-capturer
151+
"Turn a macro's &env into code that produces a map of locals."
152152
[env]
153-
(into {} (for [[sym bind] (filter-env env)
154-
:when (instance? Compiler$LocalBinding bind)]
155-
[`(quote ~sym) (.sym ^Compiler$LocalBinding bind)])))
153+
`(-> {}
154+
~@(for [[sym bind] (filter-env env)
155+
:when (instance? Compiler$LocalBinding bind)]
156+
`(DebugSupport/assoc ~(name sym) ~(.sym ^Compiler$LocalBinding bind)))))
156157

157158
;;;; ## Getting user input
158159
;;;
@@ -470,7 +471,7 @@ this map (identified by a key), and will `dissoc` it afterwards."}
470471
"Let-wrap `body` with STATE__ map containing code, file, line, column etc.
471472
STATE__ is an anaphoric variable available to all breakpoint macros. Ends with
472473
__ to avid conflicts with user locals and to signify that it's an internal
473-
variable which is cleaned in `sanitize-env' along other clojure's
474+
variable which is cleaned in `locals-capturer` along other clojure's
474475
temporaries."
475476
{:style/indent 0}
476477
[& body]
@@ -554,7 +555,7 @@ this map (identified by a key), and will `dissoc` it afterwards."}
554555
`(apply-instrumented-maybe (var ~fn-sym) [~@args] ~coor ~'STATE__))
555556
form)
556557
locals (when *do-locals*
557-
(sanitize-env &env))]
558+
(locals-capturer &env))]
558559
;; Keep original forms in a separate atom to save some code
559560
;; size. Unfortunately same trick wouldn't work for locals.
560561
(swap! *tmp-forms* assoc coor original-form)
@@ -656,8 +657,12 @@ this map (identified by a key), and will `dissoc` it afterwards."}
656657
(binding [*tmp-forms* (atom {})]
657658
(eval form1))
658659
(catch java.lang.RuntimeException e
659-
(if (re-matches #".*Method code too large!.*" (.getMessage e))
660-
(do (notify-client (str "Method code too large!\n"
660+
(if (some #(when %
661+
(re-matches #".*Method code too large!.*"
662+
(.getMessage ^Throwable %)))
663+
[e (.getCause e)])
664+
(do (notify-client *msg*
665+
(str "Method code too large!\n"
661666
"Locals and evaluation in local context won't be available.")
662667
:warning)
663668
;; re-try without locals

src/cider/nrepl/middleware/enlighten.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
(symbol? original-form) `(do
6767
(send-if-local '~original-form
6868
(assoc (:msg ~'STATE__) :coor ~coor)
69-
~(d/sanitize-env &env))
69+
~(d/locals-capturer &env))
7070
~form)
7171
(seq? form) (wrap-function-form form extras)
7272
:else form))

test/clj/cider/nrepl/middleware/debug_test.clj

Lines changed: 114 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
(:require
33
[cider.nrepl.middleware.util.instrument :as ins]
44
[cider.nrepl.middleware.debug :as d]
5+
[cider.test-helpers :refer :all]
56
[clojure.test :refer :all]
67
[clojure.walk :as walk]
8+
[matcher-combinators.matchers :as matchers]
79
[nrepl.middleware.interruptible-eval :refer [*msg*]]
810
[nrepl.transport :as t]))
911

@@ -72,13 +74,13 @@
7274
~m))
7375

7476
(defmacro locals []
75-
`~(#'d/sanitize-env &env))
77+
`~(#'d/locals-capturer &env))
7678

7779
(defmacro add-locals
7880
"Add locals to map m."
7981
[& [m]]
8082
`(assoc ~m
81-
:locals ~(#'d/sanitize-env &env)
83+
:locals ~(#'d/locals-capturer &env)
8284
:original-ns "user"))
8385

8486
(deftest read-debug-input-roundtrip
@@ -200,38 +202,113 @@
200202

201203
(deftest breakpoint
202204
;; Map merging
203-
(with-redefs [d/read-debug-command (fn [_ v _ s] (assoc (:msg s) :value v))
204-
d/debugger-message (atom [:fake])
205-
d/*skip-breaks* (atom nil)]
206-
(binding [*msg* {:session (atom {})
207-
:code :code
208-
:id :id
209-
:file :file
210-
:line :line
211-
:column :column}]
212-
(let [form `(d/with-initial-debug-bindings
213-
(d/breakpoint-if-interesting (inc 10) {:coor [6]} ~'(inc 10)))
214-
m (eval form)]
215-
(are [k v] (= (k m) v)
216-
:value 11
217-
:file :file
218-
:line :line
219-
:column :column
220-
:code :code
221-
:original-id :id))
222-
(reset! d/debugger-message [:fake])
223-
;; Locals capturing
224-
(is (= (:value (eval `(d/with-initial-debug-bindings
225-
(let [~'x 10]
226-
(d/breakpoint-if-interesting
227-
(locals)
228-
{:coor [1]} nil)))))
229-
'{x 10}))
230-
;; Top-level sexps are not debugged, just returned.
231-
(is (= (eval `(d/with-initial-debug-bindings
232-
(let [~'x 10]
233-
(d/breakpoint-if-interesting
234-
(locals)
235-
{:coor []}
236-
nil))))
237-
'{x 10})))))
205+
(let [capture (atom nil)]
206+
(with-redefs [d/read-debug-command (fn [_ v _ s]
207+
(reset! capture (assoc (:msg s) :value v))
208+
v)
209+
d/debugger-message (atom [:fake])
210+
d/*skip-breaks* (atom nil)]
211+
(binding [*msg* {:session (atom {})
212+
:code :code
213+
:id :id
214+
:file :file
215+
:line :line
216+
:column :column}]
217+
(let [form `(d/with-initial-debug-bindings
218+
(d/breakpoint-if-interesting (inc 10) {:coor [6]} ~'(inc 10)))
219+
m (eval form)]
220+
(is+ {:value 11
221+
:file :file
222+
:line :line
223+
:column :column
224+
:code :code
225+
:original-id :id}
226+
@capture))
227+
(reset! d/debugger-message [:fake])
228+
;; Locals capturing
229+
(eval `(d/with-initial-debug-bindings
230+
(let [~'x 10]
231+
(d/breakpoint-if-interesting
232+
(locals) {:coor [1]} nil))))
233+
(is (= (:value @capture) '{x 10}))
234+
;; Top-level sexps are not debugged, just returned.
235+
(is (= (eval `(d/with-initial-debug-bindings
236+
(let [~'x 10]
237+
(d/breakpoint-if-interesting
238+
(locals)
239+
{:coor []}
240+
nil))))
241+
'{x 10}))))))
242+
243+
(deftest instrumentation-stress-test
244+
(testing "able to compile this function full of locals"
245+
(is
246+
(-> '(defn a-fn [a0]
247+
(let [a0 (long (+))
248+
a1 (long (+ a0))
249+
a2 (+ a0 a1)
250+
a3 (+ a0 a1 a2)
251+
a4 (+ a0 a1 a2 a3)
252+
a5 (+ a0 a1 a2 a3 a4)
253+
a6 (+ a0 a1 a2 a3 a4 a5)
254+
a7 (+ a0 a1 a2 a3 a4 a5 a6)
255+
a8 (+ a0 a1 a2 a3 a4 a5 a6 a7)
256+
a9 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8)
257+
a10 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9)
258+
a11 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10)
259+
a12 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11)
260+
a13 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12)
261+
a14 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13)
262+
a15 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14)
263+
a16 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15)
264+
a17 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16)
265+
a18 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17)
266+
a19 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18)
267+
a20 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19)
268+
a21 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20)
269+
a22 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21)
270+
a23 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22)
271+
a24 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22 a23)
272+
a25 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22 a23 a24)
273+
a26 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22 a23 a24 a25)]
274+
a0))
275+
d/debug-reader
276+
ins/instrument-tagged-code
277+
eval)))
278+
279+
(testing "fails if there is one extra line"
280+
(is (thrown? clojure.lang.Compiler$CompilerException
281+
(-> '(defn a-fn [a0]
282+
(let [a0 (long (+))
283+
a1 (long (+ a0))
284+
a2 (+ a0 a1)
285+
a3 (+ a0 a1 a2)
286+
a4 (+ a0 a1 a2 a3)
287+
a5 (+ a0 a1 a2 a3 a4)
288+
a6 (+ a0 a1 a2 a3 a4 a5)
289+
a7 (+ a0 a1 a2 a3 a4 a5 a6)
290+
a8 (+ a0 a1 a2 a3 a4 a5 a6 a7)
291+
a9 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8)
292+
a10 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9)
293+
a11 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10)
294+
a12 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11)
295+
a13 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12)
296+
a14 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13)
297+
a15 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14)
298+
a16 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15)
299+
a17 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16)
300+
a18 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17)
301+
a19 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18)
302+
a20 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19)
303+
a21 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20)
304+
a22 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21)
305+
a23 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22)
306+
a24 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22 a23)
307+
a25 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22 a23 a24)
308+
a26 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22 a23 a24 a25)
309+
a27 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22 a23 a24 a25 a26)
310+
a28 (+ a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 a11 a12 a13 a14 a15 a16 a17 a18 a19 a20 a21 a22 a23 a24 a25 a26 a27)]
311+
a0))
312+
d/debug-reader
313+
ins/instrument-tagged-code
314+
eval)))))

0 commit comments

Comments
 (0)