From bdcc9adb23d4c413609973cac0f2d50f11331845 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 21 May 2026 14:36:12 +0300 Subject: [PATCH 1/5] Don't mutate the live nREPL dict when stamping log entries nrepl-log-message wrapped the message in nrepl-plist-put to attach a time-stamp field for display. plist-put extends the list in place when the key is absent, so the original response dict ended up with an extra "time-stamp" key that callbacks downstream of nrepl--dispatch-response would see on a freshly-arrived message. Build a fresh head and cons the time-stamp pair onto a shared tail instead; nothing downstream sees the change. --- lisp/nrepl-client.el | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lisp/nrepl-client.el b/lisp/nrepl-client.el index 308a957d5..f6cbace05 100644 --- a/lisp/nrepl-client.el +++ b/lisp/nrepl-client.el @@ -1263,11 +1263,14 @@ operations.") TYPE is either request or response. The message is logged to a buffer described by `nrepl-message-buffer-name-template'." (when nrepl-log-messages - ;; append a time-stamp to the message before logging it - ;; the time-stamps are quite useful for debugging + ;; Prepend a time-stamp pair to a fresh head, sharing the original + ;; cdr as the tail. Using `nrepl-plist-put' here would mutate the + ;; live message dict, so downstream response handlers would see an + ;; unexpected "time-stamp" key appearing on the response they got. (setq msg (cons (car msg) - (nrepl-plist-put (cdr msg) "time-stamp" - (format-time-string "%Y-%m-%0d %H:%M:%S.%N")))) + (cons "time-stamp" + (cons (format-time-string "%Y-%m-%0d %H:%M:%S.%N") + (cdr msg))))) (let ((log-buffer (nrepl-messages-buffer (current-buffer)))) (with-current-buffer log-buffer (setq buffer-read-only nil) From 6d92b7df5c387fdfc7287fc8fdb8feeb18a5a205 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 21 May 2026 14:36:30 +0300 Subject: [PATCH 2/5] Promote nrepl-message-buffer size knobs to defcustom nrepl-message-buffer-max-size and nrepl-message-buffer-reduce-denominator were defconst, but the docstrings invite tuning ("defaults to 4", "limits the number of buffer shrinking operations"). defcustom matches the docstring intent and lets users adjust through Customize without re-evaluating constants. --- lisp/nrepl-client.el | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lisp/nrepl-client.el b/lisp/nrepl-client.el index f6cbace05..9182fc248 100644 --- a/lisp/nrepl-client.el +++ b/lisp/nrepl-client.el @@ -1216,18 +1216,20 @@ keep it enabled unless you need to debug something." :type 'boolean :safe #'booleanp) -(defconst nrepl-message-buffer-max-size 1000000 +(defcustom nrepl-message-buffer-max-size 1000000 "Maximum size for the nREPL message buffer. Defaults to 1000000 characters, which should be an insignificant -memory burden, while providing reasonable history.") +memory burden, while providing reasonable history." + :type 'integer) -(defconst nrepl-message-buffer-reduce-denominator 4 +(defcustom nrepl-message-buffer-reduce-denominator 4 "Divisor by which to reduce message buffer size. When the maximum size for the nREPL message buffer is exceeded, the size of the buffer is reduced by one over this value. Defaults to 4, so that 1/4 of the buffer is removed, which should ensure the buffer's maximum is reasonably utilized, while limiting the number of buffer shrinking -operations.") +operations." + :type 'integer) (defvar nrepl-messages-mode-map (let ((map (make-sparse-keymap))) From 5b397f019d3951aeff119b02c05e50ecfd7050e6 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 21 May 2026 14:36:50 +0300 Subject: [PATCH 3/5] Use prin1 instead of pp in nrepl-log-pp-object leaf paths The two non-dict, non-marker branches in nrepl-log-pp-object called pp, which is significantly slower than prin1 and only pays off when output needs multi-line layout. nREPL leaf values are almost always atoms or short lists where prin1 produces the same single-line output. The non-list branch also dropped one of two trailing newlines pp emitted, removing a blank line between entries that was probably never intended. --- lisp/nrepl-client.el | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lisp/nrepl-client.el b/lisp/nrepl-client.el index 9182fc248..9c56be98a 100644 --- a/lisp/nrepl-client.el +++ b/lisp/nrepl-client.el @@ -1455,13 +1455,14 @@ it into the buffer." (t (if (and button (> (length object) min-list-fold-size)) (nrepl-log-insert-button (format "(%s ...)" (prin1-to-string head)) object) - (pp object (current-buffer))))) + (prin1 object (current-buffer)) + (insert "\n")))) ;; non-list objects (if (stringp object) (if (and button (> (length object) min-string-fold-size)) (nrepl-log-insert-button (format "\"%s...\"" (substring object 0 min-string-fold-size)) object) (insert (prin1-to-string object) "\n")) - (pp object (current-buffer)) + (prin1 object (current-buffer)) (insert "\n"))))) (defun nrepl-messages-buffer (conn) From dbacadd01eb8bbd8bd472e0fad116814638f84dc Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 21 May 2026 14:38:20 +0300 Subject: [PATCH 4/5] Rewrite nrepl-log--pp-listlike as a single pass The old implementation copy-sequence'd the cdr, seq-partition'd into pairs, sorted alphabetically, seq-map'd for name lengths, seq-max'd, seq-filter + seq-remove'd to split off special keys, seq-concatenate'd twice, apply'd seq-concatenate to flatten back into a plist, then cl-loop'd to emit -- roughly six list-sized allocations plus an O(n log n) sort per logged message, half of it just to assemble a plist that immediately got destructured again. Replace with one pass: bucket special keys into a fixed-position vector (so they emit in canonical id/op/session/time-stamp order without sorting), collect the rest in insertion order, and track the widest key inline for column alignment. No more sort, no more copy-sequence, allocations down to a single small vector plus the others list. Behavior change: non-special keys now print in the order they appeared in the dict instead of alphabetically. The order reflects how the peer constructed the message, which is more useful for protocol debugging than alphabetic by accident. --- lisp/nrepl-client.el | 47 +++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/lisp/nrepl-client.el b/lisp/nrepl-client.el index 9c56be98a..0405d777c 100644 --- a/lisp/nrepl-client.el +++ b/lisp/nrepl-client.el @@ -1396,6 +1396,9 @@ If ID is nil, return nil." (mod (length nrepl-message-colors)) (nth nrepl-message-colors)))) +(defconst nrepl-log--special-keys '("id" "op" "session" "time-stamp") + "Keys that are displayed first, in this order, in `nrepl-log--pp-listlike'.") + (defun nrepl-log--pp-listlike (object &optional foreground button) "Pretty print nREPL list like OBJECT. FOREGROUND and BUTTON are as in `nrepl-log-pp-object'." @@ -1407,28 +1410,32 @@ FOREGROUND and BUTTON are as in `nrepl-log-pp-object'." (insert (color head)) (if (null (cdr object)) (insert ")\n") + ;; Walk the plist once: bucket pairs whose key is in + ;; `nrepl-log--special-keys' into a fixed-position vector so they + ;; emit in canonical order, collect the rest in insertion order, + ;; and track the widest key for column alignment. Replaces a + ;; pipeline that copy-sequence'd, partitioned, sorted, mapped, + ;; filtered, removed, and concatenated the plist for every message. (let* ((indent (+ 2 (- (current-column) (length head)))) - (sorted-pairs (sort (seq-partition (copy-sequence (cdr object)) 2) - (lambda (a b) - (string< (car a) (car b))))) - (name-lengths (seq-map (lambda (pair) (length (car pair))) sorted-pairs)) - (longest-name (seq-max name-lengths)) - ;; Special entries are displayed first - (specialq (lambda (pair) (member (car pair) '("id" "op" "session" "time-stamp")))) - (special-pairs (seq-filter specialq sorted-pairs)) - (not-special-pairs (seq-remove specialq sorted-pairs)) - (all-pairs (seq-concatenate 'list special-pairs not-special-pairs)) - (sorted-object (apply #'seq-concatenate 'list all-pairs))) + (specials (make-vector (length nrepl-log--special-keys) nil)) + (others nil) + (longest-name 0)) + (cl-loop for (k v) on (cdr object) by #'cddr + do (setq longest-name (max longest-name (length k))) + do (let ((idx (cl-position k nrepl-log--special-keys :test #'equal))) + (if idx + (aset specials idx (cons k v)) + (push (cons k v) others)))) (insert "\n") - (cl-loop for l on sorted-object by #'cddr - do (let ((indent-str (make-string indent ?\s)) - (name-str (propertize (car l) 'face - ;; Only highlight top-level keys. - (unless (eq (car object) 'dict) - 'font-lock-keyword-face))) - (spaces-str (make-string (- longest-name (length (car l))) ?\s))) - (insert (format "%s%s%s " indent-str name-str spaces-str)) - (nrepl-log-pp-object (cadr l) nil button))) + (let ((indent-str (make-string indent ?\s)) + ;; Only highlight top-level keys. + (face (unless (eq (car object) 'dict) 'font-lock-keyword-face))) + (dolist (pair (nconc (delq nil (append specials nil)) (nreverse others))) + (let* ((k (car pair)) + (v (cdr pair)) + (spaces-str (make-string (- longest-name (length k)) ?\s))) + (insert indent-str (propertize k 'face face) spaces-str " ") + (nrepl-log-pp-object v nil button)))) (when (eq (car object) 'dict) (delete-char -1)) (insert (color ")\n"))))))) From 2475ab70f77f25f876cec9279bde080f794b74ac Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 21 May 2026 14:38:41 +0300 Subject: [PATCH 5/5] Mention the nrepl-log-message perf/correctness pass in CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb12dbdd3..05f23e188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,11 @@ - Convert modern tuple-format indent specs (e.g. `[[:block 1] [:inner 0]]`) to legacy format for compatibility with older clojure-mode versions. - Rename `cider-eval-spinner-type`, `cider-show-eval-spinner`, and `cider-eval-spinner-delay` to `cider-spinner-type`, `cider-show-spinner`, and `cider-spinner-delay`. The old names are kept as obsolete aliases. - Replace `cider-jack-in-universal-options` with the more general `cider-jack-in-tools` registry; the old variable is removed. Anyone who customized it should migrate by calling `cider-register-jack-in-tool` instead. +- Performance and correctness pass on the nREPL message logger: + - `nrepl-log-message` no longer mutates the live response dict to attach its display timestamp. Response callbacks used to see a stray `"time-stamp"` key on freshly-arrived messages. + - `nrepl-log--pp-listlike` now walks the plist in a single pass instead of copy-sequencing it through a sort/filter/concat pipeline. Specials (`id`, `op`, `session`, `time-stamp`) still print first but in canonical order, and the remaining keys now print in insertion order rather than alphabetically. + - `pp` was swapped for `prin1` in the non-dict leaf paths of `nrepl-log-pp-object`. + - `nrepl-message-buffer-max-size` and `nrepl-message-buffer-reduce-denominator` are now `defcustom` to match their docstring intent. ## 1.21.0 (2026-02-07)