├── .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. should be', 1245 | \ ' replaced with an output file.', 1246 | \ '', 1247 | \ ] 1248 | let l:command = s:ProfileCmd('', a:options) 1249 | call add(l:lines, ' ' . shellescape(l:command[0])) 1250 | for l:line in l:command[1:] 1251 | call add(l:lines, ' ' . shellescape(l:line)) 1252 | endfor 1253 | call extend(l:lines, [ 1254 | \ '', 1255 | \ ' Try running vim-startuptime again once the problem is avoided via a', 1256 | \ ' configuration update. You may have to update g:startuptime_exe_path.', 1257 | \ ]) 1258 | for l:line in l:lines 1259 | call s:SetBufLine(a:bufnr, line('$') + 1, l:line) 1260 | endfor 1261 | setlocal nomodifiable 1262 | finally 1263 | call win_gotoid(l:winid) 1264 | call s:RestoreVisualMode(l:mode) 1265 | let &eventignore = l:eventignore 1266 | endtry 1267 | endfunction 1268 | 1269 | " Updates progress bar. Returns a status indicating whether the startuptime 1270 | " buffer still exists. 1271 | function! s:OnProgress(bufnr, options, total, pending) abort 1272 | if !bufexists(a:bufnr) 1273 | return 0 1274 | endif 1275 | let l:winid_pre = win_getid() 1276 | let l:eventignore = &eventignore 1277 | let l:mode = mode(1) 1278 | set eventignore=all 1279 | try 1280 | call s:ExitVisualMode(l:mode) 1281 | let l:winid = get(win_findbuf(a:bufnr), 0, -1) 1282 | if l:winid ==# -1 | return 0 | endif 1283 | call win_gotoid(l:winid) 1284 | setlocal modifiable 1285 | let b:startuptime_zero_progress = a:pending ==# a:total 1286 | " Delete the zero-progress message. 1287 | if line('$') ># 2 1288 | " 'deletebufline' works better than 'delete' since it retains the 1289 | " position of the cursor, but is not available on earlier versions. 1290 | if exists('*deletebufline') 1291 | call deletebufline(a:bufnr, 3, '$') 1292 | else 1293 | 3,$delete _ 1294 | endif 1295 | endif 1296 | let l:percent = 100.0 * (a:total - a:pending) / a:total 1297 | call s:SetBufLine(a:bufnr, 2, printf("Running: [%.0f%%]", l:percent)) 1298 | if a:pending ==# a:total 1299 | call timer_start(g:startuptime_zero_progress_time, 1300 | \ {-> call('s:ShowZeroProgressMsg', [l:winid, a:bufnr, a:options])}) 1301 | endif 1302 | setlocal nomodifiable 1303 | finally 1304 | call win_gotoid(l:winid_pre) 1305 | call s:RestoreVisualMode(l:mode) 1306 | let &eventignore = l:eventignore 1307 | endtry 1308 | return 1 1309 | endfunction 1310 | 1311 | " Create a new window or tab with a buffer for startuptime. 1312 | function! s:New(mods) abort 1313 | try 1314 | let l:vert = s:Contains(a:mods, 'vertical') 1315 | let l:parts = ['split', '+enew'] 1316 | if s:Contains(a:mods, 'tab') 1317 | let l:parts = ['tabnew', '+enew'] 1318 | elseif s:Contains(a:mods, 'aboveleft') || s:Contains(a:mods, 'leftabove') 1319 | let l:parts = ['topleft'] + l:parts 1320 | elseif s:Contains(a:mods, 'belowright') || s:Contains(a:mods, 'rightbelow') 1321 | let l:parts = ['botright'] + l:parts 1322 | elseif &splitbelow && !l:vert 1323 | let l:parts = ['botright'] + l:parts 1324 | elseif &splitright && l:vert 1325 | let l:parts = ['botright'] + l:parts 1326 | else 1327 | let l:parts = ['topleft'] + l:parts 1328 | endif 1329 | if l:vert 1330 | let l:parts = ['vertical'] + l:parts 1331 | endif 1332 | let l:parts = ['silent'] + l:parts 1333 | execute join(l:parts) 1334 | catch 1335 | return 0 1336 | endtry 1337 | return 1 1338 | endfunction 1339 | 1340 | function! s:Options(args) abort 1341 | let l:options = { 1342 | \ 'help': 0, 1343 | \ 'hidden': 0, 1344 | \ 'input_file': v:null, 1345 | \ 'other_events': g:startuptime_other_events, 1346 | \ 'save': '', 1347 | \ 'sourced': g:startuptime_sourced, 1348 | \ 'sort': g:startuptime_sort, 1349 | \ 'sourcing_events': g:startuptime_sourcing_events, 1350 | \ 'tries': v:null, 1351 | \ 'exe_args': deepcopy(g:startuptime_exe_args), 1352 | \ } 1353 | let l:idx = 0 1354 | " WARN: Any new/removed/changed arguments below should have corresponding 1355 | " updates below in the startuptime#CompleteOptions function and the 1356 | " startuptime#StartupTime usage documentation. 1357 | while l:idx <# len(a:args) 1358 | let l:arg = a:args[l:idx] 1359 | if l:arg ==# '--help' 1360 | let l:options.help = 1 1361 | break 1362 | elseif l:arg ==# '--hidden' 1363 | let l:options.hidden = 1 1364 | elseif l:arg ==# '--input-file' 1365 | let l:idx += 1 1366 | let l:arg = a:args[l:idx] 1367 | let l:options.input_file = l:arg 1368 | elseif l:arg ==# '--other-events' || l:arg ==# '--no-other-events' 1369 | let l:options.other_events = l:arg ==# '--other-events' 1370 | elseif l:arg ==# '--save' 1371 | let l:idx += 1 1372 | let l:options.save = a:args[l:idx] 1373 | elseif l:arg ==# '--sourced' || l:arg ==# '--no-sourced' 1374 | let l:options.sourced = l:arg ==# '--sourced' 1375 | elseif l:arg ==# '--sort' || l:arg ==# '--no-sort' 1376 | let l:options.sort = l:arg ==# '--sort' 1377 | elseif l:arg ==# '--sourcing-events' || l:arg ==# '--no-sourcing-events' 1378 | let l:options.sourcing_events = l:arg ==# '--sourcing-events' 1379 | elseif l:arg ==# '--tries' 1380 | let l:idx += 1 1381 | let l:arg = a:args[l:idx] 1382 | let l:options.tries = str2nr(l:arg) 1383 | elseif l:arg ==# '--' 1384 | " Treat everything after a double dash as extra arguments. 1385 | let l:options.exe_args = a:args[l:idx + 1:] 1386 | break 1387 | else 1388 | throw 'vim-startuptime: unknown argument (' . l:arg . ')' 1389 | endif 1390 | 1391 | let l:idx += 1 1392 | endwhile 1393 | if l:options.tries isnot# v:null && l:options.input_file isnot# v:null 1394 | throw 'vim-startuptime: ' 1395 | \ . '--input-file and --tries cannot be combined' 1396 | endif 1397 | if !l:options.other_events && !l:options.sourcing_events 1398 | throw 'vim-startuptime: ' 1399 | \ . '--no-other-events and --no-sourcing-events cannot be combined' 1400 | endif 1401 | if l:options.tries is# v:null && l:options.input_file is# v:null 1402 | let l:options.tries = g:startuptime_tries 1403 | endif 1404 | if l:options.tries isnot# v:null 1405 | if type(l:options.tries) ==# v:t_float 1406 | let l:options.tries = float2nr(l:options.tries) 1407 | endif 1408 | if type(l:options.tries) !=# v:t_number || l:options.tries <# 1 1409 | throw 'vim-startuptime: invalid argument (tries)' 1410 | endif 1411 | endif 1412 | return l:options 1413 | endfunction 1414 | 1415 | " Returns the script ID, for testing functions with internal visibility. 1416 | function! startuptime#Sid() abort 1417 | let l:sid = expand('') 1418 | if !empty(l:sid) 1419 | return l:sid 1420 | endif 1421 | " Older versions of Vim cannot expand "". 1422 | if !exists('*s:Sid') 1423 | function s:Sid() abort 1424 | return matchstr(expand(''), '\zs\d\+_\zeSid$') 1425 | endfunction 1426 | endif 1427 | return s:Sid() 1428 | endfunction 1429 | 1430 | " A 'custom' completion function for :StartupTime. A 'custom' function is used 1431 | " instead of a 'customlist' function, for the automatic filtering that is 1432 | " conducted for the former, but not the latter. 1433 | function! startuptime#CompleteOptions(arglead, cmdline, cursorpos) abort 1434 | let l:args = [] 1435 | let l:cmdline = a:cmdline[:a:cursorpos - 1] 1436 | " First try to complete --input-file argument, with handling for paths with 1437 | " spaces. 1438 | let l:idx = stridx(l:cmdline, '--input-file') 1439 | if l:idx !=# -1 1440 | let l:offset = len('--input-file') 1441 | let l:str = l:cmdline[l:idx + l:offset:] 1442 | let l:str = substitute(l:str, '^ *', '', '') 1443 | " Don't proceed if there are spaces not preceded by backslash. 1444 | if l:str !~# '\\\@ substitute(x, ' ', '\\ ', 'g')}) 1448 | call map(l:matches, {_, x -> isdirectory(x) ? x .. s:pathsep : x}) 1449 | call extend(l:args, l:matches) 1450 | endif 1451 | endif 1452 | " If we couldn't complete an --input-file argument, try the other 1453 | " completions. 1454 | if empty(l:args) 1455 | let l:lead = trim(substitute(a:cmdline, a:arglead . '$', '', ''), ' ', 2) 1456 | if l:lead =~# '--tries$' 1457 | for l:digit in range(10) 1458 | let l:candidate = a:arglead .. l:digit 1459 | if str2nr(l:candidate) !=# 0 1460 | call add(l:args, l:candidate) 1461 | endif 1462 | endfor 1463 | elseif l:lead =~# '--input-file$' 1464 | " The handling above didn't have any completion. An argument is 1465 | " necessary for --input-file, so don't give the completions for other 1466 | " options. 1467 | else 1468 | call extend(l:args, [ 1469 | \ '--help', 1470 | \ '--hidden', 1471 | \ '--input-file', 1472 | \ '--other-events', '--no-other-events', 1473 | \ '--save', 1474 | \ '--sourced', '--no-sourced', 1475 | \ '--sort', '--no-sort', 1476 | \ '--sourcing-events', '--no-sourcing-events', 1477 | \ '--tries', 1478 | \ '--', 1479 | \ ]) 1480 | endif 1481 | endif 1482 | call sort(l:args) 1483 | return join(l:args, "\n") 1484 | endfunction 1485 | 1486 | function! s:ValidateInputFile(file) abort 1487 | let l:max_lines = 10 1488 | let l:input = readfile(a:file, '', l:max_lines) 1489 | let l:valid_format = v:false 1490 | let l:valid_editor = v:false 1491 | for l:line in l:input 1492 | if !l:valid_format && stridx(l:line, 'times in msec') !=# -1 1493 | let l:valid_format = v:true 1494 | endif 1495 | let l:editor_text = '--- VIM STARTING ---' 1496 | if has('nvim') 1497 | let l:editor_text = '--- NVIM STARTING ---' 1498 | endif 1499 | if !l:valid_editor && stridx(l:line, l:editor_text) !=# -1 1500 | let l:valid_editor = v:true 1501 | endif 1502 | endfor 1503 | if !l:valid_format 1504 | throw 'vim-startuptime: unrecognized file format' 1505 | endif 1506 | if !l:valid_editor 1507 | " Neovim startuptime files require additional handling (Neovim #23036, 1508 | " #26790). We don't support analzying startuptime files that weren't 1509 | " generated by the same editor in use. 1510 | throw 'vim-startuptime: unsupported file format' 1511 | endif 1512 | return v:true 1513 | endfunction 1514 | 1515 | " Usage: 1516 | " :StartupTime 1517 | " \ [--hidden] 1518 | " \ [--input-file STRING] 1519 | " \ [--other-events] [--no-other-events] 1520 | " \ [--save STRING] 1521 | " \ [--sort] [--no-sort] 1522 | " \ [--sourced] [--no-sourced] 1523 | " \ [--sourcing-events] [--no-sourcing-events] 1524 | " \ [--tries INT] 1525 | " \ [-- STRING] 1526 | " \ [--help] 1527 | function! startuptime#StartupTime(mods, ...) abort 1528 | if !has('nvim') && !has('terminal') 1529 | throw 'vim-startuptime: +terminal feature required' 1530 | endif 1531 | if !has('nvim') && !has('startuptime') 1532 | throw 'vim-startuptime: +startuptime feature required' 1533 | endif 1534 | let l:mods = split(a:mods) 1535 | let l:options = s:Options(a:000) 1536 | if l:options.help 1537 | execute a:mods . ' help startuptime.txt' 1538 | return 1539 | endif 1540 | let l:items = [] 1541 | let l:file = tempname() 1542 | if l:options.input_file isnot# v:null 1543 | call s:ValidateInputFile(l:options.input_file) 1544 | call writefile(readfile(l:options.input_file), l:file) 1545 | endif 1546 | if l:options.hidden 1547 | let l:OnProgress = function('s:True') 1548 | let l:OnFinish = function('s:Process', [l:options, l:items]) 1549 | else 1550 | if !s:New(l:mods) 1551 | throw 'vim-startuptime: couldn''t create new buffer' 1552 | endif 1553 | setlocal buftype=nofile 1554 | setlocal noswapfile 1555 | setlocal nofoldenable 1556 | setlocal foldcolumn=0 1557 | setlocal bufhidden=wipe 1558 | setlocal nobuflisted 1559 | setlocal filetype=startuptime 1560 | setlocal nospell 1561 | setlocal wrap 1562 | " Prevent the built-in matchparen plugin from highlighting matching brackets 1563 | " (on the vim-startuptime loading screen). The plugin can't be disabled at 1564 | " the buffer level. 1565 | setlocal matchpairs= 1566 | call s:SetFile() 1567 | call setline(1, '# vim-startuptime') 1568 | setlocal nomodifiable 1569 | let l:bufnr = bufnr('%') 1570 | let b:startuptime_profiling = 1 1571 | " Call feedkeys repetitively on Windows Neovim, as a workaround for Neovim 1572 | " #23203. 1573 | if has('nvim') && has('win32') 1574 | call s:FeedKeysWhileProfiling(l:bufnr) 1575 | endif 1576 | let l:winid = win_getid() 1577 | let l:OnProgress = function( 1578 | \ 's:OnProgress', [l:bufnr, l:options, l:options.tries]) 1579 | let l:OnFinish = function( 1580 | \ 'startuptime#Main', [l:file, l:bufnr, l:options, l:items]) 1581 | endif 1582 | call s:Profile( 1583 | \ l:OnFinish, l:OnProgress, l:options, l:options.tries, l:file, l:items) 1584 | endfunction 1585 | -------------------------------------------------------------------------------- /autoload/startuptime9.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # (documented in autoload/startuptime.vim) 4 | export def Extract( 5 | file: string, 6 | event_types: dict 7 | ): list>> 8 | const other_event_type = event_types['other'] 9 | const sourcing_event_type = event_types['sourcing'] 10 | final result = [] 11 | const lines = readfile(file) 12 | var occurrences: dict 13 | for line in lines 14 | if strchars(line) ==# 0 || line[0] !~# '^\d$' 15 | continue 16 | endif 17 | if line =~# ': --- N\=VIM STARTING ---$' 18 | add(result, []) 19 | occurrences = {} 20 | endif 21 | const idx = stridx(line, ':') 22 | const times = split(line[: idx - 1], '\s\+') 23 | const event = line[idx + 2 :] 24 | var type = other_event_type 25 | if len(times) ==# 3 26 | type = sourcing_event_type 27 | endif 28 | const key = type .. '-' .. event 29 | if has_key(occurrences, key) 30 | occurrences[key] += 1 31 | else 32 | occurrences[key] = 1 33 | endif 34 | # 'finish' time is reported as 'clock' in --startuptime output. 35 | final item = { 36 | 'event': event, 37 | 'occurrence': occurrences[key], 38 | 'finish': str2float(times[0]), 39 | 'type': type 40 | } 41 | if type ==# sourcing_event_type 42 | item['self+sourced'] = str2float(times[1]) 43 | item.self = str2float(times[2]) 44 | item.start = item.finish - item['self+sourced'] 45 | else 46 | item.elapsed = str2float(times[1]) 47 | item.start = item.finish - item.elapsed 48 | endif 49 | add(result[-1], item) 50 | endfor 51 | return result 52 | enddef 53 | 54 | def Mean(numbers: list): float 55 | if len(numbers) ==# 0 56 | throw 'vim-startuptime: cannot take mean of empty list' 57 | endif 58 | var result = 0.0 59 | for number in numbers 60 | result += number 61 | endfor 62 | result = result / len(numbers) 63 | return result 64 | enddef 65 | 66 | # (documented in autoload/startuptime.vim) 67 | def StandardDeviation( 68 | numbers: list, 69 | ddof: number, 70 | mean: float = str2float('nan') 71 | ): float 72 | const mean2 = isnan(mean) ? Mean(numbers) : mean 73 | var result = 0.0 74 | for number in numbers 75 | const diff = mean2 - number 76 | result += diff * diff 77 | endfor 78 | result = result / (len(numbers) - ddof) 79 | result = sqrt(result) 80 | return result 81 | enddef 82 | 83 | # (documented in autoload/startuptime.vim) 84 | export def Consolidate( 85 | items: list>>, tfields: list): list> 86 | final lookup = {} 87 | for try in items 88 | for item in try 89 | const key = item.type .. '-' .. item.occurrence .. '-' .. item.event 90 | if has_key(lookup, key) 91 | for tfield in tfields 92 | if has_key(item, tfield) 93 | add(lookup[key][tfield], item[tfield]) 94 | endif 95 | endfor 96 | lookup[key].tries += 1 97 | else 98 | lookup[key] = deepcopy(item) 99 | for tfield in tfields 100 | if has_key(lookup[key], tfield) 101 | # Put item in a list. 102 | lookup[key][tfield] = [lookup[key][tfield]] 103 | endif 104 | endfor 105 | lookup[key].tries = 1 106 | endif 107 | endfor 108 | endfor 109 | final result = values(lookup) 110 | for item in result 111 | for tfield in tfields 112 | if has_key(item, tfield) 113 | const mean = Mean(item[tfield]) 114 | # Use 1 for ddof, for sample standard deviation. 115 | const std = StandardDeviation(item[tfield], 1, mean) 116 | item[tfield] = {'mean': mean, 'std': std} 117 | endif 118 | endfor 119 | endfor 120 | # Sort on mean start time, event name, then occurrence. 121 | const Compare = (i1, i2) => 122 | i1.start.mean !=# i2.start.mean 123 | ? (i1.start.mean <# i2.start.mean ? -1 : 1) 124 | : (i1.event !=# i2.event 125 | ? (i1.event <# i2.event ? -1 : 1) 126 | : (i1.occurrence !=# i2.occurrence 127 | ? (i1.occurrence <# i2.occurrence ? -1 : 1) 128 | : 0)) 129 | sort(result, Compare) 130 | return result 131 | enddef 132 | -------------------------------------------------------------------------------- /doc/startuptime.txt: -------------------------------------------------------------------------------- 1 | *startuptime.txt* Plugin for viewing startup timing information 2 | *vim-startuptime* 3 | 4 | Author: Daniel Steinberg - https://www.dannyadam.com 5 | Web: https://github.com/dstein64/vim-startuptime 6 | 7 | 1. Requirements |startuptime-requirements| 8 | 2. Installation |startuptime-installation| 9 | 3. Usage |startuptime-usage| 10 | 4. Configuration |startuptime-configuration| 11 | 5. API |startuptime-api| 12 | 6. Information |startuptime-information| 13 | 14 | |vim-startuptime| is a plugin for viewing vim and nvim startup event timing 15 | information, obtained by running (n)vim with the |--startuptime| argument. The 16 | plugin is customizable (see |startuptime-configuration|). 17 | 18 | Related Documentation ~ 19 | 20 | 1. |--startuptime| `vim` command option 21 | 2. |+startuptime| `vim` compile-time feature 22 | 23 | ============================================================================ 24 | 1. Requirements *startuptime-requirements* 25 | 26 | * `vim>=8.0.1453` or `nvim>=0.3.1` 27 | - The plugin may work on earlier versions, but has not been tested. 28 | - The plugin depends on compile-time features for vim (not applicable for 29 | nvim). 30 | * |+startuptime| is required. 31 | * |+timers| is recommended, to capture all startup events. 32 | * |+terminal| is required. 33 | 34 | ============================================================================ 35 | 2. Installation *startuptime-installation* 36 | 37 | Use |packages| or one of the various package managers. 38 | 39 | ============================================================================ 40 | 3. Usage *startuptime-usage* 41 | 42 | * The *:StartupTime* command launches |vim-startuptime|. 43 | * Press `K` on events to get additional information. 44 | * Press `gf` on sourcing events to load the corresponding file in a new split. 45 | * The key sequences above can be customized (see |startuptime-configuration|). 46 | * Times are in milliseconds. 47 | 48 | Arguments ~ 49 | *startuptime-arguments* 50 | |:StartupTime| takes the following optional arguments. 51 | 52 | * `--hidden` prevents the output table from being generated. This may be useful 53 | when |:StartupTime| is being run for its side effects (see |startuptime-api|). 54 | * `--input-file` specifies a file that was generated by running (n)vim with the 55 | |--startuptime| argument. This can be used instead of having the plugin obtain 56 | the timing information. The argument can't be combined with `--tries`. 57 | * `--sort` and `--no-sort` specify whether events are sorted. 58 | * `--sourcing-events` and `--no-sourcing-events` specify whether sourcing 59 | events are included. 60 | * `--other-events` and `--no-other-events` specify whether other events 61 | are included. 62 | * `--save` specifies a global variable for saving the underlying data (see 63 | |startuptime-api|). 64 | * `--sourced` and `--no-sourced` specify whether to use 'self+sourced' timings 65 | for sourcing events (otherwise, 'self' timings are used). 66 | * `--tries` specifies how many startup times are averaged. 67 | * `--` is followed by space-separated arguments to pass to `vim`; this must come 68 | after any other |vim-startuptime| optional arguments. Be sure to escape 69 | spaces (with a leading backslash) that don't separate arguments. 70 | * `--help` shows this help documentation. 71 | > 72 | :StartupTime 73 | \ [--hidden] 74 | \ [--input-file STRING] 75 | \ [--sort] [--no-sort] 76 | \ [--sourcing-events] [--no-sourcing-events] 77 | \ [--other-events] [--no-other-events] 78 | \ [--save STRING] 79 | \ [--sourced] [--no-sourced] 80 | \ [--tries INT] 81 | \ [-- STRING] 82 | \ [--help] 83 | 84 | Modifiers ~ 85 | *startuptime-modifiers* 86 | |:StartupTime| accepts the following modifiers. 87 | 88 | * |:tab| 89 | * |:aboveleft| or |:leftabove| 90 | * |:belowright| or |:rightbelow| 91 | * |:vertical| 92 | 93 | Vim Options ~ 94 | *startuptime-vim-options* 95 | |:StartupTime| observes the following options, but these are overruled by 96 | |startuptime-modifiers|. 97 | 98 | * |'splitbelow'| 99 | * |'splitright'| 100 | 101 | ============================================================================ 102 | 4. Configuration *startuptime-configuration* 103 | 104 | The following variables can be used to customize the behavior of 105 | |vim-startuptime|. The optional |startuptime-arguments| have higher precedence 106 | than these options. 107 | 108 | `Variable` `Default` 109 | Description Info 110 | ------------- ------- 111 | *g:startuptime_more_info_key_seq* `'K'` 112 | Key sequence for getting more Disable with `''` 113 | information 114 | *g:startuptime_split_edit_key_seq* `'gf'` 115 | Key sequence for loading a sourcing Disable with `''` 116 | event file in a split window 117 | *g:startuptime_exe_path* `'RUNNING_VIM_PATH'` 118 | Path to `vim` for startup timing 119 | *g:startuptime_exe_args* `[]` 120 | Optional arguments to pass to `vim` 121 | *g:startuptime_sort* |v:true| 122 | Specifies whether events are sorted 123 | *g:startuptime_tries* `1` 124 | Specifies how many startup times 125 | are averaged 126 | *g:startuptime_sourcing_events* |v:true| 127 | Specifies whether sourcing events 128 | are included 129 | *g:startuptime_other_events* |v:true| 130 | Specifies whether other events are 131 | are included 132 | *g:startuptime_sourced* |v:true| 133 | Specifies whether to include 134 | 'sourced' timings (in addition to 135 | 'self' timings) for sourcing events 136 | *g:startuptime_event_width* `20` 137 | Event column width. When set to 0, 138 | the column width will be dynamically 139 | set so that no text is truncated. 140 | *g:startuptime_time_width* `6` 141 | Time column width 142 | *g:startuptime_percent_width* `7` 143 | Percent column width 144 | *g:startuptime_plot_width* `26` 145 | Plot column width 146 | *g:startuptime_colorize* |v:true| 147 | Specifies whether table data is 148 | colorized 149 | *g:startuptime_use_blocks* |v:true| if 'encoding' is set to "utf-8" 150 | Specifies whether Unicode block and |v:false| otherwise 151 | elements are used for plotting 152 | *g:startuptime_fine_blocks* |v:false| on Windows and |v:true| 153 | Specifies whether 1/8 increments otherwise 154 | are used for Unicode blocks (1/2 155 | increments are used otherwise) 156 | *g:startuptime_startup_indent* `7` 157 | Indentation for the startup row 158 | *g:startuptime_zero_progress_msg* |v:true| 159 | Specifies whether a debug message 160 | is shown when progress is 0% 161 | *g:startuptime_zero_progress_time* `2000` 162 | Specifies the time in milliseconds 163 | before showing a debug message when 164 | progress is 0% 165 | 166 | The variables can be customized in your |.vimrc|, as shown in the following 167 | example. 168 | > 169 | let g:startuptime_sort = v:false 170 | let g:startuptime_tries = 5 171 | let g:startuptime_exe_args = ['-u', '~/.vim/vimrc'] 172 | 173 | Color Customization ~ 174 | *startuptime-color-customization* 175 | The following highlight groups can be configured to change |vim-startuptime|'s 176 | colors. 177 | 178 | Name Default Description 179 | ---- ------- ----------- 180 | `StartupTimeStartupKey` `Normal` Color for the startup key label 181 | `StartupTimeStartupValue` `Title` Color for the startup value 182 | `StartupTimeHeader` `ModeMsg` Color for the header row text 183 | `StartupTimeSourcingEvent` `Type` Color for sourcing event names 184 | `StartupTimeOtherEvent` `Identifier` Color for other event names 185 | `StartupTimeTime` `Directory` Color for the time column 186 | `StartupTimePercent` `Special` Color for the percent column 187 | `StartupTimePlot` `Normal` Color for the plot column 188 | 189 | The highlight groups can be customized in your |.vimrc|, as shown in the 190 | following example. 191 | > 192 | " Link StartupTimeSourcingEvent highlight to Title highlight 193 | highlight link StartupTimeSourcingEvent Title 194 | 195 | " Specify custom highlighting for StartupTimeTime 196 | highlight StartupTimeTime 197 | \ term=bold ctermfg=12 ctermbg=159 guifg=Blue guibg=LightCyan 198 | 199 | Filetype Plugin ~ 200 | *startuptime-ftplugin* 201 | A `startuptime` filetype plugin (|ftplugin|) can be used to further customize 202 | |vim-startuptime|. 203 | 204 | ============================================================================ 205 | 5. API *startuptime-api* 206 | 207 | The plugin provides a way to access the data used for generating the output 208 | table. When the `--save` option is given to |:StartupTime|, a dictionary of 209 | data will be saved to the specified variable. 210 | 211 | {'startup': ..., 'items': ..., 'types': ...} 212 | 213 | The `startup` entry has a dictionary with mean and standard deviation of total 214 | startup time, and the number of tries. 215 | 216 | {'mean': ..., 'std': ..., 'tries': ...} 217 | 218 | The `items` entry is an array of dictionaries, each corresponding to a startup 219 | event. This is the data used to generate |:StartupTime| output. The following 220 | example shows a `sourcing` event and an `other` event. 221 | 222 | [ 223 | { 224 | 'occurrence': 1, 225 | 'elapsed': { 226 | 'std': str2float('nan'), 227 | 'mean': 0.006 228 | }, 229 | 'finish': { 230 | 'std': str2float('nan'), 231 | 'mean': 0.006 232 | }, 233 | 'event': '--- NVIM STARTING ---', 234 | 'time': 0.006, 235 | 'tries': 1, 236 | 'type': 1, 237 | 'start': { 238 | 'std': str2float('nan'), 239 | 'mean': 0.0 240 | } 241 | }, 242 | ..., 243 | { 244 | 'occurrence': 1, 245 | 'self+sourced': { 246 | 'std': str2float('nan'), 247 | 'mean': 0.053 248 | }, 249 | 'finish': { 250 | 'std': str2float('nan'), 251 | 'mean': 4.913 252 | }, 253 | 'self': { 254 | 'std': str2float('nan'), 255 | 'mean': 0.053 256 | }, 257 | 'type': 0, 258 | 'time': 0.053, 259 | 'tries': 1, 260 | 'event': 'sourcing .../nvim/runtime/filetype.lua', 261 | 'start': { 262 | 'std': str2float('nan'), 263 | 'mean': 4.86 264 | } 265 | }, 266 | ... 267 | ] 268 | 269 | The `types` entry provides a mapping between different item types and their 270 | representation in the `type` field of items in the `items` array. 271 | 272 | {'sourcing': 0, 'other': 1} 273 | 274 | When the data is saved, the *StartupTimeSaved* |User| |autocommand| is triggered. 275 | 276 | The `--hidden` argument prevents the output table from being generated. This 277 | may be useful when |:StartupTime| is being run only to extract data. 278 | 279 | Example: 280 | 281 | :autocmd User StartupTimeSaved echo g:saved_startuptime.startup.mean 282 | :StartupTime --save saved_startuptime --hidden 283 | 284 | The |:autocmd| line sets up a command for outputting average startup time 285 | when it's ready. The |:StartupTime| line runs the plugin, saving the data to a 286 | global variable and specifying that the results table not be generated. Once 287 | the data is ready, the command is triggered that outputs the result. 288 | 289 | ============================================================================ 290 | 6. Information *startuptime-information* 291 | 292 | The `startup` time reported at the top of the output is the total time to start 293 | the editor. This does not necessarily equal the sum of values in the time 294 | column since events can overlap. The reported percents are taken relative to 295 | the `startup` time, and thus don't necessarily sum to 100%. 296 | 297 | The require(...) events added in Neovim PR #19267 are treated as if they are 298 | sourcing events. The full path to the underlying file is not provided, but an 299 | attempt to deduce this path is made when fulfilling a request to open the file 300 | in a new split. 301 | 302 | ============================================================================ 303 | vim:tw=78:ts=4:ft=help:norl: 304 | -------------------------------------------------------------------------------- /doc/tags: -------------------------------------------------------------------------------- 1 | :StartupTime startuptime.txt /*:StartupTime* 2 | StartupTimeSaved startuptime.txt /*StartupTimeSaved* 3 | g:startuptime_colorize startuptime.txt /*g:startuptime_colorize* 4 | g:startuptime_event_width startuptime.txt /*g:startuptime_event_width* 5 | g:startuptime_exe_args startuptime.txt /*g:startuptime_exe_args* 6 | g:startuptime_exe_path startuptime.txt /*g:startuptime_exe_path* 7 | g:startuptime_fine_blocks startuptime.txt /*g:startuptime_fine_blocks* 8 | g:startuptime_more_info_key_seq startuptime.txt /*g:startuptime_more_info_key_seq* 9 | g:startuptime_other_events startuptime.txt /*g:startuptime_other_events* 10 | g:startuptime_percent_width startuptime.txt /*g:startuptime_percent_width* 11 | g:startuptime_plot_width startuptime.txt /*g:startuptime_plot_width* 12 | g:startuptime_sort startuptime.txt /*g:startuptime_sort* 13 | g:startuptime_sourced startuptime.txt /*g:startuptime_sourced* 14 | g:startuptime_sourcing_events startuptime.txt /*g:startuptime_sourcing_events* 15 | g:startuptime_split_edit_key_seq startuptime.txt /*g:startuptime_split_edit_key_seq* 16 | g:startuptime_startup_indent startuptime.txt /*g:startuptime_startup_indent* 17 | g:startuptime_time_width startuptime.txt /*g:startuptime_time_width* 18 | g:startuptime_tries startuptime.txt /*g:startuptime_tries* 19 | g:startuptime_use_blocks startuptime.txt /*g:startuptime_use_blocks* 20 | g:startuptime_zero_progress_msg startuptime.txt /*g:startuptime_zero_progress_msg* 21 | g:startuptime_zero_progress_time startuptime.txt /*g:startuptime_zero_progress_time* 22 | startuptime-api startuptime.txt /*startuptime-api* 23 | startuptime-arguments startuptime.txt /*startuptime-arguments* 24 | startuptime-color-customization startuptime.txt /*startuptime-color-customization* 25 | startuptime-configuration startuptime.txt /*startuptime-configuration* 26 | startuptime-ftplugin startuptime.txt /*startuptime-ftplugin* 27 | startuptime-information startuptime.txt /*startuptime-information* 28 | startuptime-installation startuptime.txt /*startuptime-installation* 29 | startuptime-modifiers startuptime.txt /*startuptime-modifiers* 30 | startuptime-requirements startuptime.txt /*startuptime-requirements* 31 | startuptime-usage startuptime.txt /*startuptime-usage* 32 | startuptime-vim-options startuptime.txt /*startuptime-vim-options* 33 | startuptime.txt startuptime.txt /*startuptime.txt* 34 | vim-startuptime startuptime.txt /*vim-startuptime* 35 | -------------------------------------------------------------------------------- /lua/startuptime.lua: -------------------------------------------------------------------------------- 1 | -- (documented in autoload/startuptime.vim) 2 | local extract = function(file, event_types) 3 | local other_event_type = event_types['other'] 4 | local sourcing_event_type = event_types['sourcing'] 5 | local result = {} 6 | local occurrences 7 | for line in io.lines(file) do 8 | if #line ~= 0 and line:find('^%d') ~= nil then 9 | if line:find(': --- N?VIM STARTING ---$') ~= nil then 10 | table.insert(result, {}) 11 | occurrences = {} 12 | end 13 | local idx = line:find(':') 14 | local times = {} 15 | for s in line:sub(1, idx - 1):gmatch('[^ ]+') do 16 | table.insert(times, tonumber(s)) 17 | end 18 | local event = line:sub(idx + 2) 19 | local type = other_event_type 20 | if #times == 3 then 21 | type = sourcing_event_type 22 | end 23 | local key = type .. '-' .. event 24 | if occurrences[key] ~= nil then 25 | occurrences[key] = occurrences[key] + 1 26 | else 27 | occurrences[key] = 1 28 | end 29 | -- 'finish' time is reported as 'clock' in --startuptime output. 30 | local item = { 31 | event = event, 32 | occurrence = occurrences[key], 33 | finish = times[1], 34 | type = type 35 | } 36 | if type == sourcing_event_type then 37 | item['self+sourced'] = times[2] 38 | item.self = times[3] 39 | item.start = item.finish - item['self+sourced'] 40 | else 41 | item.elapsed = times[2] 42 | item.start = item.finish - item.elapsed 43 | end 44 | table.insert(result[#result], item) 45 | end 46 | end 47 | return result 48 | end 49 | 50 | local mean = function(numbers) 51 | if #numbers == 0 then 52 | error('vim-startuptime: cannot take mean of empty list') 53 | end 54 | local result = 0.0 55 | for _, number in ipairs(numbers) do 56 | result = result + number 57 | end 58 | result = result / #numbers 59 | return result 60 | end 61 | 62 | -- (documented in autoload/startuptime.vim) 63 | local standard_deviation = function(numbers, ddof, _mean) 64 | if _mean == nil then 65 | _mean = mean(numbers) 66 | end 67 | local result = 0.0 68 | for _, number in ipairs(numbers) do 69 | local diff = _mean - number 70 | result = result + (diff * diff) 71 | end 72 | result = result / (#numbers - ddof) 73 | result = math.sqrt(result) 74 | return result 75 | end 76 | 77 | -- (documented in autoload/startuptime.vim) 78 | local consolidate = function(items, tfields) 79 | local lookup = {} 80 | for _, try in ipairs(items) do 81 | for _, item in ipairs(try) do 82 | local key = item.type .. '-' .. item.occurrence .. '-' .. item.event 83 | if lookup[key] ~= nil then 84 | for _, tfield in ipairs(tfields) do 85 | if item[tfield] ~= nil then 86 | table.insert(lookup[key][tfield], item[tfield]) 87 | end 88 | end 89 | lookup[key].tries = lookup[key].tries + 1 90 | else 91 | lookup[key] = vim.deepcopy(item) 92 | for _, tfield in ipairs(tfields) do 93 | if lookup[key][tfield] ~= nil then 94 | -- Put item in a list. 95 | lookup[key][tfield] = {lookup[key][tfield]} 96 | end 97 | end 98 | lookup[key].tries = 1 99 | end 100 | end 101 | end 102 | local result = {} 103 | for _, val in pairs(lookup) do 104 | table.insert(result, val) 105 | end 106 | for _, item in ipairs(result) do 107 | for _, tfield in ipairs(tfields) do 108 | if item[tfield] ~= nil then 109 | local _mean = mean(item[tfield]) 110 | -- Use 1 for ddof, for sample standard deviation. 111 | local std = standard_deviation(item[tfield], 1, _mean) 112 | item[tfield] = {mean = _mean, std = std} 113 | end 114 | end 115 | end 116 | table.sort(result, function(i1, i2) 117 | -- Sort on mean start time, event name, then occurrence. 118 | if i1.start.mean ~= i2.start.mean then 119 | return i1.start.mean < i2.start.mean 120 | elseif i1.event ~= i2.event then 121 | return i1.event < i2.event 122 | else 123 | return i1.occurrence < i2.occurrence 124 | end 125 | end) 126 | return result 127 | end 128 | 129 | -- Given extraction results (from startuptime::Extract), drop the entries that 130 | -- correspond to the TUI Neovim process. Neovim #23036, #26790. 131 | -- In Neovim 0.9, the TUI data comes after the main process data. In Neovim 132 | -- 0.10, the startup times are labeled for the different processes 133 | -- (Primary/TUI or Embedded). The main process data can be in either section 134 | -- (for example, it would ordinarily be under "Embedded", but it's under 135 | -- "Primary/TUI" when nvim is called from :!). Here we determine TUI sessions 136 | -- by their lack of an event that occurs for main processes but not the TUI 137 | -- process. 138 | local remove_tui_sessions = function(sessions) 139 | local result = {} 140 | for _, session in ipairs(sessions) do 141 | for _, item in ipairs(session) do 142 | if item.event == 'opening buffers' then 143 | table.insert(result, session) 144 | break 145 | end 146 | end 147 | end 148 | return result 149 | end 150 | 151 | return { 152 | extract = extract, 153 | consolidate = consolidate, 154 | remove_tui_sessions = remove_tui_sessions, 155 | } 156 | -------------------------------------------------------------------------------- /plugin/startuptime.vim: -------------------------------------------------------------------------------- 1 | if get(g:, 'loaded_startuptime', 0) 2 | finish 3 | endif 4 | let g:loaded_startuptime = 1 5 | 6 | let s:save_cpo = &cpo 7 | set cpo&vim 8 | 9 | if !exists(':StartupTime') 10 | command -nargs=* -complete=custom,startuptime#CompleteOptions StartupTime 11 | \ :call startuptime#StartupTime(, ) 12 | endif 13 | 14 | " ************************************************* 15 | " * Utils 16 | " ************************************************* 17 | 18 | " Converts 1 and 0 to v:true and v:false. 19 | function! s:ToBool(x) 20 | if a:x 21 | return v:true 22 | else 23 | return v:false 24 | endif 25 | endfunction 26 | 27 | " Returns true if Vim is running on Windows Subsystem for Linux. 28 | function! s:OnWsl() 29 | " Recent versions of neovim provide a 'wsl' pseudo-feature. 30 | if has('wsl') | return v:true | endif 31 | if !has('unix') | return v:false | endif 32 | " Read /proc/version instead of using `uname` because 1) it's faster and 2) 33 | " so that this works in restricted mode. 34 | try 35 | if filereadable('/proc/version') 36 | let l:version = readfile('/proc/version', '', 1) 37 | if len(l:version) ># 0 && stridx(l:version[0], 'Microsoft') ># -1 38 | return v:true 39 | endif 40 | endif 41 | catch 42 | endtry 43 | return v:false 44 | endfunction 45 | 46 | " ************************************************* 47 | " * User Configuration 48 | " ************************************************* 49 | 50 | let g:startuptime_more_info_key_seq = 51 | \ get(g:, 'startuptime_more_info_key_seq', 'K') 52 | let g:startuptime_split_edit_key_seq = 53 | \ get(g:, 'startuptime_split_edit_key_seq', 'gf') 54 | 55 | let g:startuptime_exe_path = 56 | \ get(g:, 'startuptime_exe_path', exepath(v:progpath)) 57 | let g:startuptime_exe_args = 58 | \ get(g:, 'startuptime_exe_args', []) 59 | 60 | let g:startuptime_sort = get(g:, 'startuptime_sort', v:true) 61 | let g:startuptime_tries = get(g:, 'startuptime_tries', 1) 62 | let g:startuptime_sourcing_events = get(g:, 'startuptime_sourcing_events', v:true) 63 | let g:startuptime_other_events = get(g:, 'startuptime_other_events', v:true) 64 | " '--self' was removed, with '--sourced' being used now to control the same 65 | " setting (but reversed). The following handling allows configurations to 66 | " continue working if 'startuptime_self' was specified. 67 | let g:startuptime_self = get(g:, 'startuptime_self', v:false) 68 | let g:startuptime_sourced = 69 | \ get(g:, 'startuptime_sourced', s:ToBool(!g:startuptime_self)) 70 | 71 | let g:startuptime_startup_indent = 72 | \ get(g:, 'startuptime_startup_indent', 7) 73 | let g:startuptime_event_width = 74 | \ get(g:, 'startuptime_event_width', 20) 75 | let g:startuptime_time_width = 76 | \ get(g:, 'startuptime_time_width', 6) 77 | let g:startuptime_percent_width = 78 | \ get(g:, 'startuptime_percent_width', 7) 79 | let g:startuptime_plot_width = 80 | \ get(g:, 'startuptime_plot_width', 26) 81 | 82 | let g:startuptime_colorize = 83 | \ get(g:, 'startuptime_colorize', v:true) 84 | 85 | let s:use_blocks = has('multi_byte') && &g:encoding ==# 'utf-8' 86 | let g:startuptime_use_blocks = 87 | \ get(g:, 'startuptime_use_blocks', s:ToBool(s:use_blocks)) 88 | " The built-in Windows terminal emulator (used for CMD, Powershell, and WSL) 89 | " does not properly display some block characters (i.e., the 1/8 precision 90 | " blocks) using the default font, Consolas. The characters display properly on 91 | " Cygwin using its default font, Lucida Console, and also when using Consolas. 92 | let s:win_term = has('win32') || s:OnWsl() 93 | let g:startuptime_fine_blocks = 94 | \ get(g:, 'startuptime_fine_blocks', s:ToBool(!s:win_term)) 95 | 96 | let g:startuptime_zero_progress_msg = 97 | \ get(g:, 'startuptime_zero_progress_msg', v:true) 98 | let g:startuptime_zero_progress_time = 99 | \ get(g:, 'startuptime_zero_progress_time', 2000) 100 | 101 | " The default highlight groups (for colors) are specified below. 102 | " Change these default colors by defining or linking the corresponding 103 | " highlight groups. 104 | " E.g., the following will use the Title highlight for sourcing event text. 105 | " :highlight link StartupTimeSourcingEvent Title 106 | " E.g., the following will use custom highlight colors for event times. 107 | " :highlight StartupTimeTime 108 | " \ term=bold ctermfg=12 ctermbg=159 guifg=Blue guibg=LightCyan 109 | highlight default link StartupTimeStartupKey Normal 110 | highlight default link StartupTimeStartupValue Title 111 | highlight default link StartupTimeHeader ModeMsg 112 | highlight default link StartupTimeSourcingEvent Type 113 | highlight default link StartupTimeOtherEvent Identifier 114 | highlight default link StartupTimeTime Directory 115 | highlight default link StartupTimePercent Special 116 | highlight default link StartupTimePlot Normal 117 | 118 | let &cpo = s:save_cpo 119 | unlet s:save_cpo 120 | -------------------------------------------------------------------------------- /tests/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import glob 4 | import os 5 | from string import Template 6 | import subprocess 7 | import sys 8 | import tempfile 9 | 10 | test_dir = os.path.dirname(os.path.realpath(__file__)) 11 | project_dir = os.path.join(test_dir, os.path.pardir) 12 | test_scripts = sorted(glob.glob(os.path.join(test_dir, 'test_*.vim'))) 13 | ignored = { 14 | 'Vim: Warning: Output is not to a terminal', 15 | 'Vim: Warning: Input is not from a terminal' 16 | } 17 | errors = [] 18 | template = Template(""" 19 | try 20 | source ${file} 21 | catch 22 | call assert_report(v:throwpoint . ': ' . v:exception) 23 | endtry 24 | verbose echo join(v:errors, "\\n") 25 | quitall! 26 | """) 27 | for test_script in test_scripts: 28 | for program in ('vim', 'nvim'): 29 | with tempfile.TemporaryDirectory() as tmp: 30 | runner_script = os.path.join(tmp, 'runner.vim') 31 | with open(runner_script, 'w') as f: 32 | f.write(template.substitute( 33 | project_dir=project_dir, file=test_script)) 34 | args = [ 35 | program, 36 | '-n', # no swap file 37 | '-e', # start in Ex mode 38 | '-s', # silent mode 39 | '-S', runner_script, # source the test runner script 40 | ] 41 | if program == 'vim': 42 | args.append('-N') # Disable Vi-compatibility 43 | result = subprocess.run(args, capture_output=True) 44 | lines = result.stderr.decode('ascii').splitlines() 45 | lines = [line.strip() for line in lines] 46 | lines = [line for line in lines if line] 47 | lines = [line for line in lines if line not in ignored] 48 | for line in lines: 49 | print(line, file=sys.stderr) 50 | errors.extend(lines) 51 | sys.exit(min(len(errors), 255)) 52 | -------------------------------------------------------------------------------- /tests/test_consistency.vim: -------------------------------------------------------------------------------- 1 | " Test that startuptime functions are consistent across VimScript/Lua in 2 | " Neovim and VimScript/Vim9 in Vim. 3 | 4 | let s:sid = startuptime#Sid() 5 | let s:Options = function(s:sid . 'Options') 6 | let s:ExtractVimScript = function(s:sid . 'ExtractVimScript') 7 | let s:ConsolidateVimScript = function(s:sid . 'ConsolidateVimScript') 8 | let s:tfields = function(s:sid . 'TFields')() 9 | if has('nvim') 10 | let s:ExtractOpt = function(s:sid . 'ExtractLua') 11 | let s:ConsolidateOpt = function(s:sid . 'ConsolidateLua') 12 | else 13 | let s:ExtractOpt = function(s:sid . 'ExtractVim9') 14 | let s:ConsolidateOpt = function(s:sid . 'ConsolidateVim9') 15 | endif 16 | 17 | function! s:Execute(file) abort 18 | " XXX: A more thorough approach for using --startuptime is used in s:Profile 19 | " in autoload/startuptime.vim, to retain all events. 20 | let l:exepath = exepath(v:progpath) 21 | silent execute '!' . l:exepath . ' --startuptime ' . a:file . ' +qall\!' 22 | endfunction 23 | 24 | let s:file = tempname() 25 | try 26 | call s:Execute(s:file) 27 | let s:optionss = [] 28 | for s:other in ['--other-events', '--no-other-events'] 29 | for s:sourced in ['--sourced', '--no-sourced'] 30 | for s:sort in ['--sort', '--no-sort'] 31 | for s:sourcing in ['--sourcing-events', '--no-sourcing-events'] 32 | try 33 | let s:options = s:Options([s:other, s:sourced, s:sort, s:sourcing]) 34 | call add(s:optionss, s:options) 35 | catch 36 | endtry 37 | endfor 38 | endfor 39 | endfor 40 | endfor 41 | " 4 of the 16 option configurations should error. 42 | call assert_equal(12, len(s:optionss)) 43 | 44 | for s:options in s:optionss 45 | " Test Lua/VimScript consistency with default options. 46 | let s:extracted_vimscript = s:ExtractVimScript(s:file) 47 | call assert_equal(1, len(s:extracted_vimscript)) 48 | call assert_true(!empty(s:extracted_vimscript[0])) 49 | let s:extracted_opt = s:ExtractOpt(s:file) 50 | call assert_equal(s:extracted_vimscript, s:extracted_opt) 51 | let s:consolidated_vimscript = 52 | \ s:ConsolidateVimScript(s:extracted_vimscript) 53 | let s:consolidated_opt = s:ConsolidateOpt(s:extracted_opt) 54 | call assert_equal( 55 | \ len(s:consolidated_vimscript), len(s:consolidated_opt)) 56 | " Compare each item individually for improved output when test fails. 57 | for s:idx in range(len(s:consolidated_vimscript)) 58 | let s:item_vimscript = s:consolidated_vimscript[s:idx] 59 | let s:item_opt = s:consolidated_opt[s:idx] 60 | " Convert NaN standard deviation to -1 since NaN !=# NaN. 61 | for s:item in [s:item_vimscript, s:item_opt] 62 | for s:tfield in s:tfields 63 | if has_key(s:item, s:tfield) && isnan(s:item[s:tfield].std) 64 | let s:item[s:tfield].std = -1 65 | endif 66 | endfor 67 | endfor 68 | call assert_equal(s:item_vimscript, s:item_opt) 69 | endfor 70 | endfor 71 | 72 | " Add more profiling data and test again. 73 | for _ in range(5) 74 | call s:Execute(s:file) 75 | endfor 76 | for s:options in s:optionss 77 | let s:extracted_vimscript = s:ExtractVimScript(s:file) 78 | call assert_equal(6, len(s:extracted_vimscript)) 79 | call assert_true(!empty(s:extracted_vimscript[0])) 80 | let s:extracted_opt = s:ExtractOpt(s:file) 81 | call assert_equal(s:extracted_vimscript, s:extracted_opt) 82 | let s:consolidated_vimscript = 83 | \ s:ConsolidateVimScript(s:extracted_vimscript) 84 | let s:consolidated_opt = s:ConsolidateOpt(s:extracted_opt) 85 | call assert_equal(len(s:consolidated_vimscript), len(s:consolidated_opt)) 86 | " Compare each item individually for improved output when test fails. 87 | for s:idx in range(len(s:consolidated_vimscript)) 88 | call assert_equal( 89 | \ s:consolidated_vimscript[s:idx], s:consolidated_opt[s:idx]) 90 | endfor 91 | endfor 92 | finally 93 | call delete(s:file) 94 | endtry 95 | -------------------------------------------------------------------------------- /tests/test_startuptime.vim: -------------------------------------------------------------------------------- 1 | " Test various functionality of vim-startuptime. 2 | 3 | " ############################################### 4 | " # Test --hidden, --save, and --tries. 5 | " ############################################### 6 | 7 | augroup test 8 | autocmd! 9 | autocmd User StartupTimeSaved let autocmd = 1 10 | augroup END 11 | StartupTime --tries 3 --save save1 --hidden 12 | sleep 3 13 | call assert_true(has_key(g:, 'save1')) 14 | call assert_equal(['items', 'startup', 'types'], sort(copy(keys(g:save1)))) 15 | call assert_equal(v:t_float, type(g:save1.startup.mean)) 16 | call assert_equal({'sourcing': 0, 'other': 1}, g:save1.types) 17 | call assert_true(len(g:save1.items) ># 1) 18 | for s:item in g:save1.items 19 | if s:item.type ==# g:save1.types['sourcing'] 20 | call assert_equal( 21 | \ [ 22 | \ 'event', 23 | \ 'finish', 24 | \ 'occurrence', 25 | \ 'self', 26 | \ 'self+sourced', 27 | \ 'start', 28 | \ 'time', 29 | \ 'tries', 30 | \ 'type' 31 | \ ], 32 | \ sort(copy(keys(s:item)))) 33 | call assert_equal(v:t_float, type(s:item['self+sourced'].mean)) 34 | call assert_equal(v:t_float, type(s:item['self'].mean)) 35 | call assert_false(isnan(s:item['self+sourced'].std)) 36 | call assert_false(isnan(s:item['self'].std)) 37 | call assert_equal(s:item['self+sourced'].mean, s:item['time']) 38 | elseif s:item.type ==# g:save1.types['other'] 39 | call assert_equal( 40 | \ [ 41 | \ 'elapsed', 42 | \ 'event', 43 | \ 'finish', 44 | \ 'occurrence', 45 | \ 'start', 46 | \ 'time', 47 | \ 'tries', 48 | \ 'type' 49 | \ ], 50 | \ sort(copy(keys(s:item)))) 51 | call assert_equal(v:t_float, type(s:item['elapsed'].mean)) 52 | call assert_false(isnan(s:item['elapsed'].std)) 53 | call assert_equal(s:item['elapsed'].mean, s:item['time']) 54 | else 55 | throw 'vim-startuptime: unknown type' 56 | endif 57 | call assert_equal(v:t_float, type(s:item['finish'].mean)) 58 | call assert_equal(v:t_float, type(s:item['start'].mean)) 59 | call assert_false(isnan(s:item['finish'].std)) 60 | call assert_false(isnan(s:item['start'].std)) 61 | call assert_equal(v:t_number, type(s:item['occurrence'])) 62 | call assert_equal(v:t_string, type(s:item['event'])) 63 | call assert_equal(3, s:item.tries) 64 | endfor 65 | unlet g:save1 66 | call assert_true(has_key(g:, 'autocmd')) 67 | call assert_equal(1, g:autocmd) 68 | unlet autocmd 69 | call assert_false(has_key(g:, 'autocmd')) 70 | " When using --hidden, there should be no new window. 71 | call assert_equal(1, winnr('$')) 72 | " Test for failure with --tries 0. 73 | let s:failed = v:false 74 | try 75 | StartupTime --tries 0 --hidden 76 | sleep 3 77 | catch 78 | let s:failed = v:true 79 | endtry 80 | call assert_true(s:failed) 81 | 82 | " ############################################### 83 | " # Test --save without --hidden. 84 | " ############################################### 85 | 86 | augroup test 87 | autocmd! 88 | autocmd User StartupTimeSaved let autocmd = 1 89 | augroup END 90 | StartupTime --save save2 91 | sleep 3 92 | call assert_true(has_key(g:, 'save2')) 93 | call assert_equal(['items', 'startup', 'types'], sort(copy(keys(g:save2)))) 94 | call assert_equal(v:t_float, type(g:save2.startup.mean)) 95 | call assert_equal({'sourcing': 0, 'other': 1}, g:save2.types) 96 | call assert_true(len(g:save2.items) ># 1) 97 | for s:item in g:save2.items 98 | if s:item.type ==# g:save2.types['sourcing'] 99 | call assert_equal( 100 | \ ['event', 'finish', 'occurrence', 'self', 'self+sourced', 'start', 'time', 'tries', 'type'], 101 | \ sort(copy(keys(s:item)))) 102 | call assert_equal(v:t_float, type(s:item['self+sourced'].mean)) 103 | call assert_equal(v:t_float, type(s:item['self'].mean)) 104 | call assert_true(isnan(s:item['self+sourced'].std)) 105 | call assert_true(isnan(s:item['self'].std)) 106 | call assert_equal(s:item['self+sourced'].mean, s:item['time']) 107 | elseif s:item.type ==# g:save2.types['other'] 108 | call assert_equal( 109 | \ ['elapsed', 'event', 'finish', 'occurrence', 'start', 'time', 'tries', 'type'], 110 | \ sort(copy(keys(s:item)))) 111 | call assert_equal(v:t_float, type(s:item['elapsed'].mean)) 112 | call assert_true(isnan(s:item['elapsed'].std)) 113 | call assert_equal(s:item['elapsed'].mean, s:item['time']) 114 | else 115 | throw 'vim-startuptime: unknown type' 116 | endif 117 | call assert_equal(v:t_float, type(s:item['finish'].mean)) 118 | call assert_equal(v:t_float, type(s:item['start'].mean)) 119 | call assert_true(isnan(s:item['finish'].std)) 120 | call assert_true(isnan(s:item['start'].std)) 121 | call assert_equal(v:t_number, type(s:item['occurrence'])) 122 | call assert_equal(v:t_string, type(s:item['event'])) 123 | call assert_equal(1, s:item.tries) 124 | endfor 125 | unlet g:save2 126 | call assert_true(has_key(g:, 'autocmd')) 127 | call assert_equal(1, g:autocmd) 128 | unlet autocmd 129 | call assert_false(has_key(g:, 'autocmd')) 130 | " Without hidden there should be a new window. 131 | call assert_equal(2, winnr('$')) 132 | close 133 | call assert_equal(1, winnr('$')) 134 | 135 | " ############################################### 136 | " # Test default call. 137 | " ############################################### 138 | 139 | augroup test 140 | autocmd! 141 | autocmd User StartupTimeSaved let autocmd = 1 142 | augroup END 143 | StartupTime 144 | sleep 3 145 | " Without --save, no autocmd should execute. 146 | call assert_false(has_key(g:, 'autocmd')) 147 | " Without hidden there should be a new window. 148 | call assert_equal(2, winnr('$')) 149 | close 150 | call assert_equal(1, winnr('$')) 151 | 152 | " ############################################### 153 | " # Ensure 'opening buffers' event. 154 | " ############################################### 155 | 156 | " We depend on the presence of 'opening buffers' in lua/startuptime.lua::remove_tui_sessions. 157 | 158 | StartupTime --hidden --save save3 159 | sleep 3 160 | call assert_true(has_key(g:, 'save3')) 161 | let s:has_opening_buffers = v:false 162 | for s:item in g:save3.items 163 | if s:item.event ==# 'opening buffers' 164 | let s:has_opening_buffers = v:false 165 | break 166 | endif 167 | endfor 168 | call assert_false(s:has_opening_buffers) 169 | 170 | " ############################################### 171 | " # Test --input-file. 172 | " ############################################### 173 | 174 | " Convert all NaNs to the specified value. 175 | function! s:ConvertNan(x, replacement) abort 176 | let l:type = type(a:x) 177 | if l:type ==# v:t_dict 178 | let l:dict = {} 179 | for l:key in keys(a:x) 180 | let l:dict[l:key] = s:ConvertNan(a:x[l:key], a:replacement) 181 | endfor 182 | return l:dict 183 | elseif l:type == v:t_list 184 | let l:list = [] 185 | for l:item in a:x 186 | call add(l:list, s:ConvertNan(l:item, a:replacement)) 187 | endfor 188 | return l:list 189 | elseif isnan(a:x) 190 | return a:replacement 191 | else 192 | return a:x 193 | endif 194 | endfunction 195 | 196 | " Generate a file to pass to --input-file. 197 | " XXX: A more thorough approach for using --startuptime is used in s:Profile 198 | " in autoload/startuptime.vim, to retain all events. 199 | let s:file = tempname() 200 | let s:exepath = exepath(v:progpath) 201 | silent execute '!' . s:exepath . ' --startuptime ' . s:file . ' +qall\!' 202 | 203 | try 204 | execute 'StartupTime --save save4 --input-file ' . s:file 205 | sleep 3 206 | execute 'StartupTime --save save5 --input-file ' . s:file 207 | sleep 3 208 | call assert_true(has_key(g:, 'save4')) 209 | call assert_true(has_key(g:, 'save5')) 210 | call assert_false(empty(g:save4.items)) 211 | " NaN doesn't compare as equal to NaN, so convert to -1. 212 | call assert_equal(s:ConvertNan(g:save4, -1), s:ConvertNan(g:save5, -1)) 213 | finally 214 | call delete(s:file) 215 | endtry 216 | --------------------------------------------------------------------------------