* bug#25122: 24.5; function describe-variable hangs on large variables
@ 2016-12-06 2:21 Boruch Baum
2016-12-06 6:41 ` Thierry Volpiatto
0 siblings, 1 reply; 27+ messages in thread
From: Boruch Baum @ 2016-12-06 2:21 UTC (permalink / raw)
To: 25122
Subject: 24.5; function describe-variable hangs on large variables
1) When evaluating function describe-variable for variable
package-archive-conteqnts, emacs hangs for minutes before I gave up.
2) Aborting vua C-g works.
3) Viewing the buffer list revealed that a *Help* buffer had begun to be
created. Its content was "package-archive-contents is a variable defined
in `package.el'. Its value is " (new-lines removed).
4) If emacs is trying to stuff into that variable (and into that *Help*
buffer) all the archive information from the archive files of my
~/.emacs.d/elpa/archives/ tree, that would be about 730kb of elisp.
In GNU Emacs 24.5.1 (x86_64-pc-linux-gnu)
of 2016-03-19 on trouble, modified by Debian
System Description: Devuan GNU/Linux 1.0 (jessie)
Configured using:
`configure --build x86_64-linux-gnu --prefix=/usr
--sharedstatedir=/var/lib --libexecdir=/usr/lib
--localstatedir=/var/lib --infodir=/usr/share/info
--mandir=/usr/share/man --with-pop=yes
--enable-locallisppath=/etc/emacs24:/etc/emacs:/usr/local/share/emacs/24.5/site-lisp:/usr/local/share/emacs/site-lisp:/usr/share/emacs/24.5/site-lisp:/usr/share/emacs/site-lisp
--build x86_64-linux-gnu --prefix=/usr --sharedstatedir=/var/lib
--libexecdir=/usr/lib --localstatedir=/var/lib
--infodir=/usr/share/info --mandir=/usr/share/man --with-pop=yes
--enable-locallisppath=/etc/emacs24:/etc/emacs:/usr/local/share/emacs/24.5/site-lisp:/usr/local/share/emacs/site-lisp:/usr/share/emacs/24.5/site-lisp:/usr/share/emacs/site-lisp
--with-x=no --without-gconf --without-gsettings 'CFLAGS=-g -O2
-fstack-protector-strong -Wformat -Werror=format-security -Wall'
CPPFLAGS=-D_FORTIFY_SOURCE=2 LDFLAGS=-Wl,-z,relro'
Important settings:
value of $LANG: en_US.UTF-8
locale-coding-system: utf-8-unix
Major mode: Emacs-Lisp
Minor modes in effect:
global-anzu-mode: t
anzu-mode: t
ws-butler-mode: t
dtrt-indent-mode: t
clean-aindent-mode: t
yas-minor-mode: t
global-undo-tree-mode: t
undo-tree-mode: t
volatile-highlights-mode: t
global-ede-mode: t
ede-minor-mode: t
global-semantic-idle-scheduler-mode: t
global-semanticdb-minor-mode: t
async-bytecomp-package-mode: t
global-semantic-stickyfunc-mode: t
semantic-mode: t
helm-mode: t
shell-dirtrack-mode: t
projectile-mode: t
global-company-mode: t
company-mode: t
override-global-mode: t
winner-mode: t
show-paren-mode: t
savehist-mode: t
desktop-save-mode: t
delete-selection-mode: t
tooltip-mode: t
file-name-shadow-mode: t
global-font-lock-mode: t
font-lock-mode: t
auto-composition-mode: t
auto-encryption-mode: t
auto-compression-mode: t
size-indication-mode: t
column-number-mode: t
line-number-mode: t
transient-mark-mode: t
Features:
(shadow sort mail-extr eieio-opt emacsbug helm-command tramp-cache
conf-mode org-element org-rmail org-mhe org-irc org-info org-gnus
org-docview doc-view image-mode org-bibtex bibtex org-bbdb org-w3m
misearch multi-isearch zygospore sh-script smie executable setup-editing
help-macro sgml-mode iedit-lib rect anzu mule-util ws-butler benchmark
dtrt-indent clean-aindent-mode yasnippet undo-tree diff
volatile-highlights ede/cpp-root ede/emacs setup-cedet ede/speedbar
ede/files ede ede/base ede/auto ede/source eieio-speedbar speedbar
sb-image dframe eieio-custom wid-edit semantic/idle semantic/format
ezimage semantic/tag-ls semantic/find semantic/ctxt semantic/db-mode
semantic/db eieio-base cc-mode cc-fonts cc-guess cc-menus cc-cmds
cc-styles cc-align cc-engine cc-vars cc-defs setup-helm-gtags helm-gtags
subr-x pulse which-func setup-helm helm-projectile helm-config
async-bytecomp helm-imenu semantic/util-modes semantic/util semantic
semantic/tag semantic/lex semantic/fw mode-local cedet org org-macro
org-footnote org-pcomplete org-list org-faces org-entities noutline
outline org-version ob-sh ob-awk ob-latex ob-emacs-lisp ob ob-tangle
ob-ref ob-lob ob-table ob-exp org-src ob-keys ob-comint ob-core ob-eval
org-compat org-macs org-loaddefs cal-menu calendar cal-loaddefs imenu
helm-easymenu helm-mode helm-elisp helm-files tramp tramp-compat
tramp-loaddefs trampver shell pcomplete ffap helm-buffers helm-tags
helm-bookmark helm-locate helm-eval edebug eldoc helm-grep helm-regexp
helm-elscreen helm-adaptive helm-info info helm-types helm-external
helm-net browse-url xml helm-utils helm-help helm helm-source
helm-multi-match helm-lib smtpmail sendmail async setup-general windmove
projectile skeleton grep ibuf-ext thingatpt json epl rx company-oddmuse
company-keywords company-etags company-gtags company-dabbrev-code
company-files company-capf company-cmake company-xcode company-clang
company-semantic company-eclim company-css company-nxml company-bbdb
tempo ispell etags find-func company-dabbrev company-template company
tar-mode use-package cl diminish bind-key compile comint tool-bar
autoload lisp-mnt finder-inf mm-archive message rfc822 mml mml-sec
mailabbrev gmm-utils mailheader mm-decode mm-bodies mm-encode mail-utils
gnutls network-stream starttls url-http tls mail-parse rfc2231 rfc2047
rfc2045 ietf-drums url-gw url-cache url-auth url url-proxy url-privacy
url-expand url-methods url-history url-cookie url-domsuf url-util
mailcap url-handlers url-parse auth-source eieio byte-opt bytecomp
byte-compile cl-extra cconv eieio-core gnus-util mm-util mail-prsvr
password-cache url-vars epg xterm server warnings dired-details+
dired-details help-mode advice help-fns dired+ image-dired image-file
image dired-x dired-aux dired winner ring pcase git-blame format-spec
package epg-config bookmark cl-macs gv derived pp jka-compr ibuf-macs
ibuffer paren woman man easymenu regexp-opt ansi-color edmacro kmacro
time-date savehist desktop frameset cl-loaddefs cl-lib elec-pair delsel
tango-dark-theme debian-el debian-el-loaddefs w3m-load emacs-goodies-el
emacs-goodies-custom emacs-goodies-loaddefs easy-mmode devhelp tooltip
electric uniquify ediff-hook vc-hooks lisp-float-type tabulated-list
newcomment lisp-mode prog-mode register page menu-bar rfn-eshadow timer
select mouse jit-lock font-lock syntax facemenu font-core frame cham
georgian utf-8-lang misc-lang vietnamese tibetan thai tai-viet lao
korean japanese hebrew greek romanian slovak czech european ethiopic
indian cyrillic chinese case-table epa-hook jka-cmpr-hook help simple
abbrev minibuffer nadvice loaddefs button faces cus-face macroexp files
text-properties overlay sha1 md5 base64 format env code-pages mule
custom widget hashtable-print-readable backquote make-network-process
dbusbind gfilenotify multi-tty emacs)
Memory information:
((conses 16 582332 490741)
(symbols 48 55652 20)
(miscs 40 421 1843)
(strings 32 146045 231132)
(string-bytes 1 4264772)
(vectors 16 55051)
(vector-slots 8 1598839 288778)
(floats 8 286 3294)
(intervals 56 4525 912)
(buffers 960 27)
(heap 1024 62515 75886))
--
hkp://keys.gnupg.net
CA45 09B5 5351 7C11 A9D1 7286 0036 9E45 1595 8BC0
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2016-12-06 2:21 bug#25122: 24.5; function describe-variable hangs on large variables Boruch Baum
@ 2016-12-06 6:41 ` Thierry Volpiatto
2016-12-07 3:50 ` npostavs
2017-03-11 15:21 ` Stefan Monnier
0 siblings, 2 replies; 27+ messages in thread
From: Thierry Volpiatto @ 2016-12-06 6:41 UTC (permalink / raw)
To: 25122
Boruch Baum <boruch_baum@gmx.com> writes:
> Subject: 24.5; function describe-variable hangs on large variables
>
> 1) When evaluating function describe-variable for variable
> package-archive-conteqnts, emacs hangs for minutes before I gave up.
I have already reported this bug.
I use this to workaround it:
(progn
;; Speedup `describe-variable' for variables with huge value description.
(defun tv/describe-variable (old-fn &rest args)
;; `cl-flet' can't be used here because `pp' should
;; appear lexically in its body, which is not the case.
;; Using `flet' is an option, but even better is binding
;; (symbol-function 'pp) with `cl-letf'.
(cl-letf (((symbol-function 'pp)
(lambda (object &optional stream)
(let ((fn (lambda (ob &optional stream)
(princ (pp-to-string ob)
(or stream standard-output))
(terpri)))
(print-circle t))
(if (consp object)
(progn
(insert "\n(")
(mapc fn object)
(cl-letf (((point) (1- (point))))
(insert ")")))
(funcall fn object stream))))))
(apply old-fn args)))
(advice-add 'describe-variable :around #'tv/describe-variable))
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2016-12-06 6:41 ` Thierry Volpiatto
@ 2016-12-07 3:50 ` npostavs
2016-12-07 8:58 ` Thierry Volpiatto
2017-03-11 15:21 ` Stefan Monnier
1 sibling, 1 reply; 27+ messages in thread
From: npostavs @ 2016-12-07 3:50 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Boruch Baum
merge 13439 25122
quit
Thierry Volpiatto <thierry.volpiatto@gmail.com> writes:
> Boruch Baum <boruch_baum@gmx.com> writes:
>
>> Subject: 24.5; function describe-variable hangs on large variables
>>
>> 1) When evaluating function describe-variable for variable
>> package-archive-conteqnts, emacs hangs for minutes before I gave up.
>
> I have already reported this bug.
> I use this to workaround it:
I wonder if the suggestion in #13439 might also help?
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2016-12-07 3:50 ` npostavs
@ 2016-12-07 8:58 ` Thierry Volpiatto
2017-03-11 5:40 ` npostavs
0 siblings, 1 reply; 27+ messages in thread
From: Thierry Volpiatto @ 2016-12-07 8:58 UTC (permalink / raw)
To: npostavs; +Cc: 25122, Boruch Baum
npostavs@users.sourceforge.net writes:
> merge 13439 25122
> quit
>
> Thierry Volpiatto <thierry.volpiatto@gmail.com> writes:
>
>> Boruch Baum <boruch_baum@gmx.com> writes:
>>
>>> Subject: 24.5; function describe-variable hangs on large variables
>>>
>>> 1) When evaluating function describe-variable for variable
>>> package-archive-conteqnts, emacs hangs for minutes before I gave up.
>>
>> I have already reported this bug.
>> I use this to workaround it:
>
> I wonder if the suggestion in #13439 might also help?
I don't think it would be as fast as printing one by one each elements.
What is slow is printing the whole object.
See how bookmark file is saved, it was taking more than one minute for
my bookmarks before printing one by one, now it is instant or nearly.
--
Thierry
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2016-12-07 8:58 ` Thierry Volpiatto
@ 2017-03-11 5:40 ` npostavs
2017-03-11 15:33 ` Stefan Monnier
2017-03-11 19:34 ` Thierry Volpiatto
0 siblings, 2 replies; 27+ messages in thread
From: npostavs @ 2017-03-11 5:40 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Boruch Baum
[-- Attachment #1: Type: text/plain, Size: 1578 bytes --]
Thierry Volpiatto <thierry.volpiatto@gmail.com> writes:
>
> I don't think it would be as fast as printing one by one each elements.
> What is slow is printing the whole object.
> See how bookmark file is saved, it was taking more than one minute for
> my bookmarks before printing one by one, now it is instant or nearly.
Actually, it's the indent-sexp on the whole object that takes time.
Possibly we could sacrifice some indentation correctness if the printed
representation is big.
I've been attempting an alternate approach which prettyprints the object
while scanning it instead of the current way of printing and then
reindenting. Without optimizing, it's about 3 times as fast as the
current pp (it's the pp-prin1 in the benchmarks below), though more than
3 times slower than your mapc pp trick. On the other hand, it also
doesn't yet handle function-specific indentation or any compound
structure apart from lists, so I'm not sure if it will end up being much
faster.
(benchmark 1 '(with-temp-buffer (pp-prin1 long-list (current-buffer)) nil)) "Elapsed time: 3.391232s (0.565806s in 11 GCs)"
(benchmark 1 '(progn (pp-to-string long-list) nil)) "Elapsed time: 9.988515s (0.148034s in 3 GCs)"
(benchmark 1 '(progn (with-output-to-string (mapc 'pp long-list)) nil)) "Elapsed time: 0.983493s (0.144424s in 3 GCs)"
(benchmark 1 '(progn (cl-prin1-to-string long-list) nil)) "Elapsed time: 0.511617s (0.152483s in 3 GCs)"
(benchmark 1 '(progn (prin1-to-string long-list) nil)) "Elapsed time: 0.029320s"
[-- Attachment #2: draft patch --]
[-- Type: text/plain, Size: 7056 bytes --]
From 66f6dda0507fa4699ba929379cd1c58ef8b540f5 Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Sat, 11 Mar 2017 00:09:36 -0500
Subject: [PATCH v1] Initial draft of new pretty printer (Bug#25122)
* lisp/emacs-lisp/pp.el (pp-state): New struct.
(pp--scan): New function, measures length of sublists (actually
"logical blocks" to allow for more customizable grouping than just by
lists). Calls pp--print when scanned tokens are too wide to fit on a
single line.
(pp--print): New function, prints tokens horizontally or vertically
depending on whether the sublist can fit within the line.
(pp-prin1): New function, entry point for pp--scan and pp-print.
Wraps stream so that cl-print will dispatch to prettyprinting methods.
(cl-print-object) <_ (head :pprint)>: New method, wraps
cl-prin1-to-string for prettyprinting.
(cl-print-object) <cons (head :pprint)>: New mthod, prettyprinter for
lists.
---
lisp/emacs-lisp/pp.el | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 143 insertions(+)
diff --git a/lisp/emacs-lisp/pp.el b/lisp/emacs-lisp/pp.el
index 7ef46a48bd..3809325c4b 100644
--- a/lisp/emacs-lisp/pp.el
+++ b/lisp/emacs-lisp/pp.el
@@ -24,6 +24,9 @@
;;; Code:
+(eval-when-compile (require 'cl-lib))
+(require 'ring)
+
(defvar font-lock-verbose)
(defgroup pp nil
@@ -121,6 +124,146 @@ pp-display-expression
(setq buffer-read-only nil)
(set (make-local-variable 'font-lock-verbose) nil)))))
+
+(cl-defstruct (pp-state (:constructor
+ make-pp-state
+ (stream
+ &aux
+ (right-margin fill-column)
+ (left-margin 0)
+ (indent '(0))
+ (scan-depth 0)
+ (print-depth 0)
+ (print-width 0)
+ (scan-width 0)
+ (block-mode (list nil))
+ (fifo (make-ring 30)))))
+ stream
+ right-margin ; how far we may go.
+ left-margin ; how far printer has gone
+ print-width ; total width of tokens printed so far.
+ indent ; left-margin, stack per depth.
+ scan-width ; total width of tokens scanned so far.
+ scan-depth
+ print-depth
+ block-widths
+ block-mode ; `:vertical', `:horizontal', nil (undecided); stack per depth.
+ fifo
+ )
+
+(defun pp--print (state)
+ (cl-symbol-macrolet ((stream (pp-state-stream state))
+ (depth (pp-state-print-depth state))
+ (scan-depth (pp-state-scan-depth state))
+ (fifo (pp-state-fifo state))
+ (left-margin (pp-state-left-margin state))
+ (width (pp-state-print-width state))
+ (indent (pp-state-indent state))
+ (right-margin (pp-state-right-margin state))
+ (block-mode (pp-state-block-mode state)))
+ (catch 'rescan
+ (while (not (ring-empty-p fifo))
+ (pcase (ring-remove fifo)
+ ((and `(,len . :open-block) token)
+ (if (<= len 0)
+ ;; Not ready to print this yet!
+ (progn (ring-insert-at-beginning fifo token)
+ (throw 'rescan nil))
+ (cl-incf depth)
+ (push left-margin indent)
+ (push (if (> (+ left-margin len) right-margin)
+ :vertical :horizontal)
+ block-mode)))
+ (:close-block (cl-decf depth) (pop indent) (pop block-mode))
+ (:blank
+ (pcase (car block-mode)
+ (:vertical
+ (terpri stream)
+ (princ (make-string (car indent) ?\s) stream)
+ (setf left-margin (car indent)))
+ ((or :horizontal 'nil)
+ (write-char ?\s stream)
+ (cl-incf left-margin))
+ (_ (error "oops")))
+ (cl-incf width))
+ (:eof nil)
+ ((and (pred characterp) char)
+ (write-char char stream)
+ (cl-incf left-margin (char-width char))
+ (cl-incf width (char-width char)))
+ (string
+ (princ string stream)
+ (cl-incf left-margin (string-width string))
+ (cl-incf width (string-width string))))))))
+
+(defun pp--scan (token state)
+ (cl-symbol-macrolet ((stream (pp-state-stream state))
+ (depth (pp-state-scan-depth state))
+ (print-depth (pp-state-print-depth state))
+ (fifo (pp-state-fifo state))
+ (width (pp-state-scan-width state))
+ (right-margin (pp-state-right-margin state))
+ (block-widths (pp-state-block-widths state)))
+ (cl-flet ((scanlen (len) (cl-incf width len)))
+ (cl-assert (> (ring-size fifo) (ring-length fifo)))
+ (ring-insert fifo token)
+ (pcase token
+ (:open-block
+ (cl-incf depth)
+ (let ((block-token (cons (- width) (ring-remove fifo 0))))
+ (push block-token block-widths)
+ (ring-insert fifo block-token)))
+ (:close-block
+ (cl-incf (caar block-widths) width)
+ (when (> (caar block-widths) right-margin)
+ (pp--print state))
+ (cl-decf depth)
+ (pop block-widths))
+ (:blank (scanlen 1))
+ (:eof (pp--print state))
+ ((pred characterp) (scanlen (char-width token)))
+ (_ (scanlen (string-width token)))))
+ (when block-widths
+ (when (> (+ (caar block-widths) width) right-margin)
+ (dolist (block-width block-widths)
+ (setf (car block-width) (+ right-margin 1))))
+ (when (> (caar block-widths) right-margin)
+ (pp--print state)))))
+
+(defvar cl-print-readably) ; cl-print.el
+
+(defun pp-prin1 (object &optional stream)
+ (let ((cl-print-readably nil)
+ (stream (make-pp-state (or stream standard-output))))
+ (pp--scan :open-block stream)
+ (prog1 (cl-prin1 object (cons :pprint stream))
+ (pp--scan :close-block stream)
+ (pp--scan :eof stream))))
+
+;; fallback to standard `cl-print-object'.
+(cl-defmethod cl-print-object (object (stream (head :pprint)))
+ (pp--scan (cl-prin1-to-string object) (cdr stream))
+ object)
+
+(cl-defmethod cl-print-object ((list cons) (stream (head :pprint)))
+ (let ((state (cdr stream)))
+ (pcase list
+ (`(,head . ,tail)
+ (pp--scan "(" state)
+ (pp--scan :open-block state)
+ (cl-print-object head stream)
+ (while (consp tail)
+ (pp--scan :blank state)
+ (cl-print-object (pop tail) stream))
+ (when tail
+ (pp--scan :blank state)
+ (pp--scan ?\. state)
+ (pp--scan :blank state)
+ (cl-print-object tail stream))
+ (pp--scan :close-block state)
+ (pp--scan ")" state))))
+ list)
+
;;;###autoload
(defun pp-eval-expression (expression)
"Evaluate EXPRESSION and pretty-print its value.
--
2.11.1
^ permalink raw reply related [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2016-12-06 6:41 ` Thierry Volpiatto
2016-12-07 3:50 ` npostavs
@ 2017-03-11 15:21 ` Stefan Monnier
2017-03-11 15:35 ` npostavs
2017-03-11 19:26 ` Thierry Volpiatto
1 sibling, 2 replies; 27+ messages in thread
From: Stefan Monnier @ 2017-03-11 15:21 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122
> (cl-letf (((symbol-function 'pp)
> (lambda (object &optional stream)
> (let ((fn (lambda (ob &optional stream)
> (princ (pp-to-string ob)
> (or stream standard-output))
> (terpri)))
> (print-circle t))
> (if (consp object)
> (progn
> (insert "\n(")
> (mapc fn object)
> (cl-letf (((point) (1- (point))))
> (insert ")")))
> (funcall fn object stream))))))
Hmm... I wonder why this would be faster. In the past, the
implementation of `print-circle` had a poor complexity, but we fixed
that around Emacs-24, IIRC so it now uses a hash-table and should have
O(n) complexity, which means that pp shouldn't be slower than (mapc
#'pp).
Stefan
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-11 5:40 ` npostavs
@ 2017-03-11 15:33 ` Stefan Monnier
2017-03-11 19:29 ` Thierry Volpiatto
2017-03-11 19:34 ` Thierry Volpiatto
1 sibling, 1 reply; 27+ messages in thread
From: Stefan Monnier @ 2017-03-11 15:33 UTC (permalink / raw)
To: npostavs; +Cc: 25122, Boruch Baum, Thierry Volpiatto
> Actually, it's the indent-sexp on the whole object that takes time.
> Possibly we could sacrifice some indentation correctness if the printed
> representation is big.
Actually, I think that trying to optimize this is bound to fail: we want
*Help* to show up "instantly", so we're off by a factor of more
than 100, which seems way out of reach (unless there's really an
algorithmic problem in our code, admittedly).
I think the better way out is to do less work. E.g. bound the total
amount printed (and replace the rest with "..." that can be expanded on
demand).
Stefan
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-11 15:21 ` Stefan Monnier
@ 2017-03-11 15:35 ` npostavs
2017-03-11 19:26 ` Thierry Volpiatto
1 sibling, 0 replies; 27+ messages in thread
From: npostavs @ 2017-03-11 15:35 UTC (permalink / raw)
To: Stefan Monnier; +Cc: 25122, Thierry Volpiatto
Stefan Monnier <monnier@iro.umontreal.ca> writes:
>> (cl-letf (((symbol-function 'pp)
>> (lambda (object &optional stream)
>> (let ((fn (lambda (ob &optional stream)
>> (princ (pp-to-string ob)
>> (or stream standard-output))
>> (terpri)))
>> (print-circle t))
>> (if (consp object)
>> (progn
>> (insert "\n(")
>> (mapc fn object)
>> (cl-letf (((point) (1- (point))))
>> (insert ")")))
>> (funcall fn object stream))))))
>
> Hmm... I wonder why this would be faster. In the past, the
> implementation of `print-circle` had a poor complexity, but we fixed
> that around Emacs-24, IIRC so it now uses a hash-table and should have
> O(n) complexity, which means that pp shouldn't be slower than (mapc
> #'pp).
I think it's because when we indent-sexp only on individual entries, we
don't parse as far back.
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-11 15:21 ` Stefan Monnier
2017-03-11 15:35 ` npostavs
@ 2017-03-11 19:26 ` Thierry Volpiatto
1 sibling, 0 replies; 27+ messages in thread
From: Thierry Volpiatto @ 2017-03-11 19:26 UTC (permalink / raw)
To: Stefan Monnier; +Cc: 25122
Stefan Monnier <monnier@iro.umontreal.ca> writes:
>> (cl-letf (((symbol-function 'pp)
>> (lambda (object &optional stream)
>> (let ((fn (lambda (ob &optional stream)
>> (princ (pp-to-string ob)
>> (or stream standard-output))
>> (terpri)))
>> (print-circle t))
>> (if (consp object)
>> (progn
>> (insert "\n(")
>> (mapc fn object)
>> (cl-letf (((point) (1- (point))))
>> (insert ")")))
>> (funcall fn object stream))))))
>
> Hmm... I wonder why this would be faster.
However it is fast, it is anyway better than the actual situation were e.g C-h v
load-history takes minutes in the best case to load the 1.8M help buffer.
With this code it takes around 1s.
> In the past, the implementation of `print-circle` had a poor
> complexity, but we fixed that around Emacs-24, IIRC so it now uses a
> hash-table and should have O(n) complexity, which means that pp
> shouldn't be slower than (mapc #'pp).
The code above run at the same speed with and without print-circle...
--
Thierry
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-11 15:33 ` Stefan Monnier
@ 2017-03-11 19:29 ` Thierry Volpiatto
2017-03-11 21:59 ` npostavs
0 siblings, 1 reply; 27+ messages in thread
From: Thierry Volpiatto @ 2017-03-11 19:29 UTC (permalink / raw)
To: Stefan Monnier; +Cc: 25122, Boruch Baum, npostavs
Stefan Monnier <monnier@iro.umontreal.ca> writes:
> I think the better way out is to do less work. E.g. bound the total
> amount printed (and replace the rest with "..." that can be expanded on
> demand).
The problem will be here again when you hit RET on "..."
--
Thierry
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-11 5:40 ` npostavs
2017-03-11 15:33 ` Stefan Monnier
@ 2017-03-11 19:34 ` Thierry Volpiatto
2017-03-12 16:07 ` npostavs
1 sibling, 1 reply; 27+ messages in thread
From: Thierry Volpiatto @ 2017-03-11 19:34 UTC (permalink / raw)
To: npostavs; +Cc: 25122, Boruch Baum
npostavs@users.sourceforge.net writes:
> I've been attempting an alternate approach which prettyprints the object
> while scanning it instead of the current way of printing and then
> reindenting. Without optimizing, it's about 3 times as fast as the
> current pp (it's the pp-prin1 in the benchmarks below), though more than
> 3 times slower than your mapc pp trick. On the other hand, it also
> doesn't yet handle function-specific indentation or any compound
> structure apart from lists, so I'm not sure if it will end up being much
> faster.
>
> (benchmark 1 '(with-temp-buffer (pp-prin1 long-list (current-buffer)) nil)) "Elapsed time: 3.391232s (0.565806s in 11 GCs)"
> (benchmark 1 '(progn (pp-to-string long-list) nil)) "Elapsed time: 9.988515s (0.148034s in 3 GCs)"
> (benchmark 1 '(progn (with-output-to-string (mapc 'pp long-list)) nil)) "Elapsed time: 0.983493s (0.144424s in 3 GCs)"
> (benchmark 1 '(progn (cl-prin1-to-string long-list) nil)) "Elapsed time: 0.511617s (0.152483s in 3 GCs)"
> (benchmark 1 '(progn (prin1-to-string long-list) nil)) "Elapsed time: 0.029320s"
Interesting, thanks to work on this.
--
Thierry
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-11 19:29 ` Thierry Volpiatto
@ 2017-03-11 21:59 ` npostavs
2017-03-11 23:55 ` Drew Adams
2017-03-12 5:57 ` Thierry Volpiatto
0 siblings, 2 replies; 27+ messages in thread
From: npostavs @ 2017-03-11 21:59 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Stefan Monnier, Boruch Baum
Thierry Volpiatto <thierry.volpiatto@gmail.com> writes:
> Stefan Monnier <monnier@iro.umontreal.ca> writes:
>
>> I think the better way out is to do less work. E.g. bound the total
>> amount printed (and replace the rest with "..." that can be expanded on
>> demand).
Yes, there's no method that will achieve "instant" speeds for
load-history sized lists (well except for plain prin1, but that output
is hardly readable).
> The problem will be here again when you hit RET on "..."
Perhaps we could run the printing in a thread and suspend it after
printing X lines. Then hitting RET on "..." would just print another X
lines.
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-11 21:59 ` npostavs
@ 2017-03-11 23:55 ` Drew Adams
2017-03-12 5:57 ` Thierry Volpiatto
1 sibling, 0 replies; 27+ messages in thread
From: Drew Adams @ 2017-03-11 23:55 UTC (permalink / raw)
To: npostavs, Thierry Volpiatto; +Cc: 25122, Stefan Monnier, Boruch Baum
> >> I think the better way out is to do less work. E.g. bound the total
> >> amount printed (and replace the rest with "..." that can be expanded on
> >> demand).
>
> Yes, there's no method that will achieve "instant" speeds for
> load-history sized lists (well except for plain prin1, but that output
> is hardly readable).
>
> > The problem will be here again when you hit RET on "..."
>
> Perhaps we could run the printing in a thread and suspend it after
> printing X lines. Then hitting RET on "..." would just print another X
> lines.
(Caveat: I'm not really following this thread.)
If you plan to do things like what I think you're suggesting,
which will make a user hit several keys (e.g. RET on ...
repeatedly) then please make that behavior _optional_.
Give users the _possibility_ to just print the whole thing,
without any other action (no prefix arg, no clicking ... etc.).
I generally don't mind waiting, for example, and I don't
want to have to hit ... and wait for a bit more - I want
the whole thing the first time.
And if/when you do show the value only partially, please
make that _obvious_ to users. They should _immediately_
know that they are not seeing the full value.
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-11 21:59 ` npostavs
2017-03-11 23:55 ` Drew Adams
@ 2017-03-12 5:57 ` Thierry Volpiatto
2017-03-12 14:07 ` Stefan Monnier
2017-03-12 14:15 ` npostavs
1 sibling, 2 replies; 27+ messages in thread
From: Thierry Volpiatto @ 2017-03-12 5:57 UTC (permalink / raw)
To: npostavs; +Cc: 25122, Stefan Monnier, Boruch Baum
npostavs@users.sourceforge.net writes:
> Perhaps we could run the printing in a thread
This is a good idea.
> and suspend it after printing X lines.
Instead just print something like "Computing foo value ..." and let finish
the thread, letting user reading and moving cursor in docstring while it
finishes, once done save-excursion and send message "Computing foo value
done".
> Then hitting RET on "..." would just print another X lines.
I think like Drew that this would be annoying.
That said, what's the reason of choosing the slower approach to compute
value (in a thread or not) instead of using the approach described in
the advice I sent here which takes 1s to compute load-history instead of
3mn ? (I use this advice since one year now without any problems).
--
Thierry
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-12 5:57 ` Thierry Volpiatto
@ 2017-03-12 14:07 ` Stefan Monnier
2017-03-12 14:15 ` npostavs
1 sibling, 0 replies; 27+ messages in thread
From: Stefan Monnier @ 2017-03-12 14:07 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Boruch Baum, npostavs
> That said, what's the reason of choosing the slower approach to compute
> value (in a thread or not) instead of using the approach described in
> the advice I sent here which takes 1s to compute load-history instead of
> 3mn ? (I use this advice since one year now without any problems).
Yes, we should definitely do that. I'm still not sure why it's so much
faster: it indicates we have a performance bug in pp.el and all it take
is to fix it.
Stefan
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-12 5:57 ` Thierry Volpiatto
2017-03-12 14:07 ` Stefan Monnier
@ 2017-03-12 14:15 ` npostavs
2017-03-12 14:59 ` Drew Adams
` (2 more replies)
1 sibling, 3 replies; 27+ messages in thread
From: npostavs @ 2017-03-12 14:15 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Stefan Monnier, Boruch Baum
Thierry Volpiatto <thierry.volpiatto@gmail.com> writes:
> npostavs@users.sourceforge.net writes:
>
>> and suspend it after printing X lines.
>
> Instead just print something like "Computing foo value ..." and let finish
> the thread, letting user reading and moving cursor in docstring while it
> finishes, once done save-excursion and send message "Computing foo value
> done".
Threads aren't truly parallel. The user can't do anything while the
thread is running.
>
>> Then hitting RET on "..." would just print another X lines.
>
> I think like Drew that this would be annoying.
I wonder if we could just hook this into scrolling somehow? So the
lines would only be printed when you scroll to look at them.
>
> That said, what's the reason of choosing the slower approach to compute
> value (in a thread or not) instead of using the approach described in
> the advice I sent here which takes 1s to compute load-history instead of
> 3mn ? (I use this advice since one year now without any problems).
As mentioned in https://debbugs.gnu.org/cgi/bugreport.cgi?bug=21717#8,
it breaks circularity. Try describing this variable:
(defvar circular-list
(let ((l (number-sequence 1 100)))
(setcdr (last l) l)
l)
"A circular list that has problems with (mapc 'pp val).")
We could probably achieve something similar without breaking circular
printing by not calling indent-sexp on the full list, but 1s is longer
than "instant" anyway (which is about 50ms or less) which is why I'm
exploring other options.
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-12 14:15 ` npostavs
@ 2017-03-12 14:59 ` Drew Adams
2017-03-12 16:29 ` Stefan Monnier
2017-03-12 16:32 ` npostavs
2 siblings, 0 replies; 27+ messages in thread
From: Drew Adams @ 2017-03-12 14:59 UTC (permalink / raw)
To: npostavs, Thierry Volpiatto; +Cc: 25122, Stefan Monnier, Boruch Baum
> >> Then hitting RET on "..." would just print another X lines.
> >
> > I think like Drew that this would be annoying.
>
> I wonder if we could just hook this into scrolling somehow? So the
> lines would only be printed when you scroll to look at them.
I still would not like that behavior, so would would want to
opt out, personally. If it take a minute to display *Help*
I can at least do something different (outside Emacs) during
that time. When I'm scrolling I'm likely examining the value
as it scrolls, and I don't want to wait (scrolling in chunks
separated by delays).
E.g., I do `C-h v bookmark-alist' or `load-history' with a
large list. I know that it will not display immediately,
so I don't grumble about that fact. I'm free not to sit and
stare at the screen waiting for it to return. What you
describe just chops up the wait into scrolled chunks.
> > That said, what's the reason of choosing the slower approach to compute
> > value (in a thread or not) instead of using the approach described in
> > the advice I sent here which takes 1s to compute load-history instead of
> > 3mn ? (I use this advice since one year now without any problems).
>
> As mentioned in https://debbugs.gnu.org/cgi/bugreport.cgi?bug=21717#8,
> it breaks circularity. Try describing this variable:
>
> (defvar circular-list
> (let ((l (number-sequence 1 100))) (setcdr (last l) l) l) "")
>
> We could probably achieve something similar without breaking circular
> printing by not calling indent-sexp on the full list, but 1s is longer
> than "instant" anyway (which is about 50ms or less) which is why I'm
> exploring other options.
1 sec is not a problem, IMO. 0.7 sec is a typical Emacs
`sit-for' value, i.e., something that, yes, allows time to
notice the change/wait, but it is not so long that it
becomes annoying.
(It would be annoying if it happened for all or most *Help*
displays, but printing large values is the exception.)
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-11 19:34 ` Thierry Volpiatto
@ 2017-03-12 16:07 ` npostavs
0 siblings, 0 replies; 27+ messages in thread
From: npostavs @ 2017-03-12 16:07 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Boruch Baum
[-- Attachment #1: Type: text/plain, Size: 2534 bytes --]
Thierry Volpiatto <thierry.volpiatto@gmail.com> writes:
> npostavs@users.sourceforge.net writes:
>
>> I've been attempting an alternate approach which prettyprints the object
>> while scanning it instead of the current way of printing and then
>> reindenting. Without optimizing, it's about 3 times as fast as the
>> current pp (it's the pp-prin1 in the benchmarks below), though more than
>> 3 times slower than your mapc pp trick. On the other hand, it also
>> doesn't yet handle function-specific indentation or any compound
>> structure apart from lists, so I'm not sure if it will end up being much
>> faster.
>>
>> (benchmark 1 '(with-temp-buffer (pp-prin1 long-list (current-buffer)) nil)) "Elapsed time: 3.391232s (0.565806s in 11 GCs)"
>> (benchmark 1 '(progn (pp-to-string long-list) nil)) "Elapsed time: 9.988515s (0.148034s in 3 GCs)"
>> (benchmark 1 '(progn (with-output-to-string (mapc 'pp long-list)) nil)) "Elapsed time: 0.983493s (0.144424s in 3 GCs)"
>> (benchmark 1 '(progn (cl-prin1-to-string long-list) nil)) "Elapsed time: 0.511617s (0.152483s in 3 GCs)"
>> (benchmark 1 '(progn (prin1-to-string long-list) nil)) "Elapsed time: 0.029320s"
>
> Interesting, thanks to work on this.
With a couple of minor optimizations it's down to about 1.8s. The first
is to reuse the tempbuffer instead of letting cl-prin1-to-string make a
new one each time. Second is to add
(eval-when-compile
(cl-proclaim '(optimize (speed 3) (safety 0))))
This also stops these warnings (I guess they're caused by the "safety" code?):
../../emacs-master/lisp/emacs-lisp/pp.el:159:19:Warning: value returned from
(aref state 6) is unused
../../emacs-master/lisp/emacs-lisp/pp.el:204:24:Warning: value returned from
(aref state 10) is unused
New times:
(benchmark 1 '(with-temp-buffer (pp-prin1 long-list (current-buffer)) nil)) "Elapsed time: 1.800146s (0.231706s in 6 GCs)"
(benchmark 1 '(progn (pp-to-string long-list) nil)) "Elapsed time: 9.950225s (0.154100s in 4 GCs)"
(benchmark 1 '(progn (with-output-to-string (mapc 'pp long-list)) nil)) "Elapsed time: 0.980923s (0.149787s in 4 GCs)"
I foolishly neglected to write down what exactly long-list was before,
starting from emacs -Q this seems to approximate it though:
(progn (require 'pp)
(require 'dabbrev)
(require 'edebug)
(require 'cc-mode)
(require 'vc)
(setq long-list load-history)
(length long-list)) ;=> 142
[-- Attachment #2: patch --]
[-- Type: text/plain, Size: 7289 bytes --]
From e4b1a2ef3b4b11466e81d639a09ff671318e0968 Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Sat, 11 Mar 2017 00:09:36 -0500
Subject: [PATCH v2] New pretty printer (Bug#25122)
* lisp/emacs-lisp/pp.el (pp-state): New struct.
(pp--scan): New function, measures length of sublists (actually
"logical blocks" to allow for more customizable grouping than just by
lists). Calls pp--print when scanned tokens are too wide to fit on a
single line.
(pp--print): New function, prints tokens horizontally or vertically
depending on whether the sublist can fit within the line.
(pp-prin1): New function, entry point for pp--scan and pp-print.
(pp-print-object): New generic function.
(pp-print-object) <cons , _>: New method, prettyprinter for lists.
---
lisp/emacs-lisp/pp.el | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 156 insertions(+)
diff --git a/lisp/emacs-lisp/pp.el b/lisp/emacs-lisp/pp.el
index 7ef46a48bd..8c2ed24ffd 100644
--- a/lisp/emacs-lisp/pp.el
+++ b/lisp/emacs-lisp/pp.el
@@ -24,6 +24,9 @@
;;; Code:
+(eval-when-compile (require 'cl-lib))
+(require 'ring)
+
(defvar font-lock-verbose)
(defgroup pp nil
@@ -121,6 +124,159 @@ pp-display-expression
(setq buffer-read-only nil)
(set (make-local-variable 'font-lock-verbose) nil)))))
+(eval-when-compile
+ ;; FIXME: should we try to restore original settings? (how?)
+ (cl-proclaim '(optimize (speed 3) (safety 0))))
+
+(cl-defstruct (pp-state (:constructor
+ make-pp-state
+ (stream
+ tempbuffer
+ right-margin
+ &aux
+ (left-margin 0)
+ (indent '(0))
+ (scan-depth 0)
+ (print-depth 0)
+ (print-width 0)
+ (scan-width 0)
+ (block-mode (list nil))
+ (fifo (make-ring 30)))))
+ stream
+ tempbuffer
+ right-margin ; how far we may go.
+ left-margin ; how far printer has gone
+ print-width ; total width of tokens printed so far.
+ indent ; left-margin, stack per depth.
+ scan-width ; total width of tokens scanned so far.
+ scan-depth
+ print-depth
+ block-widths
+ block-mode ; `:vertical', `:horizontal', nil (undecided); stack per depth.
+ fifo
+ )
+
+(defun pp--print (state)
+ (cl-symbol-macrolet ((stream (pp-state-stream state))
+ (depth (pp-state-print-depth state))
+ (scan-depth (pp-state-scan-depth state))
+ (fifo (pp-state-fifo state))
+ (left-margin (pp-state-left-margin state))
+ (width (pp-state-print-width state))
+ (indent (pp-state-indent state))
+ (right-margin (pp-state-right-margin state))
+ (block-mode (pp-state-block-mode state)))
+ (catch 'rescan
+ (while (not (ring-empty-p fifo))
+ (pcase (ring-remove fifo)
+ ((and `(,len . :open-block) token)
+ (if (<= len 0)
+ ;; Not ready to print this yet!
+ (progn (ring-insert-at-beginning fifo token)
+ (throw 'rescan nil))
+ (cl-incf depth)
+ (push left-margin indent)
+ (push (if (> (+ left-margin len) right-margin)
+ :vertical :horizontal)
+ block-mode)))
+ (:close-block (cl-decf depth) (pop indent) (pop block-mode))
+ (:blank
+ (pcase (car block-mode)
+ (:vertical
+ (terpri stream)
+ (princ (make-string (car indent) ?\s) stream)
+ (setf left-margin (car indent)))
+ ((or :horizontal 'nil)
+ (write-char ?\s stream)
+ (cl-incf left-margin))
+ (_ (error "oops")))
+ (cl-incf width))
+ (:eof nil)
+ ((and (pred characterp) char)
+ (write-char char stream)
+ (cl-incf left-margin (char-width char))
+ (cl-incf width (char-width char)))
+ (string
+ (princ string stream)
+ (cl-incf left-margin (string-width string))
+ (cl-incf width (string-width string))))))))
+
+(defun pp--scan (token state)
+ (cl-symbol-macrolet ((stream (pp-state-stream state))
+ (depth (pp-state-scan-depth state))
+ (print-depth (pp-state-print-depth state))
+ (fifo (pp-state-fifo state))
+ (width (pp-state-scan-width state))
+ (right-margin (pp-state-right-margin state))
+ (block-widths (pp-state-block-widths state)))
+ (cl-flet ((scanlen (len) (cl-incf width len)))
+ (cl-assert (> (ring-size fifo) (ring-length fifo)))
+ (ring-insert fifo token)
+ (pcase token
+ (:open-block
+ (cl-incf depth)
+ (let ((block-token (cons (- width) (ring-remove fifo 0))))
+ (push block-token block-widths)
+ (ring-insert fifo block-token)))
+ (:close-block
+ (cl-incf (caar block-widths) width)
+ (when (> (caar block-widths) right-margin)
+ (pp--print state))
+ (cl-decf depth)
+ (pop block-widths))
+ (:blank (scanlen 1))
+ (:eof (pp--print state))
+ ((pred characterp) (scanlen (char-width token)))
+ (_ (scanlen (string-width token)))))
+ (when block-widths
+ (when (> (+ (caar block-widths) width) right-margin)
+ (dolist (block-width block-widths)
+ (setf (car block-width) (+ right-margin 1))))
+ (when (> (caar block-widths) right-margin)
+ (pp--print state)))))
+
+(defvar cl-print-readably) ; cl-print.el
+
+(defun pp-prin1 (object &optional stream right-margin)
+ (unless right-margin
+ (setq right-margin fill-column))
+ (with-temp-buffer
+ (let ((cl-print-readably nil)
+ (state (make-pp-state (or stream standard-output) (current-buffer)
+ right-margin)))
+ (pp--scan :open-block state)
+ (prog1 (pp-print-object object state)
+ (pp--scan :close-block state)
+ (pp--scan :eof state)))))
+
+
+(cl-defgeneric pp-print-object (object state)
+ ;; Fallback to standard `cl-print-object'.
+ (pp--scan (with-current-buffer (pp-state-tempbuffer state)
+ (cl-prin1 object (current-buffer))
+ (prog1 (buffer-string)
+ (erase-buffer)))
+ state)
+ object)
+
+(cl-defmethod pp-print-object ((list cons) state)
+ (pcase list
+ (`(,head . ,tail)
+ (pp--scan "(" state)
+ (pp--scan :open-block state)
+ (pp-print-object head state)
+ (while (consp tail)
+ (pp--scan :blank state)
+ (pp-print-object (pop tail) state))
+ (when tail
+ (pp--scan :blank state)
+ (pp--scan ?\. state)
+ (pp--scan :blank state)
+ (pp-print-object tail state))
+ (pp--scan :close-block state)
+ (pp--scan ")" state)))
+ list)
+
;;;###autoload
(defun pp-eval-expression (expression)
"Evaluate EXPRESSION and pretty-print its value.
--
2.11.1
^ permalink raw reply related [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-12 14:15 ` npostavs
2017-03-12 14:59 ` Drew Adams
@ 2017-03-12 16:29 ` Stefan Monnier
2017-03-12 16:32 ` npostavs
2 siblings, 0 replies; 27+ messages in thread
From: Stefan Monnier @ 2017-03-12 16:29 UTC (permalink / raw)
To: npostavs; +Cc: 25122, Boruch Baum, Thierry Volpiatto
> Threads aren't truly parallel. The user can't do anything while the
> thread is running.
I think the idea is to build the printout while the user is reading the
docstring (for example). The loop that prints would have regular
`yield` calls so that as soon as not to block other operations.
> I wonder if we could just hook this into scrolling somehow?
We definitely could (at least, there's no fundamental reason why we
can't insert the text from a jit-lock thingy, although modifying the
buffer text from jit-lock might trigger some latent bugs).
> As mentioned in https://debbugs.gnu.org/cgi/bugreport.cgi?bug=21717#8,
> it breaks circularity. Try describing this variable:
> (defvar circular-list
> (let ((l (number-sequence 1 100)))
> (setcdr (last l) l)
> l)
> "A circular list that has problems with (mapc 'pp val).")
That's why I think the (mapc #'pp) trick is an interesting observation
(it points to a performance bug that should be easy to fix) but is not
a solution in itself.
Stefan
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-12 14:15 ` npostavs
2017-03-12 14:59 ` Drew Adams
2017-03-12 16:29 ` Stefan Monnier
@ 2017-03-12 16:32 ` npostavs
2017-03-13 4:47 ` npostavs
2 siblings, 1 reply; 27+ messages in thread
From: npostavs @ 2017-03-12 16:32 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Stefan Monnier, Boruch Baum
> We could probably achieve something similar without breaking circular
> printing by not calling indent-sexp on the full list
The following patch is almost as fast as the mapc 'pp trick, but doesn't
get stuck on circular lists. The indentation is a bit off: sublists
after the first one incorrectly start in column 0. Also, this doesn't
really solve the performance problem, it just makes it much less likely
to occur, e.g., (pp (list load-history)) is still slow.
--- i/lisp/emacs-lisp/pp.el
+++ w/lisp/emacs-lisp/pp.el
@@ -76,9 +76,15 @@ pp-buffer
(progn (skip-chars-forward " \t\n") (point)))
(insert ?\n))
(t (goto-char (point-max)))))
(goto-char (point-min))
- (indent-sexp))
+ (condition-case () (down-list)
+ (scan-error nil))
+ (while (and (not (eobp))
+ (condition-case () (progn (indent-sexp)
+ (forward-sexp)
+ t)
+ (scan-error nil)))))
;;;###autoload
(defun pp (object &optional stream)
"Output the pretty-printed representation of OBJECT, any Lisp object.
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-12 16:32 ` npostavs
@ 2017-03-13 4:47 ` npostavs
2017-03-13 14:01 ` npostavs
0 siblings, 1 reply; 27+ messages in thread
From: npostavs @ 2017-03-13 4:47 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Stefan Monnier, Boruch Baum
[-- Attachment #1: Type: text/plain, Size: 258 bytes --]
tags 25122 patch
quit
npostavs@users.sourceforge.net writes:
> Also, this doesn't really solve the performance problem, it just makes
> it much less likely to occur, e.g., (pp (list load-history)) is still
> slow.
Okay, I think I found the real fix now:
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: patch --]
[-- Type: text/x-diff, Size: 3568 bytes --]
From 5188d6e366426d83934505296b585744f50e24a5 Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Sun, 12 Mar 2017 23:59:19 -0400
Subject: [PATCH] Don't reparse the sexp in indent-sexp (Bug#25122)
* lisp/emacs-lisp/lisp-mode.el (calculate-lisp-indent): Let
PARSE-START be a parse state that can be reused.
(indent-sexp): Pass the running parse state to calculate-lisp-indent
instead of the sexp beginning position.
---
lisp/emacs-lisp/lisp-mode.el | 31 ++++++++++++++++++-------------
1 file changed, 18 insertions(+), 13 deletions(-)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index eb07c18b03..8d4abc24e8 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -781,6 +781,10 @@ calculate-lisp-indent
If the value is nil, that means don't change the indentation
because the line starts inside a string.
+PARSE-START may be a buffer position to start parsing from, or a
+parse state as returned by calling `parse-partial-sexp' up to the
+beginning of the current line.
+
The value can also be a list of the form (COLUMN CONTAINING-SEXP-START).
This means that following lines at the same level of indentation
should not necessarily be indented the same as this line.
@@ -794,12 +798,14 @@ calculate-lisp-indent
(desired-indent nil)
(retry t)
calculate-lisp-indent-last-sexp containing-sexp)
- (if parse-start
- (goto-char parse-start)
- (beginning-of-defun))
- ;; Find outermost containing sexp
- (while (< (point) indent-point)
- (setq state (parse-partial-sexp (point) indent-point 0)))
+ (cond ((or (markerp parse-start) (integerp parse-start))
+ (goto-char parse-start))
+ ((null parse-start) (beginning-of-defun))
+ (t (setq state parse-start)))
+ (unless state
+ ;; Find outermost containing sexp
+ (while (< (point) indent-point)
+ (setq state (parse-partial-sexp (point) indent-point 0))))
;; Find innermost containing sexp
(while (and retry
state
@@ -1070,11 +1076,6 @@ indent-sexp
ENDPOS is encountered."
(interactive)
(let* ((indent-stack (list nil))
- ;; If ENDPOS is non-nil, use beginning of defun as STARTING-POINT.
- ;; If ENDPOS is nil, it is safe not to scan before point
- ;; since every line we indent is more deeply nested than point is.
- (starting-point (save-excursion (if endpos (beginning-of-defun))
- (point)))
;; Use `syntax-ppss' to get initial state so we don't get
;; confused by starting inside a string. We don't use
;; `syntax-ppss' in the loop, because this is measurably
@@ -1132,8 +1133,12 @@ indent-sexp
(unless (or (eolp) (eq (char-syntax (char-after)) ?<))
(let ((this-indent (car indent-stack)))
(when (listp this-indent)
- (let ((val (calculate-lisp-indent
- (or (car this-indent) starting-point))))
+ ;; The state here is actually to the end of the
+ ;; previous line, but that's fine for our purposes.
+ ;; And continuing the parse to the next line would
+ ;; destroy element 2 (last sexp position) which
+ ;; `calculate-lisp-indent' needs.
+ (let ((val (calculate-lisp-indent state)))
(setq
this-indent
(cond ((integerp val)
--
2.11.1
^ permalink raw reply related [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-13 4:47 ` npostavs
@ 2017-03-13 14:01 ` npostavs
2017-03-16 2:54 ` npostavs
0 siblings, 1 reply; 27+ messages in thread
From: npostavs @ 2017-03-13 14:01 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Stefan Monnier, Boruch Baum
[-- Attachment #1: Type: text/plain, Size: 307 bytes --]
npostavs@users.sourceforge.net writes:
> npostavs@users.sourceforge.net writes:
>
>> Also, this doesn't really solve the performance problem, it just makes
>> it much less likely to occur, e.g., (pp (list load-history)) is still
>> slow.
>
> Okay, I think I found the real fix now:
Missed a corner case.
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: patch --]
[-- Type: text/x-diff, Size: 7743 bytes --]
From b8dd372f65ce8979324c00b12a9ae767c1ffabda Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Sun, 12 Mar 2017 23:59:19 -0400
Subject: [PATCH v2] Don't reparse the sexp in indent-sexp (Bug#25122)
* lisp/emacs-lisp/lisp-mode.el (calculate-lisp-indent): Let
PARSE-START be a parse state that can be reused.
(indent-sexp): Pass the running parse state to calculate-lisp-indent
instead of the sexp beginning position. Saving the
CONTAINING-SEXP-START returned by `calculate-lisp-indent' is no longer
needed.
* test/lisp/emacs-lisp/lisp-mode-tests.el (indent-sexp): Add blank
line to test case.
---
lisp/emacs-lisp/lisp-mode.el | 71 ++++++++++++++++++---------------
test/lisp/emacs-lisp/lisp-mode-tests.el | 5 ++-
2 files changed, 42 insertions(+), 34 deletions(-)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index eb07c18b03..3fefb69066 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -781,6 +781,10 @@ calculate-lisp-indent
If the value is nil, that means don't change the indentation
because the line starts inside a string.
+PARSE-START may be a buffer position to start parsing from, or a
+parse state as returned by calling `parse-partial-sexp' up to the
+beginning of the current line.
+
The value can also be a list of the form (COLUMN CONTAINING-SEXP-START).
This means that following lines at the same level of indentation
should not necessarily be indented the same as this line.
@@ -794,12 +798,14 @@ calculate-lisp-indent
(desired-indent nil)
(retry t)
calculate-lisp-indent-last-sexp containing-sexp)
- (if parse-start
- (goto-char parse-start)
- (beginning-of-defun))
- ;; Find outermost containing sexp
- (while (< (point) indent-point)
- (setq state (parse-partial-sexp (point) indent-point 0)))
+ (cond ((or (markerp parse-start) (integerp parse-start))
+ (goto-char parse-start))
+ ((null parse-start) (beginning-of-defun))
+ (t (setq state parse-start)))
+ (unless state
+ ;; Find outermost containing sexp
+ (while (< (point) indent-point)
+ (setq state (parse-partial-sexp (point) indent-point 0))))
;; Find innermost containing sexp
(while (and retry
state
@@ -1070,11 +1076,6 @@ indent-sexp
ENDPOS is encountered."
(interactive)
(let* ((indent-stack (list nil))
- ;; If ENDPOS is non-nil, use beginning of defun as STARTING-POINT.
- ;; If ENDPOS is nil, it is safe not to scan before point
- ;; since every line we indent is more deeply nested than point is.
- (starting-point (save-excursion (if endpos (beginning-of-defun))
- (point)))
;; Use `syntax-ppss' to get initial state so we don't get
;; confused by starting inside a string. We don't use
;; `syntax-ppss' in the loop, because this is measurably
@@ -1093,16 +1094,21 @@ indent-sexp
(save-excursion
(while (< (point) endpos)
;; Parse this line so we can learn the state to indent the
- ;; next line.
- (while (progn
- (setq state (parse-partial-sexp
- last-syntax-point (progn (end-of-line) (point))
- nil nil state))
- ;; Skip over newlines within strings.
- (nth 3 state))
- (setq state (parse-partial-sexp (point) (point-max)
- nil nil state 'syntax-table))
- (setq last-syntax-point (point)))
+ ;; next line. Preserve element 2 of the state (last sexp) for
+ ;; `calculate-lisp-indent'.
+ (let ((last-sexp (nth 2 state)))
+ (while (progn
+ (setq state (parse-partial-sexp
+ last-syntax-point (progn (end-of-line) (point))
+ nil nil state))
+ (setq last-sexp (or (nth 2 state) last-sexp))
+ ;; Skip over newlines within strings.
+ (nth 3 state))
+ (setq state (parse-partial-sexp (point) (point-max)
+ nil nil state 'syntax-table))
+ (setq last-sexp (or (nth 2 state) last-sexp))
+ (setq last-syntax-point (point)))
+ (setf (nth 2 state) last-sexp))
(setq next-depth (car state))
;; If the line contains a comment indent it now with
;; `indent-for-comment'.
@@ -1115,6 +1121,8 @@ indent-sexp
(make-list (- init-depth next-depth) nil))
last-depth (- last-depth next-depth)
next-depth init-depth))
+ ;; Now indent the next line according to what we learned from
+ ;; parsing the previous one.
(forward-line 1)
(when (< (point) endpos)
(let ((depth-delta (- next-depth last-depth)))
@@ -1124,28 +1132,25 @@ indent-sexp
(setq indent-stack (nconc (make-list depth-delta nil)
indent-stack))))
(setq last-depth next-depth))
- ;; Now indent the next line according
- ;; to what we learned from parsing the previous one.
- (skip-chars-forward " \t")
;; But not if the line is blank, or just a comment (we
;; already called `indent-for-comment' above).
+ (skip-chars-forward " \t")
(unless (or (eolp) (eq (char-syntax (char-after)) ?<))
- (let ((this-indent (car indent-stack)))
- (when (listp this-indent)
- (let ((val (calculate-lisp-indent
- (or (car this-indent) starting-point))))
- (setq
- this-indent
+ (indent-line-to
+ (or (car indent-stack)
+ ;; The state here is actually to the end of the
+ ;; previous line, but that's fine for our purposes.
+ ;; And parsing over the newline would only destroy
+ ;; element 2 (last sexp position).
+ (let ((val (calculate-lisp-indent state)))
(cond ((integerp val)
(setf (car indent-stack) val))
((consp val) ; (COLUMN CONTAINING-SEXP-START)
- (setf (car indent-stack) (cdr val))
(car val))
;; `calculate-lisp-indent' only returns nil
;; when we're in a string, but this won't
;; happen because we skip strings above.
- (t (error "This shouldn't happen!"))))))
- (indent-line-to this-indent))))))))
+ (t (error "This shouldn't happen!"))))))))))))
(defun indent-pp-sexp (&optional arg)
"Indent each line of the list starting just after point, or prettyprint it.
diff --git a/test/lisp/emacs-lisp/lisp-mode-tests.el b/test/lisp/emacs-lisp/lisp-mode-tests.el
index 2801f23df6..848dd7ca3e 100644
--- a/test/lisp/emacs-lisp/lisp-mode-tests.el
+++ b/test/lisp/emacs-lisp/lisp-mode-tests.el
@@ -31,6 +31,9 @@
1
2)
2)
+ (fun arg1
+
+ arg2)
(1
\"string
noindent\" (\"string2
@@ -58,7 +61,7 @@
(save-excursion
(let ((n 0))
(while (not (eobp))
- (unless (looking-at "noindent")
+ (unless (looking-at "noindent\\|^[[:blank:]]*$")
(insert (make-string n ?\s)))
(cl-incf n)
(forward-line))))
--
2.11.1
^ permalink raw reply related [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-13 14:01 ` npostavs
@ 2017-03-16 2:54 ` npostavs
2017-04-18 3:53 ` npostavs
0 siblings, 1 reply; 27+ messages in thread
From: npostavs @ 2017-03-16 2:54 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Stefan Monnier, Boruch Baum
[-- Attachment #1: Type: text/plain, Size: 304 bytes --]
>> npostavs@users.sourceforge.net writes:
>>
>> Okay, I think I found the real fix now:
Same issue with indent-region. The gains are not so dramatic for
typical lisp code that has normal size sexps, but C-x h C-M-\ on
subr.el's text runs twice as fast for me with the new lisp-indent-region
function.
[-- Attachment #2: patch --]
[-- Type: text/plain, Size: 1155 bytes --]
From 1a5464d36582b5efcea43aac37bf0b9ddd4c1ff1 Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Wed, 15 Mar 2017 21:59:13 -0400
Subject: [PATCH v2 2/4] Remove ignored argument from lisp-indent-line
* lisp/emacs-lisp/lisp-mode.el (lisp-indent-line): Remove WHOLE-EXP
argument, the behvior has long since been handled in
`indent-for-tab-command'.
---
lisp/emacs-lisp/lisp-mode.el | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index 3fefb69066..7eb8b986be 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -744,11 +744,9 @@ lisp-indent-function
:type 'function
:group 'lisp)
-(defun lisp-indent-line (&optional _whole-exp)
- "Indent current line as Lisp code.
-With argument, indent any additional lines of the same expression
-rigidly along with this one."
- (interactive "P")
+(defun lisp-indent-line ()
+ "Indent current line as Lisp code."
+ (interactive)
(let ((indent (calculate-lisp-indent)) shift-amt
(pos (- (point-max) (point)))
(beg (progn (beginning-of-line) (point))))
--
2.11.1
[-- Attachment #3: patch --]
[-- Type: text/plain, Size: 3088 bytes --]
From 9cf8f32910c0c899f108c3907d099a18fff2cec4 Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Wed, 15 Mar 2017 22:27:27 -0400
Subject: [PATCH v2 3/4] Add new `lisp-indent-region' that doesn't reparse the
code.
* lisp/emacs-lisp/lisp-mode.el (lisp-indent-region): New function,
like `indent-region-line-by-line' but additionally keep a running
parse state to avoid reparsing the code repeatedly.
(lisp-indent-line): Take optional PARSE-STATE argument, pass it to
`calculate-lisp-indent'.
(lisp-mode-variables): Set `indent-region-function' to
`lisp-indent-region'.
---
lisp/emacs-lisp/lisp-mode.el | 33 +++++++++++++++++++++++++++++++--
1 file changed, 31 insertions(+), 2 deletions(-)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index 7eb8b986be..7577267903 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -586,6 +586,7 @@ lisp-mode-variables
;; I believe that newcomment's auto-fill code properly deals with it -stef
;;(set (make-local-variable 'adaptive-fill-mode) nil)
(setq-local indent-line-function 'lisp-indent-line)
+ (setq-local indent-region-function 'lisp-indent-region)
(setq-local outline-regexp ";;;\\(;* [^ \t\n]\\|###autoload\\)\\|(")
(setq-local outline-level 'lisp-outline-level)
(setq-local add-log-current-defun-function #'lisp-current-defun-name)
@@ -744,10 +745,38 @@ lisp-indent-function
:type 'function
:group 'lisp)
-(defun lisp-indent-line ()
+(defun lisp-indent-region (start end)
+ "Indent region as Lisp code, efficiently."
+ (save-excursion
+ (setq end (copy-marker end))
+ (goto-char start)
+ ;; The default `indent-region-line-by-line' doesn't hold a running
+ ;; parse state, which forces each indent call to reparse from the
+ ;; beginning. That has O(n^2) complexity.
+ (let* ((parse-state (syntax-ppss start))
+ (last-syntax-point start)
+ (pr (unless (minibufferp)
+ (make-progress-reporter "Indenting region..." (point) end))))
+ (while (< (point) end)
+ (unless (and (bolp) (eolp))
+ (lisp-indent-line parse-state))
+ (forward-line 1)
+ (let ((last-sexp (nth 2 parse-state)))
+ (setq parse-state (parse-partial-sexp last-syntax-point (point)
+ nil nil parse-state))
+ ;; It's important to preserve last sexp location for
+ ;; `calculate-lisp-indent'.
+ (unless (nth 2 parse-state)
+ (setf (nth 2 parse-state) last-sexp))
+ (setq last-syntax-point (point)))
+ (and pr (progress-reporter-update pr (point))))
+ (and pr (progress-reporter-done pr))
+ (move-marker end nil))))
+
+(defun lisp-indent-line (&optional parse-state)
"Indent current line as Lisp code."
(interactive)
- (let ((indent (calculate-lisp-indent)) shift-amt
+ (let ((indent (calculate-lisp-indent parse-state)) shift-amt
(pos (- (point-max) (point)))
(beg (progn (beginning-of-line) (point))))
(skip-chars-forward " \t")
--
2.11.1
[-- Attachment #4: patch --]
[-- Type: text/plain, Size: 1969 bytes --]
From 4dbe8fd84064b1b3fed4188f0a2a84de5550cf11 Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Wed, 15 Mar 2017 22:35:47 -0400
Subject: [PATCH v2 4/4] * lisp/emacs-lisp/lisp-mode.el (indent-sexp): Clean up
marker.
---
lisp/emacs-lisp/lisp-mode.el | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index 7577267903..4fa9eb97de 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -1112,12 +1112,13 @@ indent-sexp
(next-depth init-depth)
(last-depth init-depth)
(last-syntax-point (point)))
- (unless endpos
- ;; Get error now if we don't have a complete sexp after point.
- (save-excursion (forward-sexp 1)
- ;; We need a marker because we modify the buffer
- ;; text preceding endpos.
- (setq endpos (point-marker))))
+ ;; We need a marker because we modify the buffer
+ ;; text preceding endpos.
+ (setq endpos (copy-marker
+ (if endpos endpos
+ ;; Get error now if we don't have a complete sexp
+ ;; after point.
+ (save-excursion (forward-sexp 1) (point)))))
(save-excursion
(while (< (point) endpos)
;; Parse this line so we can learn the state to indent the
@@ -1177,7 +1178,8 @@ indent-sexp
;; `calculate-lisp-indent' only returns nil
;; when we're in a string, but this won't
;; happen because we skip strings above.
- (t (error "This shouldn't happen!"))))))))))))
+ (t (error "This shouldn't happen!"))))))))))
+ (move-marker endpos nil)))
(defun indent-pp-sexp (&optional arg)
"Indent each line of the list starting just after point, or prettyprint it.
--
2.11.1
^ permalink raw reply related [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-03-16 2:54 ` npostavs
@ 2017-04-18 3:53 ` npostavs
2017-04-22 18:25 ` npostavs
0 siblings, 1 reply; 27+ messages in thread
From: npostavs @ 2017-04-18 3:53 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Stefan Monnier, Boruch Baum
[-- Attachment #1: Type: text/plain, Size: 974 bytes --]
npostavs@users.sourceforge.net writes:
>>> npostavs@users.sourceforge.net writes:
>>>
>>> Okay, I think I found the real fix now:
>
> Same issue with indent-region. The gains are not so dramatic for
> typical lisp code that has normal size sexps, but C-x h C-M-\ on
> subr.el's text runs twice as fast for me with the new lisp-indent-region
> function.
Had some test failures due to some corner cases, should all be fixed
now. I think this is the final patch set. This also fixes
lisp-indent-region and lisp-indent-line to not indent string literal
contents.
I intend to close this bug as fixed after merging these, as this does
fix the performance bug. I will probably pursue the alternate pretty
printer separately; it doesn't help performance (after the indent-sexp
performance is fixed), but gives prettier results in some cases. The
threading idea may also be worth looking at, but can also be considered
separately, as this bug report is already long enough.
[-- Attachment #2: patch --]
[-- Type: text/plain, Size: 8386 bytes --]
From 9ae3809189d08c15d64363c963eeeae62efae242 Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Sun, 12 Mar 2017 23:59:19 -0400
Subject: [PATCH v3 1/4] Don't reparse the sexp in indent-sexp (Bug#25122)
* lisp/emacs-lisp/lisp-mode.el (calculate-lisp-indent): Let
PARSE-START be a parse state that can be reused.
(indent-sexp): Pass the running parse state to calculate-lisp-indent
instead of the sexp beginning position. Saving the
CONTAINING-SEXP-START returned by `calculate-lisp-indent' is no longer
needed. Don't bother stopping if we don't descend below init-depth,
since we now alway scan the whole buffer (via syntax-ppss) anyway.
* test/lisp/emacs-lisp/lisp-mode-tests.el (indent-sexp): Add blank
line to test case.
---
lisp/emacs-lisp/lisp-mode.el | 76 +++++++++++++++++----------------
test/lisp/emacs-lisp/lisp-mode-tests.el | 5 ++-
2 files changed, 43 insertions(+), 38 deletions(-)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index 2e6e13f1dd..607a4c3d11 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -785,6 +785,10 @@ calculate-lisp-indent
If the value is nil, that means don't change the indentation
because the line starts inside a string.
+PARSE-START may be a buffer position to start parsing from, or a
+parse state as returned by calling `parse-partial-sexp' up to the
+beginning of the current line.
+
The value can also be a list of the form (COLUMN CONTAINING-SEXP-START).
This means that following lines at the same level of indentation
should not necessarily be indented the same as this line.
@@ -798,12 +802,14 @@ calculate-lisp-indent
(desired-indent nil)
(retry t)
calculate-lisp-indent-last-sexp containing-sexp)
- (if parse-start
- (goto-char parse-start)
- (beginning-of-defun))
- ;; Find outermost containing sexp
- (while (< (point) indent-point)
- (setq state (parse-partial-sexp (point) indent-point 0)))
+ (cond ((or (markerp parse-start) (integerp parse-start))
+ (goto-char parse-start))
+ ((null parse-start) (beginning-of-defun))
+ (t (setq state parse-start)))
+ (unless state
+ ;; Find outermost containing sexp
+ (while (< (point) indent-point)
+ (setq state (parse-partial-sexp (point) indent-point 0))))
;; Find innermost containing sexp
(while (and retry
state
@@ -1074,11 +1080,6 @@ indent-sexp
ENDPOS is encountered."
(interactive)
(let* ((indent-stack (list nil))
- ;; If ENDPOS is non-nil, use beginning of defun as STARTING-POINT.
- ;; If ENDPOS is nil, it is safe not to scan before point
- ;; since every line we indent is more deeply nested than point is.
- (starting-point (save-excursion (if endpos (beginning-of-defun))
- (point)))
;; Use `syntax-ppss' to get initial state so we don't get
;; confused by starting inside a string. We don't use
;; `syntax-ppss' in the loop, because this is measurably
@@ -1087,8 +1088,7 @@ indent-sexp
(init-depth (car state))
(next-depth init-depth)
(last-depth init-depth)
- (last-syntax-point (point))
- (real-endpos endpos))
+ (last-syntax-point (point)))
(unless endpos
;; Get error now if we don't have a complete sexp after point.
(save-excursion (forward-sexp 1)
@@ -1098,16 +1098,21 @@ indent-sexp
(save-excursion
(while (< (point) endpos)
;; Parse this line so we can learn the state to indent the
- ;; next line.
- (while (progn
- (setq state (parse-partial-sexp
- last-syntax-point (progn (end-of-line) (point))
- nil nil state))
- ;; Skip over newlines within strings.
- (nth 3 state))
- (setq state (parse-partial-sexp (point) (point-max)
- nil nil state 'syntax-table))
- (setq last-syntax-point (point)))
+ ;; next line. Preserve element 2 of the state (last sexp) for
+ ;; `calculate-lisp-indent'.
+ (let ((last-sexp (nth 2 state)))
+ (while (progn
+ (setq state (parse-partial-sexp
+ last-syntax-point (progn (end-of-line) (point))
+ nil nil state))
+ (setq last-sexp (or (nth 2 state) last-sexp))
+ ;; Skip over newlines within strings.
+ (nth 3 state))
+ (setq state (parse-partial-sexp (point) (point-max)
+ nil nil state 'syntax-table))
+ (setq last-sexp (or (nth 2 state) last-sexp))
+ (setq last-syntax-point (point)))
+ (setf (nth 2 state) last-sexp))
(setq next-depth (car state))
;; If the line contains a comment indent it now with
;; `indent-for-comment'.
@@ -1120,9 +1125,9 @@ indent-sexp
(make-list (- init-depth next-depth) nil))
last-depth (- last-depth next-depth)
next-depth init-depth))
+ ;; Now indent the next line according to what we learned from
+ ;; parsing the previous one.
(forward-line 1)
- (when (and (not real-endpos) (<= next-depth init-depth))
- (goto-char endpos))
(when (< (point) endpos)
(let ((depth-delta (- next-depth last-depth)))
(cond ((< depth-delta 0)
@@ -1131,28 +1136,25 @@ indent-sexp
(setq indent-stack (nconc (make-list depth-delta nil)
indent-stack))))
(setq last-depth next-depth))
- ;; Now indent the next line according
- ;; to what we learned from parsing the previous one.
- (skip-chars-forward " \t")
;; But not if the line is blank, or just a comment (we
;; already called `indent-for-comment' above).
+ (skip-chars-forward " \t")
(unless (or (eolp) (eq (char-syntax (char-after)) ?<))
- (let ((this-indent (car indent-stack)))
- (when (listp this-indent)
- (let ((val (calculate-lisp-indent
- (or (car this-indent) starting-point))))
- (setq
- this-indent
+ (indent-line-to
+ (or (car indent-stack)
+ ;; The state here is actually to the end of the
+ ;; previous line, but that's fine for our purposes.
+ ;; And parsing over the newline would only destroy
+ ;; element 2 (last sexp position).
+ (let ((val (calculate-lisp-indent state)))
(cond ((integerp val)
(setf (car indent-stack) val))
((consp val) ; (COLUMN CONTAINING-SEXP-START)
- (setf (car indent-stack) (cdr val))
(car val))
;; `calculate-lisp-indent' only returns nil
;; when we're in a string, but this won't
;; happen because we skip strings above.
- (t (error "This shouldn't happen!"))))))
- (indent-line-to this-indent))))))))
+ (t (error "This shouldn't happen!"))))))))))))
(defun indent-pp-sexp (&optional arg)
"Indent each line of the list starting just after point, or prettyprint it.
diff --git a/test/lisp/emacs-lisp/lisp-mode-tests.el b/test/lisp/emacs-lisp/lisp-mode-tests.el
index 8e3f2e185c..27f0bb5ec1 100644
--- a/test/lisp/emacs-lisp/lisp-mode-tests.el
+++ b/test/lisp/emacs-lisp/lisp-mode-tests.el
@@ -31,6 +31,9 @@
1
2)
2)
+ (fun arg1
+
+ arg2)
(1
\"string
noindent\" (\"string2
@@ -58,7 +61,7 @@
(save-excursion
(let ((n 0))
(while (not (eobp))
- (unless (looking-at "noindent")
+ (unless (looking-at "noindent\\|^[[:blank:]]*$")
(insert (make-string n ?\s)))
(cl-incf n)
(forward-line))))
--
2.11.1
[-- Attachment #3: patch --]
[-- Type: text/plain, Size: 1969 bytes --]
From b14e7d088ef23074841cb1384503dd60c1e939e9 Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Wed, 15 Mar 2017 22:35:47 -0400
Subject: [PATCH v3 2/4] * lisp/emacs-lisp/lisp-mode.el (indent-sexp): Clean up
marker.
---
lisp/emacs-lisp/lisp-mode.el | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index 607a4c3d11..810fc95614 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -1089,12 +1089,13 @@ indent-sexp
(next-depth init-depth)
(last-depth init-depth)
(last-syntax-point (point)))
- (unless endpos
- ;; Get error now if we don't have a complete sexp after point.
- (save-excursion (forward-sexp 1)
- ;; We need a marker because we modify the buffer
- ;; text preceding endpos.
- (setq endpos (point-marker))))
+ ;; We need a marker because we modify the buffer
+ ;; text preceding endpos.
+ (setq endpos (copy-marker
+ (if endpos endpos
+ ;; Get error now if we don't have a complete sexp
+ ;; after point.
+ (save-excursion (forward-sexp 1) (point)))))
(save-excursion
(while (< (point) endpos)
;; Parse this line so we can learn the state to indent the
@@ -1154,7 +1155,8 @@ indent-sexp
;; `calculate-lisp-indent' only returns nil
;; when we're in a string, but this won't
;; happen because we skip strings above.
- (t (error "This shouldn't happen!"))))))))))))
+ (t (error "This shouldn't happen!"))))))))))
+ (move-marker endpos nil)))
(defun indent-pp-sexp (&optional arg)
"Indent each line of the list starting just after point, or prettyprint it.
--
2.11.1
[-- Attachment #4: patch --]
[-- Type: text/plain, Size: 2016 bytes --]
From 6cbe2b3acbb22e75b373d39d716ab4dc1a0f276f Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Wed, 15 Mar 2017 21:59:13 -0400
Subject: [PATCH v3 3/4] Remove ignored argument from lisp-indent-line
* lisp/emacs-lisp/lisp-mode.el (lisp-indent-line): Remove WHOLE-EXP
argument, the behavior has long since been handled in
`indent-for-tab-command'. Also remove redundant `beg' and `shift-amt'
variables and use `indent-line-to'.
---
lisp/emacs-lisp/lisp-mode.el | 20 +++++++-------------
1 file changed, 7 insertions(+), 13 deletions(-)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index 810fc95614..89d5659f30 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -748,14 +748,12 @@ lisp-indent-function
:type 'function
:group 'lisp)
-(defun lisp-indent-line (&optional _whole-exp)
- "Indent current line as Lisp code.
-With argument, indent any additional lines of the same expression
-rigidly along with this one."
- (interactive "P")
- (let ((indent (calculate-lisp-indent)) shift-amt
- (pos (- (point-max) (point)))
- (beg (progn (beginning-of-line) (point))))
+(defun lisp-indent-line ()
+ "Indent current line as Lisp code."
+ (interactive)
+ (let ((indent (calculate-lisp-indent))
+ (pos (- (point-max) (point))))
+ (beginning-of-line)
(skip-chars-forward " \t")
(if (or (null indent) (looking-at "\\s<\\s<\\s<"))
;; Don't alter indentation of a ;;; comment line
@@ -767,11 +765,7 @@ lisp-indent-line
;; as comment lines, not as code.
(progn (indent-for-comment) (forward-char -1))
(if (listp indent) (setq indent (car indent)))
- (setq shift-amt (- indent (current-column)))
- (if (zerop shift-amt)
- nil
- (delete-region beg (point))
- (indent-to indent)))
+ (indent-line-to indent))
;; If initial point was within line's indentation,
;; position after the indentation. Else stay at same point in text.
(if (> (- (point-max) pos) (point))
--
2.11.1
[-- Attachment #5: patch --]
[-- Type: text/plain, Size: 4032 bytes --]
From d2f3101d88eb9601a52c06d1171c413fc23e3d8d Mon Sep 17 00:00:00 2001
From: Noam Postavsky <npostavs@gmail.com>
Date: Wed, 15 Mar 2017 22:27:27 -0400
Subject: [PATCH v3 4/4] Add new `lisp-indent-region' that doesn't reparse the
code.
Both `lisp-indent-region' and `lisp-indent-line' now use `syntax-ppss'
to get initial state, so they will no longer indent string literal
contents.
* lisp/emacs-lisp/lisp-mode.el (lisp-ppss): New function, like
`syntax-ppss', but with a more dependable item 2.
(lisp-indent-region): New function, like `indent-region-line-by-line'
but additionally keep a running parse state to avoid reparsing the
code repeatedly. Use `lisp-ppss' to get initial state.
(lisp-indent-line): Take optional PARSE-STATE argument, pass it to
`calculate-lisp-indent', use `lisp-ppss' if not given.
(lisp-mode-variables): Set `indent-region-function' to
`lisp-indent-region'.
---
lisp/emacs-lisp/lisp-mode.el | 48 ++++++++++++++++++++++++++++++++++++++++----
1 file changed, 44 insertions(+), 4 deletions(-)
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index 89d5659f30..54d916887c 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -594,6 +594,7 @@ lisp-mode-variables
;; I believe that newcomment's auto-fill code properly deals with it -stef
;;(set (make-local-variable 'adaptive-fill-mode) nil)
(setq-local indent-line-function 'lisp-indent-line)
+ (setq-local indent-region-function 'lisp-indent-region)
(setq-local outline-regexp ";;;\\(;* [^ \t\n]\\|###autoload\\)\\|(")
(setq-local outline-level 'lisp-outline-level)
(setq-local add-log-current-defun-function #'lisp-current-defun-name)
@@ -748,12 +749,51 @@ lisp-indent-function
:type 'function
:group 'lisp)
-(defun lisp-indent-line ()
+(defun lisp-ppss (&optional pos)
+ "Return Parse-Partial-Sexp State at POS, defaulting to point.
+Like to `syntax-ppss' but includes the character address of the
+last complete sexp in the innermost containing list at position
+2 (counting from 0). This is important for lisp indentation."
+ (unless pos (setq pos (point)))
+ (let ((pss (syntax-ppss pos)))
+ (if (nth 9 pss)
+ (parse-partial-sexp (car (last (nth 9 pss))) pos)
+ pss)))
+
+(defun lisp-indent-region (start end)
+ "Indent region as Lisp code, efficiently."
+ (save-excursion
+ (setq end (copy-marker end))
+ (goto-char start)
+ ;; The default `indent-region-line-by-line' doesn't hold a running
+ ;; parse state, which forces each indent call to reparse from the
+ ;; beginning. That has O(n^2) complexity.
+ (let* ((parse-state (lisp-ppss start))
+ (last-syntax-point start)
+ (pr (unless (minibufferp)
+ (make-progress-reporter "Indenting region..." (point) end))))
+ (while (< (point) end)
+ (unless (and (bolp) (eolp))
+ (lisp-indent-line parse-state))
+ (forward-line 1)
+ (let ((last-sexp (nth 2 parse-state)))
+ (setq parse-state (parse-partial-sexp last-syntax-point (point)
+ nil nil parse-state))
+ ;; It's important to preserve last sexp location for
+ ;; `calculate-lisp-indent'.
+ (unless (nth 2 parse-state)
+ (setf (nth 2 parse-state) last-sexp))
+ (setq last-syntax-point (point)))
+ (and pr (progress-reporter-update pr (point))))
+ (and pr (progress-reporter-done pr))
+ (move-marker end nil))))
+
+(defun lisp-indent-line (&optional parse-state)
"Indent current line as Lisp code."
(interactive)
- (let ((indent (calculate-lisp-indent))
- (pos (- (point-max) (point))))
- (beginning-of-line)
+ (let ((pos (- (point-max) (point)))
+ (indent (progn (beginning-of-line)
+ (calculate-lisp-indent (or parse-state (lisp-ppss))))))
(skip-chars-forward " \t")
(if (or (null indent) (looking-at "\\s<\\s<\\s<"))
;; Don't alter indentation of a ;;; comment line
--
2.11.1
^ permalink raw reply related [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-04-18 3:53 ` npostavs
@ 2017-04-22 18:25 ` npostavs
2017-04-26 3:57 ` Michael Heerdegen
0 siblings, 1 reply; 27+ messages in thread
From: npostavs @ 2017-04-22 18:25 UTC (permalink / raw)
To: Thierry Volpiatto; +Cc: 25122, Stefan Monnier, Boruch Baum
tags 25122 fixed
close 25122 26.1
quit
npostavs@users.sourceforge.net writes:
>
> I intend to close this bug as fixed after merging these, as this does
> fix the performance bug.
Pushed to master.
[1: 6fa9cc0593]: 2017-04-22 14:18:46 -0400
; Merge: improve indent-sexp and lisp-indent-region performance
http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=6fa9cc0593150a318f0e08e69ec10672d548a7c1
[2: 4713dd425b]: 2017-04-22 14:09:58 -0400
Add new `lisp-indent-region' that doesn't reparse the code.
http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=4713dd425beac5cb459704e67dcb8f6faf714375
[3: 2f6769f9cd]: 2017-04-22 14:09:58 -0400
Remove ignored argument from lisp-indent-line
http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=2f6769f9cdb799e880fdcc09057353a0a2349bfc
[4: 8bb5d7adaf]: 2017-04-22 14:09:57 -0400
* lisp/emacs-lisp/lisp-mode.el (indent-sexp): Clean up marker.
http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=8bb5d7adaf45264900385530c7f76175ba490a77
[5: 43c84577a3]: 2017-04-22 14:09:57 -0400
Don't reparse the sexp in indent-sexp (Bug#25122)
http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=43c84577a3055d5ddf1f5d1b999e6ecca6139f60
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-04-22 18:25 ` npostavs
@ 2017-04-26 3:57 ` Michael Heerdegen
2017-04-26 10:35 ` Michael Heerdegen
0 siblings, 1 reply; 27+ messages in thread
From: Michael Heerdegen @ 2017-04-26 3:57 UTC (permalink / raw)
To: npostavs; +Cc: 25122
Hi Noam,
> > I intend to close this bug as fixed after merging these, as this does
> > fix the performance bug.
I have a question: how much performance gain can I expect from your
changes?
My use case is the following: "el-search" has now an occur-like
operation mode. It copies all matches (i.e. s-expressions) into an *El
Occur* buffer. Matches can be tiny (like `1') or arbitrarily large.
Since most expressions don't start at column 0 in their source buffer
(unless they are top-level), I have to remove the amount of additional
indentation from all but the first line of a match. Well - not from
all, that's where it gets complicated - there are cases where this is
wrong, like in strings and some comments.
To have a sane solution, I just used `indent-region' to reindent the
expression in the occur buffer. This works great, but `indent-region'
was so slow that it took up to 50 percent of the whole running time of
the occur search.
With an naive alternative approach (just remove additional indentation
from lines where it makes sense (whereby finding out whether it "makes
sense" involves running syntax scanning)), I get an improvement of a
factor around five or ten relative to `indent-region' for reindenting.
Would you expect your improved `indent-region' is fast enough now so
that it makes sense that I switch back to it in my code? Is the running
time now something close to O(number of lines to indent)?
Thanks,
Michael.
^ permalink raw reply [flat|nested] 27+ messages in thread
* bug#25122: 24.5; function describe-variable hangs on large variables
2017-04-26 3:57 ` Michael Heerdegen
@ 2017-04-26 10:35 ` Michael Heerdegen
0 siblings, 0 replies; 27+ messages in thread
From: Michael Heerdegen @ 2017-04-26 10:35 UTC (permalink / raw)
To: npostavs; +Cc: 25122
Michael Heerdegen <michael_heerdegen@web.de> writes:
> Would you expect your improved `indent-region' is fast enough now so
> that it makes sense that I switch back to it in my code? Is the
> running time now something close to O(number of lines to indent)?
After some tests, I think it is fast enough for my scenario. And I see
from the comments in the code that you addressed the complexity problem.
Michael.
^ permalink raw reply [flat|nested] 27+ messages in thread
end of thread, other threads:[~2017-04-26 10:35 UTC | newest]
Thread overview: 27+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2016-12-06 2:21 bug#25122: 24.5; function describe-variable hangs on large variables Boruch Baum
2016-12-06 6:41 ` Thierry Volpiatto
2016-12-07 3:50 ` npostavs
2016-12-07 8:58 ` Thierry Volpiatto
2017-03-11 5:40 ` npostavs
2017-03-11 15:33 ` Stefan Monnier
2017-03-11 19:29 ` Thierry Volpiatto
2017-03-11 21:59 ` npostavs
2017-03-11 23:55 ` Drew Adams
2017-03-12 5:57 ` Thierry Volpiatto
2017-03-12 14:07 ` Stefan Monnier
2017-03-12 14:15 ` npostavs
2017-03-12 14:59 ` Drew Adams
2017-03-12 16:29 ` Stefan Monnier
2017-03-12 16:32 ` npostavs
2017-03-13 4:47 ` npostavs
2017-03-13 14:01 ` npostavs
2017-03-16 2:54 ` npostavs
2017-04-18 3:53 ` npostavs
2017-04-22 18:25 ` npostavs
2017-04-26 3:57 ` Michael Heerdegen
2017-04-26 10:35 ` Michael Heerdegen
2017-03-11 19:34 ` Thierry Volpiatto
2017-03-12 16:07 ` npostavs
2017-03-11 15:21 ` Stefan Monnier
2017-03-11 15:35 ` npostavs
2017-03-11 19:26 ` Thierry Volpiatto
Code repositories for project(s) associated with this public inbox
https://git.savannah.gnu.org/cgit/emacs.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).