Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance issue related indent/clojure.vim #88

Open
hovsater opened this issue Jan 9, 2019 · 2 comments
Open

Performance issue related indent/clojure.vim #88

hovsater opened this issue Jan 9, 2019 · 2 comments

Comments

@hovsater
Copy link

hovsater commented Jan 9, 2019

I've just got my hands into Clojure recently and noticed that Vim was lagging pretty hard. It seems like the culprit is indent/clojure.vim.

Given the following Clojure file, adding newlines at the end of the file will result in noticeable lag.

core.clojure

(ns introduction-to-clojure.core
  (:require [bakery.core :refer :all]))

(defn error [& args]
  (apply println args)
  :error)

(def baking  {:recipes {:cake {:ingredients {:egg   2
                                             :flour 2
                                             :sugar 1
                                             :milk  1}
                               :steps [[:add :all]
                                       [:mix]
                                       [:pour]
                                       [:bake 25]
                                       [:cool]]}
                        :cookies {:ingredients {:egg 1
                                                :flour 1
                                                :butter 1
                                                :sugar 1}
                                  :steps [[:add :all]
                                          [:mix]
                                          [:pour]
                                          [:bake 30]
                                          [:cool]]}
                        :brownies {:ingredients {:egg 2
                                                 :flour 2
                                                 :butter 2
                                                 :cocoa 2
                                                 :sugar 1
                                                 :milk 1}
                                   :steps [[:add :butter]
                                           [:add :cocoa]
                                           [:add :sugar]
                                           [:mix]
                                           [:add :egg]
                                           [:add :flour]
                                           [:add :milk]
                                           [:mix]
                                           [:pour]
                                           [:bake 35]
                                           [:cool]]}}
              :ingredients {:egg {:storage :fridge
                                  :usage :squeezed}
                            :milk {:storage :fridge
                                   :usage :scooped}
                            :flour {:storage :pantry
                                    :usage :scooped}
                            :butter {:storage :fridge
                                     :usage :simple}
                            :sugar {:storage :pantry
                                    :usage :scooped}
                            :cocoa {:storage :pantry
                                    :usage :scooped}}})

(def usage {:squeezed (fn [ingredient amount]
                        (dotimes [i amount]
                          (grab ingredient)
                          (squeeze)
                          (add-to-bowl)))
            :simple (fn [ingredient amount]
                      (dotimes [i amount]
                        (grab ingredient)
                        (add-to-bowl)))
            :scooped (fn [ingredient amount]
                       (grab :cup)
                       (dotimes [i amount]
                         (scoop ingredient)
                         (add-to-bowl))
                       (release))})

(defn usage-type [ingredient]
  (let [ingredients (get baking :ingredients)
        info (get ingredients ingredient)]
    (get info :usage)))

(defn add
  ([ingredient]
    (add ingredient 1))
  ([ingredient amount]
    (let [ingredient-type (usage-type ingredient)]
      (if (contains? usage ingredient-type)
        (let [f (get usage ingredient-type)]
          (f ingredient amount))
        (error "I do not know the ingredient" ingredient)))))

(def actions {:cool (fn [ingredients step]
                      (cool-pan))
              :mix  (fn [ingredients step]
                      (mix))
              :pour (fn [ingredients step]
                      (pour-into-pan))
              :bake (fn [ingredients step]
                      (bake-pan (second step)))
              :add  (fn [ingredients step]
                      (cond
                        (and (= 2 (count step))
                             (= :all (second step)))
                          (doseq [kv ingredients]
                            (add (first kv) (second kv)))
                        (and (= 2 (count step))
                             (contains? ingredients (second step)))
                        (add (second step) (get ingredients (second step)))
                        (= 3 (count step))
                        (add (second step) (get step 2))
                        :else
                        (error "I don't know how to add" (second step) (get step 2))))})

(defn perform [ingredients step]
  (let [f (get actions (first step) (fn [ingredients step]
                                      (println "I do not know how to" (first step))))]
    (f ingredients step)))

(defn bake-recipe [recipe]
  (last
    (for [step (get recipe :steps)]
      (perform (get recipe :ingredients) step))))

(defn load-up-amount [ingredient amount]
  (dotimes [i amount]
    (load-up ingredient)))

(defn unload-amount [ingredient amount]
  (dotimes [i amount]
    (unload ingredient)))

(defn fetch-ingredient
  ([ingredient]
    (fetch-ingredient ingredient 1))
  ([ingredient amount]
    (let [ingredients (get baking :ingredients)
          info (get ingredients ingredient)]
      (if (contains? ingredients ingredient)
        (do
          (go-to (get info :storage))
          (load-up-amount ingredient amount)
          (go-to :prep-area)
          (unload-amount ingredient amount))
        (error "I don't know the ingredient" ingredient)))))

(defn storage-location [ingredient]
  (let [ingredients (get baking :ingredients)
        info (get ingredients ingredient)]
    (get info :storage)))

(defn fetch-list [shopping]
  (let [by-location (group-by (fn [item-amount]
                                (storage-location (first item-amount)))
                              shopping)]
    (doseq [loc by-location]
      (go-to (first loc))
      (doseq [item-amount (second loc)]
        (load-up-amount (first item-amount) (second item-amount)))))

  (go-to :prep-area)
  (doseq [item-amount shopping]
    (unload-amount (first item-amount) (second item-amount))))

(defn add-ingredients [a b]
  (merge-with + a b))

(defn multiply-ingredients [n ingredients]
  (into {}
    (for [kv ingredients]
      [(first kv) (* n (second kv))])))

(defn order->ingredients [order]
  (let [recipes (get baking :recipes)
        items (get order :items)]
    (reduce add-ingredients {}
            (for [kv items]
              (let [recipe (get recipes (first kv))
                    ingredients (get recipe :ingredients)]
                (multiply-ingredients (second kv) ingredients))))))

(defn orders->ingredients [orders]
  (reduce add-ingredients {}
    (for [order orders]
      (order->ingredients order))))

(defn bake [item]
  (let [recipes (get baking :recipes)]
    (bake-recipe (get recipes item))))

(defn day-at-the-bakery []
  (let [orders (get-morning-orders-day3)
        ingredients (orders->ingredients orders)]
    (fetch-list ingredients)
    (doseq [order orders]
      (let [items (get order :items)
            racks (for [kv items
                        i (range (second kv))]
                    (bake (first kv)))
            receipt {:orderid (get order :orderid)
                     :address (get order :address)
                     :rackids racks}]
        (delivery receipt)))))

(defn -main []
  (bake-cake)
  (bake-cookies))

profile.log

FUNCTION  <SNR>8_match_pairs()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 87
Called 63 times
Total time:   4.081067
 Self time:   0.245977

count  total (s)   self (s)
                            		" Stop only on vector and map [ resp. {. Ignore the ones in strings and
                            		" comments.
   63              0.000099 		if a:stopat == 0
   63              0.000279 			let stopat = max([line(".") - g:clojure_maxlines, 0])
                            		else
                            			let stopat = a:stopat
   63              0.000036 		endif
                            
   63   4.080176   0.245086 		let pos = searchpairpos(a:open, '', a:close, 'bWn', "!s:is_paren()", stopat)
   63              0.000211 		return [pos[0], col(pos)]

FUNCTION  <SNR>8_clojure_indent_pos()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 206
Called 21 times
Total time:   4.092601
 Self time:   0.001783

count  total (s)   self (s)
                            		" Get rid of special case.
   21              0.000048 		if line(".") == 1
                            			return [0, 0]
   21              0.000018 		endif
                            
                            		" We have to apply some heuristics here to figure out, whether to use
                            		" normal lisp indenting or not.
   21   0.010045   0.000294 		let i = s:check_for_string()
   21              0.000020 		if i > -1
                            			return [0, i + !!g:clojure_align_multiline_strings]
   21              0.000014 		endif
                            
   21              0.000048 		call cursor(0, 1)
                            
                            		" Find the next enclosing [ or {. We can limit the second search
                            		" to the line, where the [ was found. If no [ was there this is
                            		" zero and we search for an enclosing {.
   21   3.064751   0.000319 		let paren = s:match_pairs('(', ')', 0)
   21   0.895804   0.000275 		let bracket = s:match_pairs('\[', '\]', paren[0])
   21   0.121386   0.000280 		let curly = s:match_pairs('{', '}', bracket[0])
                            
                            		" In case the curly brace is on a line later then the [ or - in
                            		" case they are on the same line - in a higher column, we take the
                            		" curly indent.
   21              0.000055 		if curly[0] > bracket[0] || curly[1] > bracket[1]
                            			if curly[0] > paren[0] || curly[1] > paren[1]
                            				return curly
                            			endif
   21              0.000014 		endif
                            
                            		" If the curly was not chosen, we take the bracket indent - if
                            		" there was one.
   21              0.000042 		if bracket[0] > paren[0] || bracket[1] > paren[1]
                            			return bracket
   21              0.000012 		endif
                            
                            		" There are neither { nor [ nor (, ie. we are at the toplevel.
   21              0.000035 		if paren == [0, 0]
   21              0.000018 			return paren
                            		endif
                            
                            		" Now we have to reimplement lispindent. This is surprisingly easy, as
                            		" soon as one has access to syntax items.
                            		"
                            		" - Check whether we are in a special position after a word in
                            		"   g:clojure_special_indent_words. These are special cases.
                            		" - Get the next keyword after the (.
                            		" - If its first character is also a (, we have another sexp and align
                            		"   one column to the right of the unmatched (.
                            		" - In case it is in lispwords, we indent the next line to the column of
                            		"   the ( + sw.
                            		" - If not, we check whether it is last word in the line. In that case
                            		"   we again use ( + sw for indent.
                            		" - In any other case we use the column of the end of the word + 2.
                            		call cursor(paren)
                            
                            		if s:is_method_special_case(paren)
                            			return [paren[0], paren[1] + shiftwidth() - 1]
                            		endif
                            
                            		if s:is_reader_conditional_special_case(paren)
                            			return paren
                            		endif
                            
                            		" In case we are at the last character, we use the paren position.
                            		if col("$") - 1 == paren[1]
                            			return paren
                            		endif
                            
                            		" In case after the paren is a whitespace, we search for the next word.
                            		call cursor(0, col('.') + 1)
                            		if s:current_char() == ' '
                            			call search('\v\S', 'W')
                            		endif
                            
                            		" If we moved to another line, there is no word after the (. We
                            		" use the ( position for indent.
                            		if line(".") > paren[0]
                            			return paren
                            		endif
                            
                            		" We still have to check, whether the keyword starts with a (, [ or {.
                            		" In that case we use the ( position for indent.
                            		let w = s:current_word()
                            		if s:bracket_type(w[0]) == 1
                            			return paren
                            		endif
                            
                            		" Test words without namespace qualifiers and leading reader macro
                            		" metacharacters.
                            		"
                            		" e.g. clojure.core/defn and #'defn should both indent like defn.
                            		let ww = s:strip_namespace_and_macro_chars(w)
                            
                            		if &lispwords =~# '\V\<' . ww . '\>'
                            			return [paren[0], paren[1] + shiftwidth() - 1]
                            		endif
                            
                            		if g:clojure_fuzzy_indent && !s:match_one(g:clojure_fuzzy_indent_blacklist, ww) && s:match_one(g:clojure_fuzzy_indent_patterns, ww)
                            			return [paren[0], paren[1] + shiftwidth() - 1]
                            		endif
                            
                            		call search('\v\_s', 'cW')
                            		call search('\v\S', 'W')
                            		if paren[0] < line(".")
                            			return [paren[0], paren[1] + (g:clojure_align_subforms ? 0 : shiftwidth() - 1)]
                            		endif
                            
                            		call search('\v\S', 'bW')
                            		return [line('.'), col('.') + 1]

FUNCTION  <SNR>8_clojure_check_for_string_worker()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 100
Called 21 times
Total time:   0.009117
 Self time:   0.000672

count  total (s)   self (s)
                            		" Check whether there is the last character of the previous line is
                            		" highlighted as a string. If so, we check whether it's a ". In this
                            		" case we have to check also the previous character. The " might be the
                            		" closing one. In case the we are still in the string, we search for the
                            		" opening ". If this is not found we take the indent of the line.
   21              0.000086 		let nb = prevnonblank(v:lnum - 1)
                            
   21              0.000019 		if nb == 0
                            			return -1
   21              0.000012 		endif
                            
   21              0.000047 		call cursor(nb, 0)
   21              0.000065 		call cursor(0, col("$") - 1)
   21   0.008778   0.000333 		if s:syn_id_name() !~? "string"
   21              0.000022 			return -1
                            		endif
                            
                            		" This will not work for a " in the first column...
                            		if s:current_char() == '"'
                            			call cursor(0, col("$") - 2)
                            			if s:syn_id_name() !~? "string"
                            				return -1
                            			endif
                            			if s:current_char() != '\\'
                            				return -1
                            			endif
                            			call cursor(0, col("$") - 1)
                            		endif
                            
                            		let p = searchpos('\(^\|[^\\]\)\zs"', 'bW')
                            
                            		if p != [0, 0]
                            			return p[1] - 1
                            		endif
                            
                            		return indent(".")

FUNCTION  <SNR>8_current_char()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 63
Called 6050 times
Total time:   0.022916
 Self time:   0.022916

count  total (s)   self (s)
 6050              0.021053 		return getline('.')[col('.')-1]

FUNCTION  <SNR>8_check_for_string()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 139
Called 21 times
Total time:   0.009751
 Self time:   0.000634

count  total (s)   self (s)
   21              0.000072 		let pos = getpos('.')
   21              0.000020 		try
   21   0.009506   0.000389 			let val = s:clojure_check_for_string_worker()
   21              0.000023 		finally
   21              0.000060 			call setpos('.', pos)
   21              0.000018 		endtry
   21              0.000032 		return val

FUNCTION  <SNR>8_is_paren()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 71
Called 6050 times
Total time:   3.835090
 Self time:   0.143374

count  total (s)   self (s)
 6050   3.833106   0.141390 		return s:current_char() =~# '\v[\(\)\[\]\{\}]' && !s:ignored_region()

FUNCTION  GetClojureIndent()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 321
Called 21 times
Total time:   4.093354
 Self time:   0.000753

count  total (s)   self (s)
   21              0.000089 		let lnum = line('.')
   21              0.000028 		let orig_lnum = lnum
   21              0.000043 		let orig_col = col('.')
   21   4.092957   0.000356 		let [opening_lnum, indent] = s:clojure_indent_pos()
                            
                            		" Account for multibyte characters
   21              0.000018 		if opening_lnum > 0
                            			let indent -= indent - virtcol([opening_lnum, indent])
   21              0.000011 		endif
                            
                            		" Return if there are no previous lines to inherit from
   21              0.000025 		if opening_lnum < 1 || opening_lnum >= lnum - 1
   21              0.000064 			call cursor(orig_lnum, orig_col)
   21              0.000019 			return indent
                            		endif
                            
                            		let bracket_count = 0
                            
                            		" Take the indent of the first previous non-white line that is
                            		" at the same sexp level. cf. src/misc1.c:get_lisp_indent()
                            		while 1
                            			let lnum = prevnonblank(lnum - 1)
                            			let col = 1
                            
                            			if lnum <= opening_lnum
                            				break
                            			endif
                            
                            			call cursor(lnum, col)
                            
                            			" Handle bracket counting edge case
                            			if s:is_paren()
                            				let bracket_count += s:bracket_type(s:current_char())
                            			endif
                            
                            			while 1
                            				if search('\v[(\[{}\])]', '', lnum) < 1
                            					break
                            				elseif !s:ignored_region()
                            					let bracket_count += s:bracket_type(s:current_char())
                            				endif
                            			endwhile
                            
                            			if bracket_count == 0
                            				" Check if this is part of a multiline string
                            				call cursor(lnum, 1)
                            				if s:syn_id_name() !~? '\vstring|regex'
                            					call cursor(orig_lnum, orig_col)
                            					return indent(lnum)
                            				endif
                            			endif
                            		endwhile
                            
                            		call cursor(orig_lnum, orig_col)
                            		return indent

FUNCTION  <SNR>8_ignored_region()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 59
Called 6050 times
Total time:   3.668800
 Self time:   0.127724

count  total (s)   self (s)
 6050   3.666821   0.125745 		return s:syn_id_name() =~? '\vstring|regex|comment|character'

FUNCTION  <SNR>8_syn_id_name()
    Defined: /usr/local/Cellar/vim/8.1.0650/share/vim/vim81/indent/clojure.vim line 55
Called 6071 times
Total time:   3.549521
 Self time:   3.549521

count  total (s)   self (s)
 6071              3.547512 		return synIDattr(synID(line("."), col("."), 0), "name")

FUNCTIONS SORTED ON TOTAL TIME
count  total (s)   self (s)  function
   21   4.093354   0.000753  GetClojureIndent()
   21   4.092601   0.001783  <SNR>8_clojure_indent_pos()
   63   4.081067   0.245977  <SNR>8_match_pairs()
 6050   3.835090   0.143374  <SNR>8_is_paren()
 6050   3.668800   0.127724  <SNR>8_ignored_region()
 6071   3.549521             <SNR>8_syn_id_name()
 6050   0.022916             <SNR>8_current_char()
   21   0.009751   0.000634  <SNR>8_check_for_string()
   21   0.009117   0.000672  <SNR>8_clojure_check_for_string_worker()

FUNCTIONS SORTED ON SELF TIME
count  total (s)   self (s)  function
 6071              3.549521  <SNR>8_syn_id_name()
   63   4.081067   0.245977  <SNR>8_match_pairs()
 6050   3.835090   0.143374  <SNR>8_is_paren()
 6050   3.668800   0.127724  <SNR>8_ignored_region()
 6050              0.022916  <SNR>8_current_char()
   21   4.092601   0.001783  <SNR>8_clojure_indent_pos()
   21   4.093354   0.000753  GetClojureIndent()
   21   0.009117   0.000672  <SNR>8_clojure_check_for_string_worker()
   21   0.009751   0.000634  <SNR>8_check_for_string()

vim --version

VIM - Vi IMproved 8.1 (2018 May 18, compiled Dec 28 2018 20:12:37)
macOS version
Included patches: 1-650
Compiled by Homebrew
Huge version without GUI.  Features included (+) or not (-):
+acl               +extra_search      +mouse_netterm     +tag_old_static
+arabic            +farsi             +mouse_sgr         -tag_any_white
+autocmd           +file_in_path      -mouse_sysmouse    -tcl
+autochdir         +find_in_path      +mouse_urxvt       +termguicolors
-autoservername    +float             +mouse_xterm       +terminal
-balloon_eval      +folding           +multi_byte        +terminfo
+balloon_eval_term -footer            +multi_lang        +termresponse
-browse            +fork()            -mzscheme          +textobjects
++builtin_terms    +gettext           +netbeans_intg     +textprop
+byte_offset       -hangul_input      +num64             +timers
+channel           +iconv             +packages          +title
+cindent           +insert_expand     +path_extra        -toolbar
-clientserver      +job               +perl              +user_commands
+clipboard         +jumplist          +persistent_undo   +vartabs
+cmdline_compl     +keymap            +postscript        +vertsplit
+cmdline_hist      +lambda            +printer           +virtualedit
+cmdline_info      +langmap           +profile           +visual
+comments          +libcall           -python            +visualextra
+conceal           +linebreak         +python3           +viminfo
+cryptv            +lispindent        +quickfix          +vreplace
+cscope            +listcmds          +reltime           +wildignore
+cursorbind        +localmap          +rightleft         +wildmenu
+cursorshape       +lua               +ruby              +windows
+dialog_con        +menu              +scrollbind        +writebackup
+diff              +mksession         +signs             -X11
+digraphs          +modify_fname      +smartindent       -xfontset
-dnd               +mouse             +startuptime       -xim
-ebcdic            -mouseshape        +statusline        -xpm
+emacs_tags        +mouse_dec         -sun_workshop      -xsmp
+eval              -mouse_gpm         +syntax            -xterm_clipboard
+ex_extra          -mouse_jsbterm     +tag_binary        -xterm_save
   system vimrc file: "$VIM/vimrc"
     user vimrc file: "$HOME/.vimrc"
 2nd user vimrc file: "~/.vim/vimrc"
      user exrc file: "$HOME/.exrc"
       defaults file: "$VIMRUNTIME/defaults.vim"
  fall-back for $VIM: "/usr/local/share/vim"
Compilation: clang -c -I. -Iproto -DHAVE_CONFIG_H   -DMACOS_X -DMACOS_X_DARWIN  -g -O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1
Linking: clang   -L. -fstack-protector-strong -L/usr/local/lib -L/usr/local/opt/libyaml/lib -L/usr/local/opt/openssl/lib -L/usr/local/opt/readline/lib  -L/usr/local/lib -o vim        -lncurses -liconv -lintl -framework AppKit  -L/usr/local/opt/lua/lib -llua5.3 -mmacosx-version-min=10.14 -fstack-protector-strong -L/usr/local/lib  -L/usr/local/Cellar/perl/5.28.1/lib/perl5/5.28.1/darwin-thread-multi-2level/CORE -lperl -lm -lutil -lc  -L/usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/python3.7/config-3.7m-darwin -lpython3.7m -framework CoreFoundation  -lruby.2.6

MacOS

MacOS Mojave
Version 10.14.2 (18C54)
@tvirolai
Copy link

tvirolai commented May 8, 2019

Setting the let g:clojure_maxlines parameter to a lower value (200 currently) fixed this for me.

@benknoble
Copy link

@tvirolai are you sure 200? The default was 100... I was considering using 20.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants