├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .luacheckrc
├── LICENSE
├── README.md
├── autoload
├── startuptime.vim
└── startuptime9.vim
├── doc
├── startuptime.txt
└── tags
├── lua
└── startuptime.lua
├── plugin
└── startuptime.vim
└── tests
├── run.py
├── test_consistency.vim
└── test_startuptime.vim
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 | # When the 'permissions' key is specified, unspecified permission scopes (e.g.,
3 | # actions, checks, etc.) are set to no access (none).
4 | permissions:
5 | contents: read
6 | on:
7 | push:
8 | branches: [master]
9 | pull_request:
10 | branches: [master]
11 | schedule:
12 | # Run daily (* is a special character in YAML, so quote the string)
13 | - cron: '0 0 * * *'
14 | workflow_dispatch:
15 | inputs:
16 | # When git-ref is empty, HEAD will be checked out.
17 | git-ref:
18 | description: Optional git ref (branch, tag, or full SHA)
19 | required: false
20 |
21 | jobs:
22 | build:
23 | runs-on: ubuntu-latest
24 |
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v4
28 | with:
29 | # When the ref is empty, HEAD will be checked out.
30 | ref: ${{ github.event.inputs.git-ref }}
31 |
32 | - name: Dependencies
33 | run: |
34 | sudo apt-get update
35 | sudo apt-get install lua-check neovim vim
36 |
37 | - name: Check Help Tags
38 | run: |
39 | # Check if the help tags file should be updated
40 | vim -c 'helptags doc/' -c quit
41 | test -z "$(git status --porcelain doc/)"
42 |
43 | - name: Luacheck
44 | run: luacheck .
45 |
46 | - name: Tests
47 | run: |
48 | mkdir -p ~/.local/share/nvim/site/pack/plugins/start/
49 | ln -s "$PWD" ~/.local/share/nvim/site/pack/plugins/start/
50 | mkdir -p ~/.vim/pack/plugins/start/
51 | ln -s "$PWD" ~/.vim/pack/plugins/start/
52 | tests/run.py
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.swo
3 | *.swp
4 | /venv/
5 |
--------------------------------------------------------------------------------
/.luacheckrc:
--------------------------------------------------------------------------------
1 | read_globals = {
2 | vim = {
3 | other_fields = true,
4 | fields = {
5 | g = {
6 | read_only = false,
7 | other_fields = true
8 | }
9 | }
10 | }
11 | }
12 | include_files = {'lua/', '*.lua'}
13 | std = 'luajit'
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Daniel Steinberg
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![build][badge_thumbnail]][badge_link]
2 |
3 | # vim-startuptime
4 |
5 | `vim-startuptime` is a plugin for viewing `vim` and `nvim` startup event timing
6 | information. The data is automatically obtained by launching `(n)vim` with the
7 | `--startuptime` argument. See `:help startuptime-configuration` for details on
8 | customization options.
9 |
10 |
11 |
12 | ## Requirements
13 |
14 | * `vim>=8.0.1453` or `nvim>=0.3.1`
15 | - The plugin may work on earlier versions, but has not been tested.
16 | - The plugin depends on compile-time features for `vim` (not applicable for
17 | `nvim`).
18 | * `+startuptime` is required.
19 | * `+timers` is recommended, to capture *all* startup events.
20 | * `+terminal` is required.
21 |
22 | ## Installation
23 |
24 | A package manager can be used to install `vim-startuptime`.
25 | Examples
26 |
27 | * [Vim8 packages][vim8pack]:
28 | - `git clone https://github.com/dstein64/vim-startuptime ~/.vim/pack/plugins/start/vim-startuptime`
29 | * [Vundle][vundle]:
30 | - Add `Plugin 'dstein64/vim-startuptime'` to `~/.vimrc`
31 | - `:PluginInstall` or `$ vim +PluginInstall +qall`
32 | * [Pathogen][pathogen]:
33 | - `git clone --depth=1 https://github.com/dstein64/vim-startuptime ~/.vim/bundle/vim-startuptime`
34 | * [vim-plug][vimplug]:
35 | - Add `Plug 'dstein64/vim-startuptime'` to `~/.vimrc`
36 | - `:PlugInstall` or `$ vim +PlugInstall +qall`
37 | * [dein.vim][dein]:
38 | - Add `call dein#add('dstein64/vim-startuptime')` to `~/.vimrc`
39 | - `:call dein#install()`
40 | * [NeoBundle][neobundle]:
41 | - Add `NeoBundle 'dstein64/vim-startuptime'` to `~/.vimrc`
42 | - Re-open vim or execute `:source ~/.vimrc`
43 |
44 |
45 |
46 | ## Usage
47 |
48 | * Launch `vim-startuptime` with `:StartupTime`.
49 | * Press `K` on events to get additional information.
50 | * Press `gf` on sourcing events to load the corresponding file in a new split.
51 | * The key sequences above can be customized (`:help startuptime-configuration`).
52 | * Times are in milliseconds.
53 |
54 | ## Documentation
55 |
56 | Documentation can be accessed with either:
57 |
58 | ```vim
59 | :help vim-startuptime
60 | ```
61 |
62 | or:
63 |
64 | ```vim
65 | :StartupTime --help
66 | ```
67 |
68 | The underlying markup is in [startuptime.txt](doc/startuptime.txt).
69 |
70 | There is documentation on the following topics.
71 |
72 | | Topic | `:help` *{subject}* |
73 | |---------------------|-----------------------------------|
74 | | Arguments | `startuptime-arguments` |
75 | | Modifiers | `startuptime-modifiers` |
76 | | Vim Options | `startuptime-vim-options` |
77 | | Configuration | `startuptime-configuration` |
78 | | Color Customization | `startuptime-color-customization` |
79 |
80 | License
81 | -------
82 |
83 | The source code has an [MIT License](https://en.wikipedia.org/wiki/MIT_License).
84 |
85 | See [LICENSE](LICENSE).
86 |
87 | [badge_link]: https://github.com/dstein64/vim-startuptime/actions/workflows/build.yml
88 | [badge_thumbnail]: https://github.com/dstein64/vim-startuptime/actions/workflows/build.yml/badge.svg
89 | [dein]: https://github.com/Shougo/dein.vim
90 | [neobundle]: https://github.com/Shougo/neobundle.vim
91 | [pathogen]: https://github.com/tpope/vim-pathogen
92 | [vim8pack]: http://vimhelp.appspot.com/repeat.txt.html#packages
93 | [vimplug]: https://github.com/junegunn/vim-plug
94 | [vundle]: https://github.com/gmarik/vundle
95 |
--------------------------------------------------------------------------------
/autoload/startuptime.vim:
--------------------------------------------------------------------------------
1 | " *************************************************
2 | " * Globals
3 | " *************************************************
4 |
5 | let s:event_types = {
6 | \ 'sourcing': 0,
7 | \ 'other': 1,
8 | \ }
9 |
10 | let s:nvim_lua = has('nvim-0.4')
11 | let s:vim9script = has('vim9script') && has('patch-8.2.4053')
12 |
13 | " 's:tfields' contains the time fields.
14 | let s:tfields = ['start', 'elapsed', 'self', 'self+sourced', 'finish']
15 | " Expose tfields through a function for use in the test script.
16 | function! s:TFields() abort
17 | return s:tfields
18 | endfunction
19 |
20 | let s:col_names = ['event', 'time', 'percent', 'plot']
21 |
22 | " The number of lines prior to the event data (e.g., startup line, header
23 | " line).
24 | let s:preamble_line_count = 2
25 |
26 | let s:startuptime_startup_key = 'startup:'
27 |
28 | let s:pathsep = (has('win32') || has('win64')) ? '\\' : '/'
29 |
30 | " *************************************************
31 | " * Utils
32 | " *************************************************
33 |
34 | function! s:Contains(list, element) abort
35 | return index(a:list, a:element) !=# -1
36 | endfunction
37 |
38 | function! s:Sum(numbers) abort
39 | let l:result = 0
40 | for l:number in a:numbers
41 | let l:result += l:number
42 | endfor
43 | return l:result
44 | endfunction
45 |
46 | " The built-in max() does not work with floats.
47 | function! s:Max(numbers) abort
48 | if len(a:numbers) ==# 0
49 | throw 'vim-startuptime: cannot take max of empty list'
50 | endif
51 | let l:result = a:numbers[0]
52 | for l:number in a:numbers
53 | if l:number ># l:result
54 | let l:result = l:number
55 | endif
56 | endfor
57 | return l:result
58 | endfunction
59 |
60 | " The built-in min() does not work with floats.
61 | function! s:Min(numbers) abort
62 | if len(a:numbers) ==# 0
63 | throw 'vim-startuptime: cannot take min of empty list'
64 | endif
65 | let l:result = a:numbers[0]
66 | for l:number in a:numbers
67 | if l:number <# l:result
68 | let l:result = l:number
69 | endif
70 | endfor
71 | return l:result
72 | endfunction
73 |
74 | function! s:Mean(numbers) abort
75 | if len(a:numbers) ==# 0
76 | throw 'vim-startuptime: cannot take mean of empty list'
77 | endif
78 | let l:result = 0.0
79 | for l:number in a:numbers
80 | let l:result += l:number
81 | endfor
82 | let l:result = l:result / len(a:numbers)
83 | return l:result
84 | endfunction
85 |
86 | " Calculate standard deviation using `ddof` delta degrees of freedom,
87 | " optionally taking the mean to avoid redundant computation.
88 | function! s:StandardDeviation(numbers, ddof, ...) abort
89 | let l:mean = a:0 ># 0 ? a:1 : s:Mean(a:numbers)
90 | let l:result = 0.0
91 | for l:number in a:numbers
92 | let l:diff = l:mean - l:number
93 | let l:result += l:diff * l:diff
94 | endfor
95 | let l:result = l:result / (len(a:numbers) - a:ddof)
96 | let l:result = sqrt(l:result)
97 | return l:result
98 | endfunction
99 |
100 | function! s:GetChar() abort
101 | try
102 | while 1
103 | let l:char = getchar()
104 | if v:mouse_win ># 0 | continue | endif
105 | if l:char ==# "\" | continue | endif
106 | break
107 | endwhile
108 | catch
109 | " E.g.,
110 | let l:char = char2nr("\")
111 | endtry
112 | if type(l:char) ==# v:t_number
113 | let l:char = nr2char(l:char)
114 | endif
115 | return l:char
116 | endfunction
117 |
118 | " Takes a list of lists. Each sublist is comprised of a highlight group name
119 | " and a corresponding string to echo.
120 | function! s:Echo(echo_list) abort
121 | redraw
122 | for [l:hlgroup, l:string] in a:echo_list
123 | execute 'echohl ' . l:hlgroup | echon l:string
124 | endfor
125 | echohl None
126 | endfunction
127 |
128 | function! s:Surround(inner, outer) abort
129 | return a:outer . a:inner . a:outer
130 | endfunction
131 |
132 | function! s:ClearCurrentBuffer() abort
133 | " Use silent to prevent --No lines in buffer-- message.
134 | silent %delete _
135 | endfunction
136 |
137 | function! s:SetBufLine(bufnr, line, text) abort
138 | let l:modifiable = getbufvar(a:bufnr, '&modifiable')
139 | call setbufvar(a:bufnr, '&modifiable', 1)
140 | " setbufline was added to Neovim in commit 9485061. Use nvim_buf_set_lines
141 | " to support older versions.
142 | if has('nvim')
143 | call nvim_buf_set_lines(a:bufnr, a:line - 1, a:line, 0, [a:text])
144 | else
145 | call setbufline(a:bufnr, a:line, a:text)
146 | endif
147 | call setbufvar(a:bufnr, '&modifiable', l:modifiable)
148 | endfunction
149 |
150 | " Return plus/minus character for supported environments, or '+/-' otherwise.
151 | function! s:PlusMinus() abort
152 | let l:plus_minus = '+/-'
153 | if has('multi_byte') && &encoding ==# 'utf-8'
154 | let l:plus_minus = nr2char(177)
155 | endif
156 | return l:plus_minus
157 | endfunction
158 |
159 | function! s:NumberToFloat(number) abort
160 | return a:number + 0.0
161 | endfunction
162 |
163 | function! s:True(...) abort
164 | return 1
165 | endfunction
166 |
167 | function! s:IsVisualMode(mode) abort
168 | return s:Contains(['v', 'V', "\"], a:mode)
169 | endfunction
170 |
171 | function! s:IsSelectMode(mode) abort
172 | return s:Contains(['s', 'S', "\"], a:mode)
173 | endfunction
174 |
175 | function! s:IsVisualSelectMode(mode) abort
176 | return s:Contains(['vs', 'Vs', "\s"], a:mode)
177 | endfunction
178 |
179 | " *************************************************
180 | " * Core
181 | " *************************************************
182 |
183 | function! s:SetFile() abort
184 | let l:isfname = &isfname
185 | " On Windows, to escape '[' with a backslash below, the character has to be
186 | " removed from 'isfname' (:help wildcard).
187 | set isfname-=[
188 | " Prepend backslash to the prefix to avoid the special wildcard meaning
189 | " (:help wildcard). Two backslashes are necessary on Windows, since Vim
190 | " removes backslashes before special characters (:help dos-backslash).
191 | " Issue #9.
192 | let l:prefix = has('win32') ? '\\[' : '\['
193 | let l:suffix = ']'
194 | let l:n = 0
195 | while 1
196 | try
197 | let l:text = 'startuptime'
198 | if l:n ># 0
199 | let l:text .= '.' . l:n
200 | endif
201 | execute 'silent file ' . l:prefix . l:text . l:suffix
202 | catch
203 | let l:n += 1
204 | continue
205 | endtry
206 | break
207 | endwhile
208 | let &isfname = l:isfname
209 | endfunction
210 |
211 | function! s:IsRequireEvent(event) abort
212 | return a:event =~# "require('.*')"
213 | endfunction
214 |
215 | " E.g., convert "require('vim.filetype')" to "vim.filetype"
216 | function! s:ExtractRequireArg(event) abort
217 | return a:event[9:-3]
218 | endfunction
219 |
220 | function! s:SimplifiedEvent(item) abort
221 | let l:event = a:item.event
222 | if a:item.type ==# s:event_types['sourcing']
223 | if s:IsRequireEvent(l:event)
224 | let l:event = s:ExtractRequireArg(l:event)
225 | else
226 | let l:event = substitute(l:event, '^sourcing ', '', '')
227 | let l:event = fnamemodify(l:event, ':t')
228 | endif
229 | endif
230 | return l:event
231 | endfunction
232 |
233 | function! s:ProfileCmd(file, options) abort
234 | " * If timer_start() is available, vim is quit with a timer. This retains
235 | " all events up to the last event, '--- VIM STARTED ---'.
236 | " * XXX: If timer_start() is not available, an autocmd is used. This retains
237 | " all events up to 'executing command arguments', which excludes:
238 | " - 'VimEnter autocommands'
239 | " - 'before starting main loop'
240 | " - 'first screen update'
241 | " - '--- VIM STARTED ---'
242 | " This approach works because the 'executing command arguments' event is
243 | " before the 'VimEnter autocommands' event.
244 | " * These are used in place of 'qall!' alone, which excludes the same events
245 | " as the autocmd approach, in addition to the 'executing command
246 | " arguments' event. 'qall!' alone can also seemingly trigger additional
247 | " autoload sourcing events (possibly from autocmds registered to Vim's
248 | " exit events (i.e., QuitPre, ExitPre, VimLeavePre, VimLeave).
249 | " * A -c command is used for quitting, as opposed to sending keys. The
250 | " latter approach would retain all events, but does not work for some
251 | " environments (e.g., gVim on Windows).
252 | let l:quit_cmd_timer = 'call timer_start(0, {-> execute(''qall!'')})'
253 | let l:quit_cmd_autocmd = 'autocmd VimEnter * qall!'
254 | let l:quit_cmd = printf(
255 | \ 'if exists(''*timer_start'') | %s | else | %s | endif',
256 | \ l:quit_cmd_timer,
257 | \ l:quit_cmd_autocmd)
258 | " Use an autocmd to disable the shada file (nvim) or viminfo file (vim). On
259 | " exit, its usage was observed to cause issues on Windows when multiple
260 | " :StartupTime commands are run in parallel (E576, E146 on nvim, E886 on
261 | " vim). This approach is used instead of '-i' so that shada/viminfo loading
262 | " is incorporated into startup profiling.
263 | let l:no_shada_viminfo_cmd = printf(
264 | \ 'if has(''nvim'') | %s | else | %s | endif',
265 | \ 'set shada= shadafile=NONE',
266 | \ 'set viminfo= viminfofile=NONE'
267 | \ )
268 | let l:no_shada_vimfile_autocmd =
269 | \ printf('autocmd VimEnter * %s', l:no_shada_viminfo_cmd)
270 | let l:command = [
271 | \ g:startuptime_exe_path,
272 | \ '--startuptime', a:file,
273 | \ '-c', l:no_shada_vimfile_autocmd,
274 | \ '-c', l:quit_cmd,
275 | \ ]
276 | call extend(l:command, a:options.exe_args)
277 | return l:command
278 | endfunction
279 |
280 | function! s:Profile(onfinish, onprogress, options, tries, file, items) abort
281 | if !a:onprogress(a:tries)
282 | return
283 | endif
284 | " Extract data when it's available (i.e., after the first call to Profile).
285 | if a:options.input_file isnot# v:null || a:tries <# a:options.tries
286 | while 1
287 | try
288 | let l:items = s:Extract(a:file)
289 | break
290 | catch /^Vim:Interrupt$/
291 | " Ignore interrupts. The loop will result in re-attempting to extract.
292 | " The plugin can be interrupted by closing the window.
293 | endtry
294 | endwhile
295 | call extend(a:items, l:items)
296 | call delete(a:file)
297 | endif
298 | if a:tries ==# 0 || a:options.input_file isnot# v:null
299 | if has('nvim-0.9')
300 | " We have to remove TUI sessions prior to handling --no-other-events,
301 | " since removal is based on 'opening buffers', an 'other' event.
302 | let l:filtered =
303 | \ luaeval('require("startuptime").remove_tui_sessions(_A)', a:items)
304 | if !empty(a:items)
305 | call remove(a:items, 0, len(a:items) - 1)
306 | endif
307 | call extend(a:items, l:filtered)
308 | endif
309 | if len(a:items) ==# 0
310 | throw 'vim-startuptime: unable to obtain startup times'
311 | endif
312 | if a:options.input_file is# v:null && a:options.tries !=# len(a:items)
313 | throw 'vim-startuptime: unexpected item count'
314 | endif
315 | call a:onfinish()
316 | return
317 | endif
318 | let l:command = s:ProfileCmd(a:file, a:options)
319 | " The 'tmp' dict is used so a local function can be created.
320 | let l:tmp = {}
321 | let l:args = [a:onfinish, a:onprogress, a:options, a:tries - 1, a:file, a:items]
322 | let l:env = {'args': args}
323 | if has('nvim')
324 | function l:tmp.exit(job, status, type) dict
325 | call function('s:Profile', self.args)()
326 | endfunction
327 | let l:jobstart_options = {
328 | \ 'pty': 1,
329 | \ 'on_exit': function(l:tmp.exit, l:env)
330 | \ }
331 | let l:env.jobnr = jobstart(l:command, l:jobstart_options)
332 | else
333 | function l:tmp.exit(job, status) dict
334 | execute self.bufnr . 'bdelete'
335 | call function('s:Profile', self.args)()
336 | endfunction
337 | let l:term_start_options = {
338 | \ 'exit_cb': function(l:tmp.exit, l:env),
339 | \ 'hidden': 1
340 | \ }
341 | " XXX: A new buffer is created each time this is run. Running many times
342 | " will result in large buffer numbers.
343 | let l:env.bufnr = term_start(l:command, l:term_start_options)
344 | endif
345 | endfunction
346 |
347 | function! s:ExtractLua(file) abort
348 | let l:args = [
349 | \ a:file,
350 | \ s:event_types,
351 | \ ]
352 | let l:result = luaeval('require("startuptime").extract(unpack(_A))', l:args)
353 | " Convert numbers to floats where applicable.
354 | for l:session in l:result
355 | for l:item in l:session
356 | for l:tfield in s:tfields
357 | if has_key(l:item, l:tfield)
358 | let l:item[l:tfield] = s:NumberToFloat(l:item[l:tfield])
359 | endif
360 | endfor
361 | endfor
362 | endfor
363 | return l:result
364 | endfunction
365 |
366 | function! s:ExtractVim9(file) abort
367 | return startuptime9#Extract(a:file, s:event_types)
368 | endfunction
369 |
370 | function! s:ExtractVimScript(file) abort
371 | let l:result = []
372 | let l:lines = readfile(a:file)
373 | for l:line in l:lines
374 | if strchars(l:line) ==# 0 || l:line[0] !~# '^\d$'
375 | continue
376 | endif
377 | if l:line =~# ': --- N\=VIM STARTING ---$'
378 | call add(l:result, [])
379 | let l:occurrences = {}
380 | endif
381 | let l:idx = stridx(l:line, ':')
382 | let l:times = split(l:line[:l:idx - 1], '\s\+')
383 | let l:event = l:line[l:idx + 2:]
384 | let l:type = s:event_types['other']
385 | if len(l:times) ==# 3
386 | let l:type = s:event_types['sourcing']
387 | endif
388 | let l:key = l:type . '-' . l:event
389 | if has_key(l:occurrences, l:key)
390 | let l:occurrences[l:key] += 1
391 | else
392 | let l:occurrences[l:key] = 1
393 | endif
394 | " 'finish' time is reported as 'clock' in --startuptime output.
395 | let l:item = {
396 | \ 'event': l:event,
397 | \ 'occurrence': l:occurrences[l:key],
398 | \ 'finish': str2float(l:times[0]),
399 | \ 'type': l:type
400 | \ }
401 | if l:type ==# s:event_types['sourcing']
402 | let l:item['self+sourced'] = str2float(l:times[1])
403 | let l:item.self = str2float(l:times[2])
404 | let l:item.start = l:item.finish - l:item['self+sourced']
405 | else
406 | let l:item.elapsed = str2float(l:times[1])
407 | let l:item.start = l:item.finish - l:item.elapsed
408 | endif
409 | call add(l:result[-1], l:item)
410 | endfor
411 | return l:result
412 | endfunction
413 |
414 | " Returns a nested list. The top-level list entries correspond to different
415 | " profiling sessions. The next level lists contain the parsed lines for each
416 | " profiling session. Each line is represented with a dict.
417 | function! s:Extract(file) abort
418 | " For improved speed, a Lua function is used for Neovim and a Vim9 function
419 | " for Vim, when available.
420 | if s:nvim_lua
421 | return s:ExtractLua(a:file)
422 | elseif s:vim9script
423 | return s:ExtractVim9(a:file)
424 | else
425 | return s:ExtractVimScript(a:file)
426 | endif
427 | endfunction
428 |
429 | " Returns the average startup time of the data returned by s:Extract(). The
430 | " result is independent of the options used (e.g., using --no-sourced doesn't
431 | " impact the result).
432 | function! s:Startup(items) abort
433 | let l:times = []
434 | for l:item in a:items
435 | let l:last = l:item[-1]
436 | let l:lookup = {
437 | \ s:event_types['sourcing']: 'self+sourced',
438 | \ s:event_types['other']: 'elapsed'
439 | \ }
440 | let l:key = l:lookup[l:last.type]
441 | call add(l:times, l:last.finish)
442 | endfor
443 | let l:mean = s:Mean(l:times)
444 | let l:std = s:StandardDeviation(l:times, 1, l:mean)
445 | " Don't use options.tries, since that will be null when --input-file is
446 | " used.
447 | let l:tries = len(l:times)
448 | let l:output = {'mean': l:mean, 'std': l:std, 'tries': l:tries}
449 | return l:output
450 | endfunction
451 |
452 | function! s:ConsolidateLua(items) abort
453 | let l:args = [a:items, s:tfields]
454 | let l:result = luaeval(
455 | \ 'require("startuptime").consolidate(unpack(_A))', l:args)
456 | " Convert numbers to floats where applicable.
457 | for l:item in l:result
458 | for l:metric in ['std', 'mean']
459 | for l:tfield in s:tfields
460 | if has_key(l:item, l:tfield)
461 | let l:item[l:tfield][l:metric] =
462 | \ s:NumberToFloat(l:item[l:tfield][l:metric])
463 | endif
464 | endfor
465 | endfor
466 | endfor
467 | return l:result
468 | endfunction
469 |
470 | function! s:ConsolidateVim9(items) abort
471 | return startuptime9#Consolidate(a:items, s:tfields)
472 | endfunction
473 |
474 | function! s:ConsolidateVimScript(items) abort
475 | let l:lookup = {}
476 | for l:try in a:items
477 | for l:item in l:try
478 | let l:key = l:item.type . '-' . l:item.occurrence . '-' . l:item.event
479 | if has_key(l:lookup, l:key)
480 | for l:tfield in s:tfields
481 | if has_key(l:item, l:tfield)
482 | call add(l:lookup[l:key][l:tfield], l:item[l:tfield])
483 | endif
484 | endfor
485 | let l:lookup[l:key].tries += 1
486 | else
487 | let l:lookup[l:key] = deepcopy(l:item)
488 | for l:tfield in s:tfields
489 | if has_key(l:lookup[l:key], l:tfield)
490 | " Put item in a list.
491 | let l:lookup[l:key][l:tfield] = [l:lookup[l:key][l:tfield]]
492 | endif
493 | endfor
494 | let l:lookup[l:key].tries = 1
495 | endif
496 | endfor
497 | endfor
498 | let l:result = values(l:lookup)
499 | for l:item in l:result
500 | for l:tfield in s:tfields
501 | if has_key(l:item, l:tfield)
502 | let l:mean = s:Mean(l:item[l:tfield])
503 | " Use 1 for ddof, for sample standard deviation.
504 | let l:std = s:StandardDeviation(l:item[l:tfield], 1, l:mean)
505 | let l:item[l:tfield] = {'mean': l:mean, 'std': l:std}
506 | endif
507 | endfor
508 | endfor
509 | " Sort on mean start time, event name, then occurrence.
510 | let l:Compare = {i1, i2 ->
511 | \ i1.start.mean !=# i2.start.mean
512 | \ ? (i1.start.mean <# i2.start.mean ? -1 : 1)
513 | \ : (i1.event !=# i2.event
514 | \ ? (i1.event <# i2.event ? -1 : 1)
515 | \ : (i1.occurrence !=# i2.occurrence
516 | \ ? (i1.occurrence <# i2.occurrence ? -1 : 1)
517 | \ : 0))}
518 | call sort(l:result, l:Compare)
519 | return l:result
520 | endfunction
521 |
522 | " Consolidates the data returned by s:Extract(), by averaging times across
523 | " tries. Adds a new field, 'tries', indicating how many tries were conducted
524 | " for each event (this can be smaller than specified by --tries).
525 | function! s:Consolidate(items) abort
526 | " For improved speed, a Lua function is used for Neovim and a Vim9 function
527 | " for Vim, when available.
528 | if s:nvim_lua
529 | return s:ConsolidateLua(a:items)
530 | elseif s:vim9script
531 | return s:ConsolidateVim9(a:items)
532 | else
533 | return s:ConsolidateVimScript(a:items)
534 | endif
535 | endfunction
536 |
537 | " Adds a time field to the data returned by s:Consolidate.
538 | function! s:Augment(items, options) abort
539 | let l:result = deepcopy(a:items)
540 | for l:item in l:result
541 | if l:item.type ==# s:event_types['sourcing']
542 | let l:key = a:options.sourced ? 'self+sourced' : 'self'
543 | elseif l:item.type ==# s:event_types['other']
544 | let l:key = 'elapsed'
545 | else
546 | throw 'vim-startuptime: unknown type'
547 | endif
548 | let l:item.time = l:item[l:key].mean
549 | endfor
550 | return l:result
551 | endfunction
552 |
553 | " Filter items based on whether --no-sourcing-events or --no-other-events were
554 | " used.
555 | function! s:Filter(items, options) abort
556 | let l:result = []
557 | let l:types = []
558 | if a:options.sourcing_events
559 | call add(l:types, s:event_types['sourcing'])
560 | endif
561 | if a:options.other_events
562 | call add(l:types, s:event_types['other'])
563 | endif
564 | for l:item in a:items
565 | if s:Contains(l:types, l:item.type)
566 | call add(l:result, l:item)
567 | endif
568 | endfor
569 | return l:result
570 | endfunction
571 |
572 | function! startuptime#ShowMoreInfo() abort
573 | let l:cmdheight = &cmdheight
574 | let l:laststatus = &laststatus
575 | if l:cmdheight ==# 0
576 | " Neovim supports cmdheight=0. When used, temporarily change to 1 to avoid
577 | " 'Press ENTER or type command to continue' after showing more info.
578 | set cmdheight=1
579 | endif
580 | " Make sure the last window has a status line, to serve as a divider between
581 | " the info message and the last window.
582 | if has('nvim') && l:laststatus ==# 3
583 | " Keep the existing value
584 | else
585 | set laststatus=2
586 | endif
587 | try
588 | let l:line = line('.')
589 | let l:info_lines = []
590 | if l:line <=# s:preamble_line_count
591 | call add(l:info_lines,
592 | \ '- You''ve queried for additional information.')
593 | let l:startup = printf('%.2f', b:startuptime_startup.mean)
594 | if b:startuptime_startup.tries ># 1
595 | let l:startup .= printf(
596 | \ ' %s %.2f', s:PlusMinus(), b:startuptime_startup.std)
597 | call extend(l:info_lines, [
598 | \ '- The startup time is ' . l:startup . ' milliseconds, an'
599 | \ . ' average',
600 | \ ' plus/minus sample standard deviation, across '
601 | \ . b:startuptime_startup.tries . ' tries.'
602 | \ ])
603 | else
604 | call add(l:info_lines,
605 | \ '- The startup time is ' . l:startup . ' milliseconds.')
606 | endif
607 | call add(l:info_lines,
608 | \ '- More specific information is available for event lines.')
609 | elseif !has_key(b:startuptime_item_map, l:line)
610 | throw 'vim-startuptime: error getting more info'
611 | else
612 | let l:item = b:startuptime_item_map[l:line]
613 | call add(l:info_lines, 'event: ' . l:item.event)
614 | let l:occurrences = b:startuptime_occurrences[l:item.event]
615 | if l:occurrences ># 1
616 | call add(
617 | \ l:info_lines,
618 | \ 'occurrence: ' . l:item.occurrence . ' of ' . l:occurrences)
619 | endif
620 | for l:tfield in s:tfields
621 | if has_key(l:item, l:tfield)
622 | let l:info = printf('%s: %.3f', l:tfield, l:item[l:tfield].mean)
623 | if l:item.tries ># 1
624 | let l:plus_minus = s:PlusMinus()
625 | let l:info .= printf(' %s %.3f', l:plus_minus, l:item[l:tfield].std)
626 | endif
627 | call add(l:info_lines, l:info)
628 | endif
629 | endfor
630 | if b:startuptime_startup.tries ># 1
631 | call add(l:info_lines, 'tries: ' . l:item.tries)
632 | endif
633 | call add(l:info_lines, '* times are in milliseconds')
634 | if l:item.tries ># 1
635 | call add(
636 | \ l:info_lines,
637 | \ '* times are averages plus/minus sample standard deviation')
638 | endif
639 | endif
640 | let l:echo_list = []
641 | call add(l:echo_list, ['Title', "vim-startuptime\n"])
642 | call add(l:echo_list, ['None', join(l:info_lines, "\n")])
643 | call add(l:echo_list, ['Question', "\n[press any key to continue]"])
644 | call s:Echo(l:echo_list)
645 | call s:GetChar()
646 | redraw | echo ''
647 | finally
648 | let &laststatus = l:laststatus
649 | let &cmdheight = l:cmdheight
650 | endtry
651 | endfunction
652 |
653 | function! startuptime#GotoFile() abort
654 | let l:line = line('.')
655 | let l:nofile = 'line'
656 | if l:line ==# 1
657 | let l:nofile = 'startup line'
658 | elseif l:line ==# 2
659 | let l:nofile = 'header'
660 | elseif has_key(b:startuptime_item_map, l:line)
661 | let l:item = b:startuptime_item_map[l:line]
662 | if l:item.type ==# s:event_types['sourcing']
663 | let l:file = ''
664 | if s:IsRequireEvent(l:item.event)
665 | " Attempt to deduce the file path.
666 | let l:arg = s:ExtractRequireArg(l:item.event)
667 | let l:part = substitute(l:arg, '\.', '/', 'g')
668 | for l:base in globpath(&runtimepath, '', 1, 1)
669 | let l:candidates = [
670 | \ l:base .. 'lua/' .. l:part .. '.lua',
671 | \ l:base .. 'lua/' .. l:part .. '/init.lua'
672 | \ ]
673 | for l:candidate in l:candidates
674 | if filereadable(l:candidate)
675 | let l:file = l:candidate
676 | break
677 | endif
678 | endfor
679 | if !empty(l:file) | break | endif
680 | endfor
681 | elseif l:item.event =~# '^sourcing '
682 | let l:file = substitute(l:item.event, '^sourcing ', '', '')
683 | endif
684 | if !empty(l:file)
685 | execute 'aboveleft split ' . l:file
686 | return
687 | endif
688 | endif
689 | let l:nofile = l:item.event
690 | let l:surround = ''
691 | if stridx(l:nofile, "'") ==# -1
692 | let l:surround = "'"
693 | elseif stridx(l:nofile, '"') ==# -1
694 | let l:surround = '"'
695 | endif
696 | if !empty(l:surround)
697 | let l:nofile = s:Surround(l:item.event, l:surround)
698 | endif
699 | endif
700 | let l:message = 'vim-startuptime: no file for ' . l:nofile
701 | call s:Echo([['WarningMsg', l:message]])
702 | endfunction
703 |
704 | function! s:RegisterMaps(items, options, startup) abort
705 | " 'b:startuptime_item_map' maps line numbers to corresponding items.
706 | let b:startuptime_item_map = {}
707 | " 'b:startuptime_occurrences' maps events to the number of times it
708 | " occurred.
709 | let b:startuptime_occurrences = {}
710 | let b:startuptime_startup = deepcopy(a:startup)
711 | let b:startuptime_options = deepcopy(a:options)
712 | for l:idx in range(len(a:items))
713 | let l:item = a:items[l:idx]
714 | " 'l:idx' is incremented to accommodate lines starting at 1 and the
715 | " preamble lines prior to the table's data.
716 | let b:startuptime_item_map[l:idx + 1 + s:preamble_line_count] = l:item
717 | if l:item.occurrence ># get(b:startuptime_occurrences, l:item.event, 0)
718 | let b:startuptime_occurrences[l:item.event] = l:item.occurrence
719 | endif
720 | endfor
721 | if g:startuptime_more_info_key_seq !=# ''
722 | execute 'nnoremap '
723 | \ . g:startuptime_more_info_key_seq
724 | \ . ' :call startuptime#ShowMoreInfo()'
725 | endif
726 | if g:startuptime_split_edit_key_seq !=# ''
727 | execute 'nnoremap '
728 | \ . g:startuptime_split_edit_key_seq
729 | \ . ' :call startuptime#GotoFile()'
730 | endif
731 | endfunction
732 |
733 | " Constrains the specified pattern to the specified lines and columns. 'lines'
734 | " and 'columns' are lists, comprised of either numbers, or lists representing
735 | " boundaries. '$' can be used as the second element in a boundary list to
736 | " represent the last line or column (this is not needed for the first element,
737 | " since 1 can be used for the first line). Use ['*'] for 'lines' or 'columns'
738 | " to represent all lines or columns. An empty list for 'lines' or 'columns'
739 | " will return a pattern that never matches.
740 | function! s:ConstrainPattern(pattern, lines, columns) abort
741 | " The 0th line will never match (when no lines specified)
742 | let l:line_parts = len(a:lines) ># 0 ? [] : ['\%0l']
743 | for l:line in a:lines
744 | if type(l:line) ==# v:t_list
745 | let l:gt = l:line[0] - 1
746 | let l:line_pattern = '\%>' . l:gt . 'l'
747 | if l:line[1] !=# '$'
748 | let l:lt = l:line[1] + 1
749 | let l:line_pattern = '\%(' . l:line_pattern . '\%<' . l:lt . 'l' . '\)'
750 | endif
751 | call add(l:line_parts, l:line_pattern)
752 | elseif type(l:line) ==# v:t_number
753 | call add(l:line_parts, '\%' . l:line . 'l')
754 | elseif type(l:line) ==# v:t_string && l:line ==# '*'
755 | continue
756 | else
757 | throw 'vim-startuptime: unsupported line type'
758 | endif
759 | endfor
760 | " The 0th column will never match (when no lines specified)
761 | let l:col_parts = len(a:columns) ># 0 ? [] : ['\%0v']
762 | for l:col in a:columns
763 | if type(l:col) ==# v:t_list
764 | let l:gt = l:col[0] - 1
765 | let l:col_pattern = '\%>' . l:gt . 'v'
766 | if l:col[1] !=# '$'
767 | let l:lt = l:col[1] + 1
768 | let l:col_pattern = '\%(' . l:col_pattern . '\%<' . l:lt . 'v' . '\)'
769 | endif
770 | call add(l:col_parts, l:col_pattern)
771 | elseif type(l:col) ==# v:t_number
772 | call add(l:col_parts, '\%'. l:col . 'v')
773 | elseif type(l:col) ==# v:t_string && l:col ==# '*'
774 | continue
775 | else
776 | throw 'vim-startuptime: unsupported column type'
777 | endif
778 | endfor
779 | let l:line_qualifier = join(l:line_parts, '\|')
780 | if len(l:line_parts) > 1
781 | let l:line_qualifier = '\%(' . l:line_qualifier . '\)'
782 | endif
783 | let l:col_qualifier = join(l:col_parts, '\|')
784 | if len(l:col_parts) > 1
785 | let l:col_qualifier = '\%(' . l:col_qualifier . '\)'
786 | endif
787 | let l:result = l:line_qualifier . l:col_qualifier . a:pattern
788 | return l:result
789 | endfunction
790 |
791 | function! s:CreatePlotLine(size, max, width) abort
792 | if g:startuptime_use_blocks
793 | let l:block_chars = {
794 | \ 1: nr2char(0x258F), 2: nr2char(0x258E),
795 | \ 3: nr2char(0x258D), 4: nr2char(0x258C),
796 | \ 5: nr2char(0x258B), 6: nr2char(0x258A),
797 | \ 7: nr2char(0x2589), 8: nr2char(0x2588)
798 | \ }
799 | if !g:startuptime_fine_blocks
800 | let l:block_chars[1] = ''
801 | let l:block_chars[2] = ''
802 | let l:block_chars[3] = l:block_chars[4]
803 | let l:block_chars[5] = l:block_chars[4]
804 | let l:block_chars[6] = l:block_chars[8]
805 | let l:block_chars[7] = l:block_chars[8]
806 | endif
807 | let l:width = 0.0 + a:width * a:size / a:max
808 | let l:plot = repeat(l:block_chars[8], float2nr(l:width))
809 | let l:remainder = s:Max([0.0, l:width - float2nr(l:width)])
810 | let l:eigths = s:Min([8, float2nr(round(l:remainder * 8.0))])
811 | if l:eigths ># 0
812 | let l:plot .= l:block_chars[l:eigths]
813 | endif
814 | else
815 | let l:plot = repeat('*', float2nr(round(a:width * a:size / a:max)))
816 | endif
817 | return l:plot
818 | endfunction
819 |
820 | function! s:ColBoundsLookup() abort
821 | let l:result = {}
822 | let l:position = 1
823 | for l:col_name in s:col_names
824 | let l:start = l:position
825 | let l:width = g:['startuptime_' . l:col_name . '_width']
826 | let l:end = l:start + l:width - 1
827 | let l:result[l:col_name] = [l:start, l:end]
828 | let l:position = l:end + 2
829 | endfor
830 | return l:result
831 | endfunction
832 |
833 | " Given a field (string), col_name, and alignment (1 for left, 0 for right),
834 | " return the column boundaries of the field.
835 | function! s:FieldBounds(field, col_name, left) abort
836 | let l:col_bounds_lookup = s:ColBoundsLookup()
837 | let l:col_bounds = l:col_bounds_lookup[a:col_name]
838 | if a:left
839 | let l:start = l:col_bounds[0]
840 | let l:field_bounds = [
841 | \ l:start,
842 | \ l:start + strchars(a:field) - 1
843 | \ ]
844 | else
845 | let l:end = l:col_bounds[1]
846 | let l:field_bounds = [
847 | \ l:end - strchars(a:field) + 1,
848 | \ l:end
849 | \ ]
850 | endif
851 | return l:field_bounds
852 | endfunction
853 |
854 | " Tabulate items and return each line's field boundaries in a
855 | " multi-dimensional array.
856 | function! s:Tabulate(items, startup) abort
857 | let l:output = []
858 | let l:startup_line = repeat(' ', g:startuptime_startup_indent)
859 | let l:startup_line .= s:startuptime_startup_key
860 | let l:startup_line .= printf(' %.1f', a:startup.mean)
861 | call setline(1, l:startup_line)
862 | let l:key_start = g:startuptime_startup_indent + 1
863 | let l:key_end = l:key_start + strdisplaywidth(s:startuptime_startup_key) - 1
864 | let l:value_start = l:key_end + 2
865 | let l:value_end = strdisplaywidth(l:startup_line)
866 | call add(l:output, [[l:key_start, l:key_end], [l:value_start, l:value_end]])
867 | let l:event = strcharpart('event', 0, g:startuptime_event_width)
868 | let l:line = printf('%-*S', g:startuptime_event_width, l:event)
869 | let l:time = strcharpart('time', 0, g:startuptime_time_width)
870 | let l:line .= printf(' %*S', g:startuptime_time_width, l:time)
871 | let l:percent = strcharpart('percent', 0, g:startuptime_percent_width)
872 | let l:line .= printf(' %*S', g:startuptime_percent_width, l:percent)
873 | let l:plot = strcharpart('plot', 0, g:startuptime_plot_width)
874 | let l:line .= ' ' . l:plot
875 | let l:field_bounds_list = [
876 | \ s:FieldBounds('event', 'event', 1),
877 | \ s:FieldBounds('time', 'time', 0),
878 | \ s:FieldBounds('percent', 'percent', 0),
879 | \ s:FieldBounds('plot', 'plot', 1),
880 | \ ]
881 | call add(l:output, l:field_bounds_list)
882 | call setline(2, l:line)
883 | if len(a:items) ==# 0 | return l:output | endif
884 | let l:max = s:Max(map(copy(a:items), 'v:val.time'))
885 | " WARN: Times won't necessarily sum to the reported startup time. This could
886 | " be due to some time being double counted. E.g., if --no-self is used,
887 | " self+sourced timings are used. These timings include time spent sourcing
888 | " other files, files which will have their own events and timings.
889 | for l:item in a:items
890 | let l:event = s:SimplifiedEvent(l:item)
891 | let l:event = strcharpart(l:event, 0, g:startuptime_event_width)
892 | let l:line = printf('%-*S', g:startuptime_event_width, l:event)
893 | let l:time = printf('%.2f', l:item.time)
894 | let l:time = strcharpart(l:time, 0, g:startuptime_time_width)
895 | let l:line .= printf(' %*S', g:startuptime_time_width, l:time)
896 | let l:percent = printf('%.2f', 100 * l:item.time / a:startup.mean)
897 | let l:percent = strcharpart(l:percent, 0, g:startuptime_percent_width)
898 | let l:line .= printf(' %*S', g:startuptime_percent_width, l:percent)
899 | let l:field_bounds_list = [
900 | \ s:FieldBounds(l:event, 'event', 1),
901 | \ s:FieldBounds(l:time, 'time', 0),
902 | \ s:FieldBounds(l:percent, 'percent', 0),
903 | \ ]
904 | let l:plot = s:CreatePlotLine(l:item.time, l:max, g:startuptime_plot_width)
905 | if strchars(l:plot) ># 0
906 | let l:line .= printf(' %s', l:plot)
907 | call add(l:field_bounds_list, s:FieldBounds(l:plot, 'plot', 1))
908 | endif
909 | call add(l:output, l:field_bounds_list)
910 | call setline(line('$') + 1, l:line)
911 | endfor
912 | normal! gg0
913 | return l:output
914 | endfunction
915 |
916 | " Converts a list of numbers into a list of numbers *and* ranges.
917 | " For example:
918 | " > echo s:Rangify([1, 3, 4, 5, 9, 10, 12, 14, 15])
919 | " [1, [3, 5], [9, 10], 12, [14, 15]]
920 | function! s:Rangify(list) abort
921 | if len(a:list) ==# 0 | return [] | endif
922 | let l:result = [[a:list[0], a:list[0]]]
923 | for l:x in a:list[1:]
924 | if l:x ==# l:result[-1][1] + 1
925 | let l:result[-1][1] = l:x
926 | else
927 | call add(l:result, [l:x, l:x])
928 | endif
929 | endfor
930 | for l:idx in range(len(l:result))
931 | if l:result[l:idx][0] ==# l:result[l:idx][1]
932 | let l:result[l:idx] = l:result[l:idx][0]
933 | endif
934 | endfor
935 | return l:result
936 | endfunction
937 |
938 | " Use syntax patterns to highlight text. Spaces within fields are not
939 | " highlighted.
940 | function! s:SyntaxColorize(event_types) abort
941 | let l:key_start = g:startuptime_startup_indent + 1
942 | let l:key_end = l:key_start + strdisplaywidth(s:startuptime_startup_key) - 1
943 | let l:startup_key_pattern = s:ConstrainPattern(
944 | \ '\S', [1], [[l:key_start, l:key_end]])
945 | execute 'syntax match StartupTimeStartupKey '
946 | \ . s:Surround(l:startup_key_pattern, "'")
947 | let l:startup_value_pattern = s:ConstrainPattern(
948 | \ '\S', [1], [[l:key_end + 2, '$']])
949 | execute 'syntax match StartupTimeStartupValue '
950 | \ . s:Surround(l:startup_value_pattern, "'")
951 | let l:header_pattern = s:ConstrainPattern('\S', [2], ['*'])
952 | execute 'syntax match StartupTimeHeader ' . s:Surround(l:header_pattern, "'")
953 | let l:line_lookup = {s:event_types['sourcing']: [], s:event_types['other']: []}
954 | for l:idx in range(len(a:event_types))
955 | let l:event_type = a:event_types[l:idx]
956 | " 'l:idx' is incremented to accommodate lines starting at 1 and the
957 | " preamble lines prior to the table's data.
958 | let l:line = l:idx + 1 + s:preamble_line_count
959 | call add(l:line_lookup[l:event_type], l:line)
960 | endfor
961 | let l:col_bounds_lookup = s:ColBoundsLookup()
962 | let l:sourcing_event_pattern = s:ConstrainPattern(
963 | \ '\S',
964 | \ s:Rangify(l:line_lookup[s:event_types['sourcing']]),
965 | \ [l:col_bounds_lookup.event])
966 | execute 'syntax match StartupTimeSourcingEvent '
967 | \ . s:Surround(l:sourcing_event_pattern, "'")
968 | let l:other_event_pattern = s:ConstrainPattern(
969 | \ '\S',
970 | \ s:Rangify(l:line_lookup[s:event_types['other']]),
971 | \ [l:col_bounds_lookup.event])
972 | execute 'syntax match StartupTimeOtherEvent '
973 | \ . s:Surround(l:other_event_pattern, "'")
974 | let l:first_event_line = s:preamble_line_count + 1
975 | let l:time_pattern = s:ConstrainPattern(
976 | \ '\S',
977 | \ [[l:first_event_line, '$']],
978 | \ [l:col_bounds_lookup.time])
979 | execute 'syntax match StartupTimeTime ' . s:Surround(l:time_pattern, "'")
980 | let l:percent_pattern = s:ConstrainPattern(
981 | \ '\S', [[l:first_event_line, '$']], [l:col_bounds_lookup.percent])
982 | execute 'syntax match StartupTimePercent ' . s:Surround(l:percent_pattern, "'")
983 | let l:plot_pattern = s:ConstrainPattern(
984 | \ '\S',
985 | \ [[l:first_event_line, '$']],
986 | \ [l:col_bounds_lookup.plot])
987 | execute 'syntax match StartupTimePlot ' . s:Surround(l:plot_pattern, "'")
988 | endfunction
989 |
990 | " Use Vim's text properties or Neovim's 'nvim_buf_add_highlight' to highlight
991 | " text based on location. Spaces within fields are highlighted.
992 | function! s:LocationColorize(event_types, field_bounds_table) abort
993 | for l:linenr in range(1, line('$'))
994 | let line = getline(l:linenr)
995 | let l:field_bounds_list = a:field_bounds_table[l:linenr - 1]
996 | for l:idx in range(len(l:field_bounds_list))
997 | let l:field_bounds = field_bounds_list[l:idx]
998 | " byteidx() returns the end byte of the corresponding character, which
999 | " requires adjustment for l:start (to include all bytes in the char),
1000 | " but is usable as-is for l:end.
1001 | let l:start = byteidx(l:line, l:field_bounds[0])
1002 | \ - strlen(nr2char(strgetchar(l:line, l:field_bounds[0] - 1)))
1003 | \ + 1
1004 | let l:end = byteidx(l:line, l:field_bounds[1])
1005 | let l:hlgroup = 'StartupTime'
1006 | if l:linenr ==# 1
1007 | let l:hlgroup .= ['StartupKey', 'StartupValue'][l:idx]
1008 | elseif l:linenr ==# 2
1009 | let l:hlgroup .= 'Header'
1010 | else
1011 | let l:col_name = s:col_names[l:idx]
1012 | if l:col_name ==# 'event'
1013 | " 'l:linenr' is decremented to accommodate lines starting at 1 and the
1014 | " preamble lines prior to the table's data.
1015 | let l:event_type = a:event_types[l:linenr - 1 - s:preamble_line_count]
1016 | if l:event_type ==# s:event_types['sourcing']
1017 | let l:hlgroup .= 'SourcingEvent'
1018 | elseif l:event_type ==# s:event_types['other']
1019 | let l:hlgroup .= 'OtherEvent'
1020 | else
1021 | throw 'vim-startuptime: unknown type'
1022 | endif
1023 | else
1024 | let l:hlgroup .= toupper(l:col_name[0]) . tolower(l:col_name[1:])
1025 | endif
1026 | endif
1027 | let l:bufnr = bufnr('%')
1028 | if has('textprop')
1029 | if empty(prop_type_get(l:hlgroup, {'bufnr': l:bufnr}))
1030 | let l:props = {
1031 | \ 'highlight': l:hlgroup,
1032 | \ 'bufnr': l:bufnr,
1033 | \ }
1034 | call prop_type_add(l:hlgroup, l:props)
1035 | endif
1036 | let l:props = {
1037 | \ 'type': l:hlgroup,
1038 | \ 'end_col': l:end + 1,
1039 | \ }
1040 | call prop_add(l:linenr, l:start, l:props)
1041 | elseif exists('*nvim_buf_add_highlight')
1042 | call nvim_buf_add_highlight(
1043 | \ l:bufnr,
1044 | \ -1,
1045 | \ l:hlgroup,
1046 | \ l:linenr - 1,
1047 | \ l:start - 1,
1048 | \ l:end)
1049 | else
1050 | throw 'vim-startuptime: unable to highlight'
1051 | endif
1052 | endfor
1053 | endfor
1054 | endfunction
1055 |
1056 | function! s:Colorize(event_types, field_bounds_table) abort
1057 | " Use text properties (introduced in Vim 8.2) or Neovim's similar
1058 | " functionality (nvim_buf_add_highlight), where applicable. This can be
1059 | " faster than pattern-based syntax matching, as processing is only done once
1060 | " (as opposed to processing on each screen redrawing) and doesn't require
1061 | " pattern matching. Use pattern-based syntax matching as a fall-back when
1062 | " the other approaches are not available. If the application of
1063 | " matchaddpos() was per-buffer, as opposed to per-window, it could be used
1064 | " in-place of the various approaches here. Per-window application is
1065 | " problematic, because subsequent changes to the file in the window will
1066 | " result in mis-applied highlighting.
1067 | if has('textprop') || exists('*nvim_buf_add_highlight')
1068 | call s:LocationColorize(a:event_types, a:field_bounds_table)
1069 | else
1070 | call s:SyntaxColorize(a:event_types)
1071 | endif
1072 | endfunction
1073 |
1074 | function! s:SaveCallback(varname, items, startup, timer_id) abort
1075 | " A mapping of types is returned since the internal integers are referenced
1076 | " by the array of items.
1077 | let g:[a:varname] = {
1078 | \ 'items': deepcopy(a:items),
1079 | \ 'startup': deepcopy(a:startup),
1080 | \ 'types': deepcopy(s:event_types),
1081 | \ }
1082 | doautocmd User StartupTimeSaved
1083 | endfunction
1084 |
1085 | function! s:Process(options, items) abort
1086 | let l:items = a:items
1087 | " Total startup time is determined prior to filtering. The reported
1088 | " startuptime above the table is independent of whether --no-sourcing-events
1089 | " or --no-other-events were used. If we filtered sourcing/other too early,
1090 | " we wouldn't be able to properly determine total startup time. Also, if we
1091 | " filtered other events too early, we wouldn't be able to remove the TUI
1092 | " sessions (see earlier code), since we rely on the 'opening buffers' event
1093 | " being present.
1094 | let l:startup = s:Startup(l:items)
1095 | let l:items = s:Consolidate(l:items)
1096 | let l:items = s:Augment(l:items, a:options)
1097 | " Filter prior to running the --save functionality, since that step should
1098 | " use the filtered data.
1099 | let l:items = s:Filter(l:items, a:options)
1100 | if a:options.sort
1101 | let l:Compare = {i1, i2 ->
1102 | \ i1.time ==# i2.time ? 0 : (i1.time <# i2.time ? 1 : -1)}
1103 | call sort(l:items, l:Compare)
1104 | endif
1105 | if !empty(a:options.save)
1106 | " Saving the data is executed asynchronously with a callback. Otherwise,
1107 | " when s:Process is called through startuptime#Main, 'eventignore' would
1108 | " be set to all and have to be handled.
1109 | call timer_start(0, function('s:SaveCallback',
1110 | \ [a:options.save, l:items, l:startup]))
1111 | endif
1112 | return [l:items, l:startup]
1113 | endfunction
1114 |
1115 | " Exit visual or selection mode (modifications can distort these modes).
1116 | function! s:ExitVisualMode(mode) abort
1117 | if s:IsVisualMode(a:mode)
1118 | \ || s:IsSelectMode(a:mode)
1119 | \ || s:IsVisualSelectMode(a:mode)
1120 | execute "normal! \"
1121 | endif
1122 | endfunction
1123 |
1124 | " Go back to visual or selection mode.
1125 | function! s:RestoreVisualMode(mode) abort
1126 | if s:IsVisualMode(a:mode)
1127 | execute 'normal! gv'
1128 | elseif s:IsSelectMode(a:mode)
1129 | execute "normal! gv\"
1130 | elseif s:IsVisualSelectMode(a:mode)
1131 | execute "normal! gv\\"
1132 | endif
1133 | endfunction
1134 |
1135 | " Load timing results from the specified file and show the results in the
1136 | " specified window. The file is deleted. The active window is retained.
1137 | function! startuptime#Main(file, bufnr, options, items) abort
1138 | let l:winid_pre = win_getid()
1139 | let l:eventignore = &eventignore
1140 | let l:mode = mode(1)
1141 | set eventignore=all
1142 | " Save event width for possible restoring.
1143 | let l:event_width = g:startuptime_event_width
1144 | try
1145 | call s:ExitVisualMode(l:mode)
1146 | let l:winid = get(win_findbuf(a:bufnr), 0, -1)
1147 | if l:winid ==# -1 | return | endif
1148 | call win_gotoid(l:winid)
1149 | let b:startuptime_profiling = 0
1150 | call s:SetBufLine(a:bufnr, 3, 'Processing...')
1151 | " Redraw so that "[100%]" and "Processing..." show. Don't do this if the
1152 | " tab changed, since it would result in flickering.
1153 | if getwininfo(l:winid_pre)[0].tabnr ==# getwininfo(l:winid)[0].tabnr
1154 | redraw!
1155 | endif
1156 | let l:processing_finished = 0
1157 | try
1158 | let [l:items, l:startup] = s:Process(a:options, a:items)
1159 | let l:processing_finished = 1
1160 | " Set 'modifiable' after :redraw so that e.g., if modifiable shows in
1161 | " the status line, it's display is not changed for the duration of
1162 | " running/processing.
1163 | setlocal modifiable
1164 | call s:ClearCurrentBuffer()
1165 | call s:RegisterMaps(l:items, a:options, l:startup)
1166 | if g:startuptime_event_width ==# 0
1167 | for l:item in l:items
1168 | let l:event = s:SimplifiedEvent(l:item)
1169 | let g:startuptime_event_width =
1170 | \ max([strchars(l:event), g:startuptime_event_width])
1171 | endfor
1172 | endif
1173 | let l:field_bounds_table = s:Tabulate(l:items, l:startup)
1174 | let l:event_types = map(copy(l:items), 'v:val.type')
1175 | if g:startuptime_colorize && (has('gui_running') || &t_Co > 1)
1176 | call s:Colorize(l:event_types, l:field_bounds_table)
1177 | endif
1178 | setlocal nowrap
1179 | setlocal list
1180 | setlocal listchars=precedes:<,extends:>
1181 | catch /^Vim:Interrupt$/
1182 | if !l:processing_finished
1183 | call s:SetBufLine(a:bufnr, 3, 'Processing cancelled')
1184 | endif
1185 | endtry
1186 | setlocal nomodifiable
1187 | finally
1188 | let g:startuptime_event_width = l:event_width
1189 | call win_gotoid(l:winid_pre)
1190 | call s:RestoreVisualMode(l:mode)
1191 | let &eventignore = l:eventignore
1192 | endtry
1193 | endfunction
1194 |
1195 | " Call feedkeys('') repetitively while vim-startuptime is profiling.
1196 | function! s:FeedKeysWhileProfiling(bufnr, ...)
1197 | if !bufexists(a:bufnr) | return | endif
1198 | if !getbufvar(a:bufnr, 'startuptime_profiling', 0) | return | endif
1199 | let l:start = a:0 ># 0 ? a:1 : reltime()
1200 | " If there has been zero progress for awhile, stop running feedkeys().
1201 | let l:elapsed = reltimefloat(reltime(l:start))
1202 | let l:limit = 60 " in seconds
1203 | if getbufvar(a:bufnr, 'startuptime_zero_progress', 1)
1204 | \ && l:elapsed ># l:limit
1205 | return
1206 | endif
1207 | call feedkeys('')
1208 | let l:interval = 50 " in milliseconds
1209 | call timer_start(l:interval,
1210 | \ {-> call('s:FeedKeysWhileProfiling', [a:bufnr, l:start])})
1211 | endfunction
1212 |
1213 | function! s:ShowZeroProgressMsg(winid, bufnr, options)
1214 | if !bufexists(a:bufnr) | return | endif
1215 | if !g:startuptime_zero_progress_msg | return | endif
1216 | if !getbufvar(a:bufnr, 'startuptime_zero_progress', 0) | return | endif
1217 | if a:options.input_file isnot# v:null | return | endif
1218 | let l:winid = win_getid()
1219 | let l:eventignore = &eventignore
1220 | let l:mode = mode(1)
1221 | set eventignore=all
1222 | try
1223 | call s:ExitVisualMode(l:mode)
1224 | if winbufnr(a:winid) !=# a:bufnr | return | endif
1225 | call win_gotoid(a:winid)
1226 | setlocal modifiable
1227 | let l:lines = [
1228 | \ '',
1229 | \ 'Is vim-startuptime stuck on 0% progress?',
1230 | \ '',
1231 | \ ' The plugin measures startuptime by asynchronously running (n)vim',
1232 | \ ' with the --startuptime argument. If there is a request for user',
1233 | \ ' input (e.g., "Press ENTER"), then processing will get stuck at 0%.',
1234 | \ '',
1235 | \ ' To investigate further, try starting a terminal with :terminal, and',
1236 | \ ' launching a nested instance of (n)vim. If you see "Press ENTER or',
1237 | \ ' type command to continue" or some other message interfering with',
1238 | \ ' ordinary startup, this could be problematic for vim-startuptime.',
1239 | \ ' Running :messages within the nested (n)vim may help identify the',
1240 | \ ' issue.',
1241 | \ '',
1242 | \ ' It may help to run a nested instance of (n)vim in a manner similar',
1243 | \ ' to vim-startuptime. The following lines show the shell-escaped',
1244 | \ ' program and arguments used by vim-startuptime.