├── .codecov.yaml
├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .goreleaser.yaml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── go.mod
├── go.sum
├── main.go
├── main_test.go
├── measure.go
├── measure_test.go
├── measurement.go
├── measurement_test.go
├── parse.go
├── parse_test.go
├── vim.go
└── vim_test.go
/.codecov.yaml:
--------------------------------------------------------------------------------
1 | # https://docs.codecov.com/docs/commit-status#disabling-a-status
2 | coverage:
3 | status:
4 | project: off
5 | patch: off
6 |
7 | comment: false
8 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | unit-tests:
6 | name: Unit tests
7 | strategy:
8 | matrix:
9 | os: [ubuntu-latest, macos-latest, windows-latest]
10 | runs-on: ${{ matrix.os }}
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-go@v5
14 | with:
15 | go-version: '>=1.23.0'
16 | - uses: rhysd/action-setup-vim@v1
17 | - uses: rhysd/action-setup-vim@v1
18 | with:
19 | neovim: true
20 | - run: go build
21 | env:
22 | CGO_ENABLED: '0'
23 | - run: go test -v -race -coverprofile coverage.txt
24 | - name: Upload coverage to codecov
25 | uses: codecov/codecov-action@v4
26 | with:
27 | token: ${{ secrets.CODECOV_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags:
5 | - "v*.*.*"
6 |
7 | jobs:
8 | release:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: actions/setup-go@v5
13 | with:
14 | go-version: '>=1.23.0'
15 | - uses: goreleaser/goreleaser-action@v6
16 | with:
17 | version: '~> v2'
18 | args: release --clean
19 | env:
20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vim-startuptime
2 | /coverage.txt
3 | /release
4 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | release:
4 | draft: false
5 | prerelease: true
6 |
7 | builds:
8 | - id: macos
9 | goos: [darwin]
10 | goarch: [amd64, arm64]
11 | env:
12 | - CGO_ENABLED=0
13 |
14 | - id: linux
15 | goos: [linux]
16 | goarch: [386, arm, amd64, arm64]
17 | env:
18 | - CGO_ENABLED=0
19 |
20 | - id: windows
21 | goos: [windows]
22 | goarch: [amd64, arm64]
23 | env:
24 | - CGO_ENABLED=0
25 |
26 | - id: freebsd
27 | goos: [freebsd]
28 | goarch: [386, amd64]
29 | env:
30 | - CGO_ENABLED=0
31 |
32 | archives:
33 | - <<: &archives_defaults
34 | files:
35 | - README.md
36 | - LICENSE
37 | id: nix
38 | builds: [macos, linux, freebsd]
39 | format: tar.gz
40 | - <<: *archives_defaults
41 | id: windows
42 | builds: [windows]
43 | format: zip
44 |
45 | changelog:
46 | disable: true
47 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # [v1.3.2](https://github.com/rhysd/vim-startuptime/releases/tag/v1.3.2) - 12 Feb 2024
3 |
4 | - Fix a parse error when using `vim-startuptime` with https://github.com/neovim/neovim/commit/8e739af064dec28886694aa448f60a570acd2173 or later. ([#4](https://github.com/rhysd/vim-startuptime/issues/4))
5 | - The parser was broken by [the recent Neovim's change](https://github.com/neovim/neovim/pull/26790 to `--startuptime` output format).
6 |
7 | [Changes][v1.3.2]
8 |
9 |
10 |
11 | # [v1.3.1](https://github.com/rhysd/vim-startuptime/releases/tag/v1.3.1) - 06 Dec 2022
12 |
13 | - Fix Vim does not exit on Windows by adding `-e` option (thanks [@mattn](https://github.com/mattn), [#3](https://github.com/rhysd/vim-startuptime/issues/3))
14 | - Replace newlines in error messages with spaces and avoid capitalizing error messages following the Go's error message convention
15 | - Remove deprecated `io/util` imports
16 |
17 | [Changes][v1.3.1]
18 |
19 |
20 |
21 | # [v1.3.0](https://github.com/rhysd/vim-startuptime/releases/tag/v1.3.0) - 11 Jul 2022
22 |
23 | - Support Lua `require('...')` as script source. It was recently [introduced to Neovim](https://github.com/neovim/neovim/pull/19267). ([#2](https://github.com/rhysd/vim-startuptime/issues/2))
24 |
25 | [Changes][v1.3.0]
26 |
27 |
28 |
29 | # [v1.2.0](https://github.com/rhysd/vim-startuptime/releases/tag/v1.2.0) - 03 Apr 2022
30 |
31 | - Add `-verbose` flag to output the progress of measurements to stderr
32 | - Add support for [LunarVim](https://www.lunarvim.org/) (thanks [@runar-rkmedia](https://github.com/runar-rkmedia), [#1](https://github.com/rhysd/vim-startuptime/issues/1))
33 | - Build binaries with the latest Go compiler 1.18.0
34 |
35 | [Changes][v1.2.0]
36 |
37 |
38 |
39 | # [v1.1.0](https://github.com/rhysd/vim-startuptime/releases/tag/v1.1.0) - 12 Feb 2020
40 |
41 | - New: `-warmup {count}` option was added for warm-up phase. Before collecting the results, `vim-startuptime` runs the Vim/Neovim process `{count}` times
42 | - Dev: Migrate CI to GitHub Actions
43 |
44 | [Changes][v1.1.0]
45 |
46 |
47 |
48 | # [v1.0.0](https://github.com/rhysd/vim-startuptime/releases/tag/v1.0.0) - 06 Jun 2018
49 |
50 | First release :tada:
51 |
52 | [Changes][v1.0.0]
53 |
54 |
55 | [v1.3.2]: https://github.com/rhysd/vim-startuptime/compare/v1.3.1...v1.3.2
56 | [v1.3.1]: https://github.com/rhysd/vim-startuptime/compare/v1.3.0...v1.3.1
57 | [v1.3.0]: https://github.com/rhysd/vim-startuptime/compare/v1.2.0...v1.3.0
58 | [v1.2.0]: https://github.com/rhysd/vim-startuptime/compare/v1.1.0...v1.2.0
59 | [v1.1.0]: https://github.com/rhysd/vim-startuptime/compare/v1.0.0...v1.1.0
60 | [v1.0.0]: https://github.com/rhysd/vim-startuptime/tree/v1.0.0
61 |
62 |
63 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | the MIT License
2 |
3 | Copyright (c) 2018 rhysd
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 copies
9 | of the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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 IMPLIED,
16 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
17 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
20 | THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Better `vim --startuptime`
2 | ==========================
3 | [![CI Badge][]][CI]
4 | [![Codecov Status][]][Codecov]
5 |
6 | `vim-startuptime` is a small Go program to measure startup time of Vim or Neovim. This program aims
7 | to be an alternative of `--startuptime` option of Vim, which measures the startup time metrics to
8 | allow vimmers to optimize Vim's startup.
9 |
10 | After warm-up, `vim-startuptime` runs `vim --startuptime` multiple times internally and collects the
11 | metrics from the results (e.g. average time for loading each plugin's scripts). The summary calculated
12 | from the measurements is output to stdout.
13 |
14 | Tested on Linux, Mac and Windows with both Vim and Neovim.
15 |
16 |
17 |
18 | ## Installation
19 |
20 | Download an executable from [a release page](https://github.com/rhysd/vim-startuptime/releases).
21 |
22 | If you want to install the latest stable version, please run following command. Go toolchain is
23 | necessary for running the command.
24 |
25 | ```sh
26 | go install github.com/rhysd/vim-startuptime@latest
27 | ```
28 |
29 |
30 |
31 | ## Requirements
32 |
33 | - `vim` 7.4.1444 or later (for `--not-a-term` startup option)
34 | - `nvim`
35 |
36 |
37 |
38 | ## Usage
39 |
40 | Just run the command with no argument.
41 |
42 | ```sh
43 | vim-startuptime
44 | ```
45 |
46 | By default, it tries to run `vim` and `:quit` immediately 10 times, collects the results and outputs
47 | a summary of them to stdout.
48 |
49 | ```
50 | Extra options: []
51 | Measured: 10 times
52 |
53 | Total Average: 189.954400 msec
54 | Total Max: 198.062000 msec
55 | Total Min: 183.966000 msec
56 |
57 | AVERAGE MAX MIN
58 | -------------------------------
59 | 98.532900 102.605000 94.275000: $HOME/.vimrc
60 | 51.859600 56.937000 49.897000: opening buffers
61 | 17.027900 18.810000 16.277000: /Users/rhysd/.vim/bundle/vim-color-spring-night/colors/spring-night.vim
62 | 11.878900 13.153000 10.567000: /Users/rhysd/.vim/bundle/vim-smartinput/autoload/smartinput.vim
63 | 9.407600 11.710000 8.606000: /usr/local/Cellar/macvim/HEAD-0db36ff_1/MacVim.app/Contents/Resources/vim/runtime/filetype.vim
64 |
65 | ...(snip)
66 |
67 | 0.009100 0.012000 0.007000: window checked
68 | 0.009000 0.012000 0.008000: inits 3
69 | 0.003000 0.005000 0.002000: clipboard setup
70 | 0.002600 0.004000 0.002000: editing files in windows
71 | ```
72 |
73 | If you want to run with Neovim, please specify `-vimpath` option as follows:
74 |
75 | ```
76 | $ vim-startuptime -vimpath nvim
77 | ```
78 |
79 | If you want to give some options or arguments to underlying `vim` command executions, please specify
80 | them after `--` argument in command line as follows:
81 |
82 | ```
83 | $ vim-startuptime -- --cmd DoSomeCommand path/to/some/file
84 | ```
85 |
86 | Please see `-help` option to know the command options.
87 |
88 |
89 |
90 | ## What's Next after running `vim-startuptime`?
91 |
92 | By running `vim-startuptime`, now you know which script file takes time to run. What you should do
93 | next is `:profile`.
94 |
95 | ```sh
96 | vim --cmd 'profile start profile.log' --cmd 'profile! file /path/to/slow_script.vim' -c quit
97 | ```
98 |
99 | Profiled results are dumped to `profile.log`. Please check it. In log file, `:set ft=vim` would help
100 | you analyze the results.
101 | Please see `:help profile` for more details.
102 |
103 |
104 |
105 | ## (Maybe) TODO
106 |
107 | - Add more metrics like median
108 | - Temporarily isolate CPU for running Vim if possible
109 |
110 |
111 |
112 | ## License
113 |
114 | Distributed under [the MIT License](./LICENSE).
115 |
116 |
117 |
118 | [CI Badge]: https://github.com/rhysd/vim-startuptime/actions/workflows/ci.yml/badge.svg
119 | [CI]: https://github.com/rhysd/vim-startuptime/actions/workflows/ci.yml
120 | [Codecov Status]: https://codecov.io/gh/rhysd/vim-startuptime/branch/master/graph/badge.svg
121 | [Codecov]: https://codecov.io/gh/rhysd/vim-startuptime
122 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/rhysd/vim-startuptime
2 |
3 | go 1.16
4 |
5 | require github.com/google/go-cmp v0.6.0
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io"
7 | "os"
8 | )
9 |
10 | type options struct {
11 | count uint
12 | vimPath string
13 | script bool
14 | extraArgs []string
15 | warmup uint
16 | verbose bool
17 | }
18 |
19 | const usageHeader = `Usage: vim-startuptime [flags] [-- VIMARGS...]
20 |
21 | vim-startuptime is a command which provides better --startuptime option of Vim
22 | or Neovim. It starts Vim with --startuptime multiple times, collects the
23 | results and outputs summary of the measurements to stdout.
24 |
25 | Flags:`
26 |
27 | func parseOptions(out io.Writer, args []string) (*options, int) {
28 | fs := flag.NewFlagSet(args[0], flag.ContinueOnError)
29 | fs.SetOutput(out)
30 |
31 | o := &options{}
32 | fs.UintVar(&o.count, "count", 10, "How many times measure startup time")
33 | fs.StringVar(&o.vimPath, "vimpath", "vim", "Command to run Vim or Neovim")
34 | fs.BoolVar(&o.script, "script", false, "Only collects script loading times")
35 | fs.UintVar(&o.warmup, "warmup", 1, "How many times start Vim at warm-up phase")
36 | fs.BoolVar(&o.verbose, "verbose", false, "Verbose output to stderr while measurements")
37 | fs.Usage = func() {
38 | fmt.Fprintln(out, usageHeader)
39 | fs.PrintDefaults()
40 | }
41 |
42 | if err := fs.Parse(args[1:]); err != nil {
43 | if err == flag.ErrHelp {
44 | return nil, 0
45 | }
46 | fmt.Fprintf(out, "error while parsing command line arguments: %s\n", err)
47 | return nil, 1
48 | }
49 |
50 | o.extraArgs = fs.Args()
51 | return o, -1
52 | }
53 |
54 | func measure(opts *options, out io.Writer) error {
55 | collected, err := collectMeasurements(opts)
56 | if err != nil {
57 | return err
58 | }
59 |
60 | summary, err := summarizeStartuptime(collected, opts.verbose)
61 | if err != nil {
62 | return err
63 | }
64 |
65 | fmt.Fprintf(out, "Extra options: %v\n", opts.extraArgs)
66 | fmt.Fprintf(out, "Measured: %d times\n\n", opts.count)
67 | summary.print(out)
68 | return nil
69 | }
70 |
71 | func main() {
72 | opts, code := parseOptions(os.Stderr, os.Args)
73 | if code >= 0 {
74 | os.Exit(code)
75 | }
76 |
77 | if err := measure(opts, os.Stdout); err != nil {
78 | fmt.Fprintln(os.Stderr, err)
79 | os.Exit(1)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/google/go-cmp/cmp"
10 | )
11 |
12 | func TestParseOptionsOK(t *testing.T) {
13 | var b bytes.Buffer
14 |
15 | o, s := parseOptions(&b, []string{"vim-startuptime", "-count", "3", "-vimpath", "nvim", "-script", "-warmup", "5", "-verbose", "--", "--foo"})
16 | if s >= 0 {
17 | t.Fatal("unexpected exit with status:", s)
18 | }
19 |
20 | want := &options{
21 | count: 3,
22 | vimPath: "nvim",
23 | script: true,
24 | extraArgs: []string{"--foo"},
25 | warmup: 5,
26 | verbose: true,
27 | }
28 |
29 | if !cmp.Equal(o, want, cmp.AllowUnexported(options{})) {
30 | t.Fatal(cmp.Diff(o, want, cmp.AllowUnexported(options{})))
31 | }
32 |
33 | stderr := b.String()
34 | if stderr != "" {
35 | t.Fatalf("Unexpected stderr output %q", stderr)
36 | }
37 | }
38 |
39 | func TestParseOptionsUnknownFlag(t *testing.T) {
40 | var b bytes.Buffer
41 |
42 | _, s := parseOptions(&b, []string{"vim-startuptime", "-foo"})
43 | if s <= 0 {
44 | t.Fatal("unexpected status:", s)
45 | }
46 |
47 | stderr := b.String()
48 | if !strings.Contains(stderr, "flag provided but not defined: -foo") {
49 | t.Fatal("unexpected error output to stderr:", stderr)
50 | }
51 | }
52 |
53 | func TestParseOptionsHelpOutput(t *testing.T) {
54 | var b bytes.Buffer
55 |
56 | _, s := parseOptions(&b, []string{"vim-startuptime", "-help"})
57 | if s != 0 {
58 | t.Fatal("unexpected status:", s)
59 | }
60 |
61 | stderr := b.String()
62 | if !strings.HasPrefix(stderr, "Usage: vim-startuptime [flags] [-- VIMARGS...]") {
63 | t.Fatal("unexpected help output to stderr:", stderr)
64 | }
65 | }
66 |
67 | func TestRunMeasurementMainOK(t *testing.T) {
68 | o := &options{
69 | count: 1,
70 | vimPath: "vim",
71 | script: false,
72 | extraArgs: []string{},
73 | warmup: 0,
74 | verbose: false,
75 | }
76 |
77 | var b bytes.Buffer
78 | if err := measure(o, &b); err != nil {
79 | t.Fatal(err)
80 | }
81 |
82 | out := b.String()
83 | for _, want := range []string{
84 | "Extra options: []",
85 | "Measured: 1 times",
86 | "Total Average:",
87 | "Total Max:",
88 | "Total Min:",
89 | } {
90 | if !strings.Contains(out, want) {
91 | t.Errorf("output should contain %q but got %q", want, out)
92 | }
93 | }
94 | }
95 |
96 | func TestErrorWhileRunningMain(t *testing.T) {
97 | o := &options{
98 | count: 1,
99 | vimPath: "this-executable-does-not-exist",
100 | script: false,
101 | extraArgs: []string{},
102 | warmup: 0,
103 | verbose: false,
104 | }
105 |
106 | err := measure(o, io.Discard)
107 | if !strings.Contains(err.Error(), `failed to run "this-executable-does-not-exist"`) {
108 | t.Fatal("error was unexpected:", err)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/measure.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "sort"
7 | "time"
8 | )
9 |
10 | type collectedMeasurements struct {
11 | total []time.Duration
12 | entries map[string][]time.Duration
13 | }
14 |
15 | func collectMeasurements(opts *options) (*collectedMeasurements, error) {
16 | if opts.verbose {
17 | fmt.Fprintf(os.Stderr, "Running warm-up phase %d times\n", opts.warmup)
18 | }
19 | for i := uint(0); i < opts.warmup; i++ {
20 | if err := runVim(opts.vimPath, opts.extraArgs); err != nil {
21 | return nil, fmt.Errorf("error while warm-up: %w", err)
22 | }
23 | }
24 |
25 | dir, err := os.MkdirTemp("", "__vim_startuptime_")
26 | if err != nil {
27 | return nil, fmt.Errorf("failed to make a temporary directory: %w", err)
28 | }
29 | defer os.RemoveAll(dir)
30 |
31 | if opts.verbose {
32 | fmt.Fprintf(os.Stderr, "Running %q with arguments %v %d times at %q\n", opts.vimPath, opts.extraArgs, opts.count, dir)
33 | }
34 | collected := &collectedMeasurements{entries: map[string][]time.Duration{}}
35 | for id := 0; id < int(opts.count); id++ {
36 | f, err := runVimStartuptime(opts.vimPath, dir, id, opts.extraArgs)
37 | if err != nil {
38 | return nil, err
39 | }
40 | m, err := parseStartuptime(f)
41 | if err != nil {
42 | f.Close()
43 | return nil, err
44 | }
45 | collected.total = append(collected.total, m.elapsedTotal)
46 | for _, e := range m.entries {
47 | if !opts.script || e.script {
48 | collected.entries[e.name] = append(collected.entries[e.name], e.total)
49 | }
50 | }
51 | f.Close()
52 | }
53 |
54 | return collected, nil
55 | }
56 |
57 | func summarizeEntry(name string, ds []time.Duration) entrySummary {
58 | total := time.Duration(0)
59 | min := ds[0]
60 | max := ds[0]
61 | for _, d := range ds {
62 | total += d
63 | if d < min {
64 | min = d
65 | }
66 | if d > max {
67 | max = d
68 | }
69 | }
70 | average := time.Duration(total.Nanoseconds()/int64(len(ds))) * time.Nanosecond
71 | return entrySummary{name, average, max, min}
72 | }
73 |
74 | func summarizeStartuptime(collected *collectedMeasurements, verbose bool) (*measurementSummary, error) {
75 | if verbose {
76 | fmt.Fprintf(os.Stderr, "Calculating summary of collected %d results\n", len(collected.entries))
77 | }
78 |
79 | summary := &measurementSummary{}
80 | summary.sortedEntries = make([]entrySummary, 0, len(collected.entries))
81 | if len(collected.total) == 0 {
82 | return nil, fmt.Errorf("no total time was collected")
83 | }
84 | summary.total = summarizeEntry("Total", collected.total)
85 | for n, ds := range collected.entries {
86 | if len(ds) == 0 {
87 | return nil, fmt.Errorf("no profile was collected for '%s'", n)
88 | }
89 | summary.sortedEntries = append(summary.sortedEntries, summarizeEntry(n, ds))
90 | }
91 |
92 | // Sort in decending order by duration
93 | sort.Slice(summary.sortedEntries, func(i, j int) bool {
94 | return summary.sortedEntries[i].average > summary.sortedEntries[j].average
95 | })
96 |
97 | if verbose {
98 | fmt.Fprintln(os.Stderr, "Calculated summary. Printing...")
99 | }
100 | return summary, nil
101 | }
102 |
--------------------------------------------------------------------------------
/measure_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strings"
5 | "testing"
6 | "time"
7 |
8 | "github.com/google/go-cmp/cmp"
9 | )
10 |
11 | var measureCmpOpts = cmp.AllowUnexported(collectedMeasurements{}, measurementSummary{}, entrySummary{})
12 |
13 | func TestCollectMeasurementsOK(t *testing.T) {
14 | for _, path := range []string{"vim", "nvim"} {
15 | t.Run(path, func(t *testing.T) {
16 | opts := &options{
17 | count: 2,
18 | vimPath: path,
19 | warmup: 1,
20 | extraArgs: []string{"-N", "-u", "NONE"},
21 | }
22 | collected, err := collectMeasurements(opts)
23 | if err != nil {
24 | t.Fatal(err)
25 | }
26 | if len(collected.total) != 2 {
27 | t.Error("2 total times should be collected but", len(collected.total))
28 | }
29 | if len(collected.entries) == 0 {
30 | t.Error("Collected entries are empty")
31 | }
32 | for s, ds := range collected.entries {
33 | if len(ds) < 2 {
34 | t.Error("Source time for", s, " should be collected twice but", ds)
35 | }
36 | }
37 | })
38 | }
39 | }
40 |
41 | func TestCollectMeasurementsVimStartError(t *testing.T) {
42 | for _, path := range []string{"vim", "nvim"} {
43 | t.Run(path, func(t *testing.T) {
44 | opts := &options{count: 2, vimPath: path, extraArgs: []string{"--foo"}}
45 | _, err := collectMeasurements(opts)
46 | if err == nil {
47 | t.Fatal("No error occurred")
48 | }
49 | if !strings.Contains(err.Error(), "failed to run \""+path+"\" with args [--foo") {
50 | t.Fatal("Unexpected error:", err.Error())
51 | }
52 | })
53 | }
54 | }
55 |
56 | func TestCollectMeasurementsVimWarmupError(t *testing.T) {
57 | opts := &options{warmup: 1, count: 1, vimPath: "vim", extraArgs: []string{"--foo"}}
58 | _, err := collectMeasurements(opts)
59 | if err == nil {
60 | t.Fatal("No error occurred")
61 | }
62 | if !strings.Contains(err.Error(), "error while warm-up") {
63 | t.Fatal("Unexpected error:", err.Error())
64 | }
65 | }
66 |
67 | func TestSummarizeStartuptime(t *testing.T) {
68 | collected := &collectedMeasurements{
69 | total: []time.Duration{
70 | time.Duration(10000),
71 | time.Duration(20000),
72 | time.Duration(30000),
73 | },
74 | entries: map[string][]time.Duration{
75 | "/foo/bar": {
76 | time.Duration(110),
77 | time.Duration(130),
78 | time.Duration(120),
79 | },
80 | "$VIM/vimrc": {
81 | time.Duration(1500),
82 | time.Duration(1400),
83 | time.Duration(1300),
84 | },
85 | },
86 | }
87 | want := &measurementSummary{
88 | total: entrySummary{
89 | "Total",
90 | time.Duration(20000),
91 | time.Duration(30000),
92 | time.Duration(10000),
93 | },
94 | sortedEntries: []entrySummary{
95 | {
96 | "$VIM/vimrc",
97 | time.Duration(1400),
98 | time.Duration(1500),
99 | time.Duration(1300),
100 | },
101 | {
102 | "/foo/bar",
103 | time.Duration(120),
104 | time.Duration(130),
105 | time.Duration(110),
106 | },
107 | },
108 | }
109 |
110 | have, err := summarizeStartuptime(collected, false)
111 | if err != nil {
112 | t.Fatal(err)
113 | }
114 | if !cmp.Equal(want, have, measureCmpOpts) {
115 | t.Fatal(cmp.Diff(want, have, measureCmpOpts))
116 | }
117 | }
118 |
119 | func TestSummarizeStartuptimeError(t *testing.T) {
120 | for _, tc := range []struct {
121 | what string
122 | collected *collectedMeasurements
123 | msg string
124 | }{
125 | {
126 | "no total time",
127 | &collectedMeasurements{
128 | total: []time.Duration{},
129 | entries: map[string][]time.Duration{
130 | "/foo/bar": {time.Duration(110)},
131 | },
132 | },
133 | "no total time was collected",
134 | },
135 | {
136 | "no entry profile result",
137 | &collectedMeasurements{
138 | total: []time.Duration{time.Duration(110)},
139 | entries: map[string][]time.Duration{
140 | "$VIM/vimrc": {},
141 | },
142 | },
143 | "no profile was collected for '$VIM/vimrc'",
144 | },
145 | } {
146 | t.Run(tc.what, func(t *testing.T) {
147 | _, err := summarizeStartuptime(tc.collected, false)
148 | if err == nil {
149 | t.Fatal("Error should happen")
150 | }
151 | msg := err.Error()
152 | if !strings.Contains(msg, tc.msg) {
153 | t.Fatalf("Unexpected error '%s', it should contain '%s'", msg, tc.msg)
154 | }
155 | })
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/measurement.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "math"
7 | "strings"
8 | "time"
9 | )
10 |
11 | type measurementEntry struct {
12 | script bool
13 | elapsed time.Duration
14 | total time.Duration
15 | self time.Duration
16 | name string
17 | }
18 |
19 | type measurement struct {
20 | elapsedTotal time.Duration
21 | entries []*measurementEntry
22 | }
23 |
24 | type entrySummary struct {
25 | name string
26 | average time.Duration
27 | max time.Duration
28 | min time.Duration
29 | }
30 |
31 | type measurementSummary struct {
32 | total entrySummary
33 | sortedEntries []entrySummary
34 | }
35 |
36 | func alignFloatColumn(data []float64, header string) []string {
37 | intPart := 0
38 | for _, f := range data {
39 | d := int(math.Log10(f)) + 1
40 | if d > intPart {
41 | intPart = d
42 | }
43 | }
44 | width := intPart + 1 /*'.'*/ + 6 /*decimal part*/
45 |
46 | aligned := make([]string, 0, len(data)+1)
47 |
48 | if len(header) < width {
49 | header = fmt.Sprintf("%s%s", strings.Repeat(" ", width-len(header)), header)
50 | }
51 | aligned = append(aligned, header)
52 |
53 | for _, f := range data {
54 | s := fmt.Sprintf("%.6f", f)
55 | if len(s) < width {
56 | s = fmt.Sprintf("%s%s", strings.Repeat(" ", width-len(s)), s)
57 | }
58 | aligned = append(aligned, s)
59 | }
60 |
61 | return aligned
62 | }
63 |
64 | func (summary *measurementSummary) print(w io.Writer) {
65 | fmt.Fprintf(w, "Total Average: %f msec\n", summary.total.average.Seconds()*1000)
66 | fmt.Fprintf(w, "Total Max: %f msec\n", summary.total.max.Seconds()*1000)
67 | fmt.Fprintf(w, "Total Min: %f msec\n\n", summary.total.min.Seconds()*1000)
68 |
69 | averages := make([]float64, 0, len(summary.sortedEntries))
70 | maxes := make([]float64, 0, len(summary.sortedEntries))
71 | mins := make([]float64, 0, len(summary.sortedEntries))
72 | for _, e := range summary.sortedEntries {
73 | averages = append(averages, e.average.Seconds()*1000)
74 | maxes = append(maxes, e.max.Seconds()*1000)
75 | mins = append(mins, e.min.Seconds()*1000)
76 | }
77 | averageColumn := alignFloatColumn(averages, "AVERAGE")
78 | maxColumn := alignFloatColumn(maxes, "MAX")
79 | minColumn := alignFloatColumn(mins, "MIN")
80 |
81 | fmt.Fprintf(w, "%s %s %s\n", averageColumn[0], maxColumn[0], minColumn[0])
82 | fmt.Fprintln(w, strings.Repeat("-", len(averageColumn[0])+1+len(maxColumn[0])+1+len(minColumn[0])+1))
83 | for i, e := range summary.sortedEntries {
84 | fmt.Fprintf(w, "%s %s %s: %s\n", averageColumn[i+1], maxColumn[i+1], minColumn[i+1], e.name)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/measurement_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "strings"
6 | "testing"
7 | "time"
8 |
9 | "github.com/google/go-cmp/cmp"
10 | )
11 |
12 | func TestPrintSummary(t *testing.T) {
13 | for _, tc := range []struct {
14 | what string
15 | summary *measurementSummary
16 | want []string
17 | }{
18 | {
19 | "different number of digits in entries",
20 | &measurementSummary{
21 | entrySummary{
22 | "Total",
23 | time.Duration(200) * time.Millisecond,
24 | time.Duration(300) * time.Millisecond,
25 | time.Duration(100) * time.Millisecond,
26 | },
27 | []entrySummary{
28 | {
29 | "/foo/bar.vim",
30 | time.Duration(12345) * time.Microsecond,
31 | time.Duration(13334) * time.Microsecond,
32 | time.Duration(11112) * time.Microsecond,
33 | },
34 | {
35 | "$VIM/vimrc",
36 | time.Duration(1234) * time.Microsecond,
37 | time.Duration(1334) * time.Microsecond,
38 | time.Duration(1112) * time.Microsecond,
39 | },
40 | },
41 | },
42 | []string{
43 | "Total Average: 200.000000 msec",
44 | "Total Max: 300.000000 msec",
45 | "Total Min: 100.000000 msec",
46 | "",
47 | " AVERAGE MAX MIN",
48 | "------------------------------",
49 | "12.345000 13.334000 11.112000: /foo/bar.vim",
50 | " 1.234000 1.334000 1.112000: $VIM/vimrc",
51 | },
52 | },
53 | {
54 | "same number of digits in entries",
55 | &measurementSummary{
56 | entrySummary{
57 | "Total",
58 | time.Duration(200) * time.Millisecond,
59 | time.Duration(1000) * time.Millisecond,
60 | time.Duration(10) * time.Millisecond,
61 | },
62 | []entrySummary{
63 | {
64 | "/foo/bar.vim",
65 | time.Duration(5678) * time.Microsecond,
66 | time.Duration(7890) * time.Microsecond,
67 | time.Duration(1234) * time.Microsecond,
68 | },
69 | {
70 | "$VIM/vimrc",
71 | time.Duration(1234) * time.Microsecond,
72 | time.Duration(2345) * time.Microsecond,
73 | time.Duration(1000) * time.Microsecond,
74 | },
75 | },
76 | },
77 | []string{
78 | "Total Average: 200.000000 msec",
79 | "Total Max: 1000.000000 msec",
80 | "Total Min: 10.000000 msec",
81 | "",
82 | " AVERAGE MAX MIN",
83 | "---------------------------",
84 | "5.678000 7.890000 1.234000: /foo/bar.vim",
85 | "1.234000 2.345000 1.000000: $VIM/vimrc",
86 | },
87 | },
88 | } {
89 | t.Run(tc.what, func(t *testing.T) {
90 | var buf bytes.Buffer
91 | tc.summary.print(&buf)
92 | lines := strings.Split(buf.String(), "\n")
93 |
94 | if lines[len(lines)-1] != "" {
95 | t.Error("Output does not end with newline")
96 | }
97 | lines = lines[:len(lines)-1]
98 |
99 | if !cmp.Equal(lines, tc.want) {
100 | t.Fatal(cmp.Diff(lines, tc.want))
101 | }
102 | })
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/parse.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "os"
7 | "strconv"
8 | "strings"
9 | "time"
10 | )
11 |
12 | func parseErrorAt(lineno uint, format string, args ...interface{}) error {
13 | s := fmt.Sprintf("parse error at line:%d: %s", lineno, format)
14 | return fmt.Errorf(s, args...)
15 | }
16 |
17 | func parseDuration(s string, lineno uint) (time.Duration, error) {
18 | s = strings.TrimSuffix(s, ":")
19 | f, err := strconv.ParseFloat(s, 64)
20 | if err != nil {
21 | return time.Duration(0), parseErrorAt(lineno, "cannot parse field '%s' as millisec duration", s)
22 | }
23 | return time.Duration(f*1000) * time.Microsecond, nil
24 | }
25 |
26 | func parseStartuptimeEntity(line string, lineno uint) (*measurementEntry, error) {
27 | e := &measurementEntry{}
28 |
29 | ss := strings.Fields(line)
30 | if len(ss) <= 2 {
31 | return nil, parseErrorAt(lineno, "lack of fields: '%s'", line)
32 | }
33 |
34 | d, err := parseDuration(ss[0], lineno)
35 | if err != nil {
36 | return nil, err
37 | }
38 | e.elapsed = d
39 |
40 | d, err = parseDuration(ss[1], lineno)
41 | if err != nil {
42 | return nil, err
43 | }
44 | e.total = d
45 |
46 | if strings.HasSuffix(ss[1], ":") {
47 | e.script = false
48 | e.self = time.Duration(0)
49 | e.name = strings.Join(ss[2:], " ")
50 | return e, nil
51 | }
52 |
53 | e.script = true
54 | if len(ss) < 4 {
55 | return nil, parseErrorAt(lineno, "failed to parse script measurement line '%s'. too few fields", line)
56 | }
57 |
58 | d, err = parseDuration(ss[2], lineno)
59 | if err != nil {
60 | return nil, err
61 | }
62 | e.self = d
63 |
64 | if ss[3] == "sourcing" {
65 | e.name = strings.Join(ss[4:], " ")
66 | if e.name == "" {
67 | return nil, parseErrorAt(lineno, "failed to parse script measurement line '%s'. script name is missing", line)
68 | }
69 | } else if strings.HasPrefix(ss[3], "require(") {
70 | e.name = strings.Join(ss[3:], " ")
71 | } else {
72 | return nil, parseErrorAt(lineno, "'sourcing' token or 'require(...)' token is expected but got '%s'", ss[3])
73 | }
74 |
75 | return e, nil
76 | }
77 |
78 | func parseStartuptime(file *os.File) (*measurement, error) {
79 | m := &measurement{}
80 |
81 | s := bufio.NewScanner(file)
82 | l := uint(1)
83 | for s.Scan() {
84 | if l < 7 {
85 | // Skip header
86 | l++
87 | continue
88 | }
89 | t := s.Text()
90 | if t == "" {
91 | // Neovim appends an extra empty line at the end of input (#4)
92 | continue
93 | }
94 | e, err := parseStartuptimeEntity(t, l)
95 | if err != nil {
96 | return nil, err
97 | }
98 | m.entries = append(m.entries, e)
99 | l++
100 | }
101 |
102 | if len(m.entries) == 0 {
103 | return nil, fmt.Errorf("broken --startuptime output while parsing file %s. no entry was parsed", file.Name())
104 | }
105 | m.elapsedTotal = m.entries[len(m.entries)-1].elapsed
106 |
107 | return m, nil
108 | }
109 |
--------------------------------------------------------------------------------
/parse_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 | "testing"
8 | "time"
9 |
10 | "github.com/google/go-cmp/cmp"
11 | )
12 |
13 | var parseCmpOpts = cmp.AllowUnexported(measurement{}, measurementEntry{})
14 |
15 | func TestParseOK(t *testing.T) {
16 | tmpfile, err := os.CreateTemp("", "__test_parse_ok")
17 | if err != nil {
18 | panic(err)
19 | }
20 | tmpname := tmpfile.Name()
21 | tmpfile.Close()
22 | defer func() {
23 | os.Remove(tmpname)
24 | }()
25 |
26 | header := `
27 |
28 | times in msec
29 | clock self+sourced self: sourced script
30 | clock elapsed: other lines
31 |
32 | `
33 |
34 | for _, tc := range []struct {
35 | what string
36 | lines []string
37 | expected *measurement
38 | }{
39 | {
40 | "system only",
41 | []string{
42 | "000.008 000.008: --- VIM STARTING ---",
43 | "000.190 000.182: Allocated generic buffers",
44 | },
45 | &measurement{
46 | time.Duration(190) * time.Microsecond,
47 | []*measurementEntry{
48 | {
49 | false,
50 | time.Duration(8) * time.Microsecond,
51 | time.Duration(8) * time.Microsecond,
52 | time.Duration(0),
53 | "--- VIM STARTING ---",
54 | },
55 | {
56 | false,
57 | time.Duration(190) * time.Microsecond,
58 | time.Duration(182) * time.Microsecond,
59 | time.Duration(0),
60 | "Allocated generic buffers",
61 | },
62 | },
63 | },
64 | },
65 | {
66 | "script only",
67 | []string{
68 | "012.696 000.429 000.429: sourcing /foo/bar.vim",
69 | "013.270 001.106 000.677: sourcing $VIM/vimrc",
70 | },
71 | &measurement{
72 | time.Duration(13270) * time.Microsecond,
73 | []*measurementEntry{
74 | {
75 | true,
76 | time.Duration(12696) * time.Microsecond,
77 | time.Duration(429) * time.Microsecond,
78 | time.Duration(429) * time.Microsecond,
79 | "/foo/bar.vim",
80 | },
81 | {
82 | true,
83 | time.Duration(13270) * time.Microsecond,
84 | time.Duration(1106) * time.Microsecond,
85 | time.Duration(677) * time.Microsecond,
86 | "$VIM/vimrc",
87 | },
88 | },
89 | },
90 | },
91 | {
92 | "mixed",
93 | []string{
94 | "198.369 000.161 000.161: sourcing /foo/bar.vim",
95 | "198.465 001.679: BufEnter autocommands",
96 | "198.467 000.002: editing files in windows",
97 | "200.107 001.135 001.135: sourcing $VIM/vimrc",
98 | },
99 | &measurement{
100 | time.Duration(200107) * time.Microsecond,
101 | []*measurementEntry{
102 | {
103 | true,
104 | time.Duration(198369) * time.Microsecond,
105 | time.Duration(161) * time.Microsecond,
106 | time.Duration(161) * time.Microsecond,
107 | "/foo/bar.vim",
108 | },
109 | {
110 | false,
111 | time.Duration(198465) * time.Microsecond,
112 | time.Duration(1679) * time.Microsecond,
113 | time.Duration(0),
114 | "BufEnter autocommands",
115 | },
116 | {
117 | false,
118 | time.Duration(198467) * time.Microsecond,
119 | time.Duration(2) * time.Microsecond,
120 | time.Duration(0),
121 | "editing files in windows",
122 | },
123 | {
124 | true,
125 | time.Duration(200107) * time.Microsecond,
126 | time.Duration(1135) * time.Microsecond,
127 | time.Duration(1135) * time.Microsecond,
128 | "$VIM/vimrc",
129 | },
130 | },
131 | },
132 | },
133 | {
134 | "lua for neovim (#2)",
135 | []string{
136 | "021.963 000.003: parsing arguments",
137 | "023.515 000.278 000.278: require('vim.shared')",
138 | "023.699 000.101 000.101: require('vim._meta')",
139 | },
140 | &measurement{
141 | time.Duration(23699) * time.Microsecond,
142 | []*measurementEntry{
143 | {
144 | false,
145 | time.Duration(21963) * time.Microsecond,
146 | time.Duration(3) * time.Microsecond,
147 | time.Duration(0),
148 | "parsing arguments",
149 | },
150 | {
151 | true,
152 | time.Duration(23515) * time.Microsecond,
153 | time.Duration(278) * time.Microsecond,
154 | time.Duration(278) * time.Microsecond,
155 | "require('vim.shared')",
156 | },
157 | {
158 | true,
159 | time.Duration(23699) * time.Microsecond,
160 | time.Duration(101) * time.Microsecond,
161 | time.Duration(101) * time.Microsecond,
162 | "require('vim._meta')",
163 | },
164 | },
165 | },
166 | },
167 | } {
168 | t.Run(tc.what, func(t *testing.T) {
169 | content := []byte(header + strings.Join(tc.lines, "\n") + "\n")
170 | if err := os.WriteFile(tmpname, content, 0644); err != nil {
171 | panic(err)
172 | }
173 |
174 | f, err := os.Open(tmpname)
175 | if err != nil {
176 | panic(err)
177 | }
178 | defer f.Close()
179 |
180 | m, err := parseStartuptime(f)
181 | if err != nil {
182 | t.Fatal(err)
183 | }
184 |
185 | if !cmp.Equal(m, tc.expected, parseCmpOpts) {
186 | t.Fatal(cmp.Diff(m, tc.expected, parseCmpOpts))
187 | }
188 | })
189 | }
190 | }
191 |
192 | func TestParseErrors(t *testing.T) {
193 | tmpfile, err := os.CreateTemp("", "__test_parse_errors")
194 | if err != nil {
195 | panic(err)
196 | }
197 | tmpname := tmpfile.Name()
198 | tmpfile.Close()
199 | defer func() {
200 | os.Remove(tmpname)
201 | }()
202 |
203 | header := []string{
204 | "",
205 | "",
206 | "times in msec",
207 | "clock self+sourced self: sourced script",
208 | "clock elapsed: other lines",
209 | "",
210 | }
211 |
212 | for _, tc := range []struct {
213 | what string
214 | lines []string
215 | msg string
216 | line uint
217 | }{
218 | {
219 | what: "empty file",
220 | lines: []string{""},
221 | msg: "broken --startuptime output while parsing file",
222 | },
223 | {
224 | what: "invalid float at elapsed time",
225 | lines: append(header, "00-.008 000.008: --- VIM STARTING ---"),
226 | msg: "cannot parse field '00-.008' as millisec duration",
227 | line: 7,
228 | },
229 | {
230 | what: "invalid float at self+source",
231 | lines: append(header, "000.008 000.a08: --- VIM STARTING ---"),
232 | msg: "cannot parse field '000.a08' as millisec duration",
233 | line: 7,
234 | },
235 | {
236 | what: "invalid float at self",
237 | lines: append(header, "198.369 000.161 000.!61: sourcing /foo/bar.vim"),
238 | msg: "cannot parse field '000.!61' as millisec duration",
239 | line: 7,
240 | },
241 | {
242 | what: "script name is not existing",
243 | lines: append(header, "198.369 000.161 000.161: sourcing"),
244 | msg: "script name is missing",
245 | line: 7,
246 | },
247 | {
248 | what: "empty description",
249 | lines: append(header, "198.369 000.161 000.161:"),
250 | msg: "too few fields",
251 | line: 7,
252 | },
253 | {
254 | what: "'sourcing' is missing",
255 | lines: append(header, "198.369 000.161 000.161: /foo/bar.vim foo"),
256 | msg: "'sourcing' token or 'require(...)' token is expected but got '/foo/bar.vim'",
257 | line: 7,
258 | },
259 | } {
260 | t.Run(tc.what, func(t *testing.T) {
261 | content := []byte(strings.Join(tc.lines, "\n") + "\n")
262 | if err := os.WriteFile(tmpname, content, 0644); err != nil {
263 | panic(err)
264 | }
265 |
266 | f, err := os.Open(tmpname)
267 | if err != nil {
268 | panic(err)
269 | }
270 | defer f.Close()
271 |
272 | _, err = parseStartuptime(f)
273 | if err == nil {
274 | t.Fatal("Error did not happen:", tc.msg)
275 | }
276 |
277 | msg := err.Error()
278 | if !strings.Contains(msg, tc.msg) {
279 | t.Fatalf("Unexpected error. '%s' is not in '%s'", tc.msg, msg)
280 | }
281 | if tc.line != 0 && !strings.Contains(msg, fmt.Sprintf("parse error at line:%d:", tc.line)) {
282 | t.Fatal("Error should occur at line", tc.line, "(error:", msg, ")")
283 | }
284 | })
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/vim.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 | "path/filepath"
8 | "runtime"
9 | "strconv"
10 | "strings"
11 | )
12 |
13 | func isNeovimPath(vimpath string) bool {
14 | suff := strings.TrimSuffix(vimpath, ".exe")
15 | if strings.HasSuffix(suff, "nvim") {
16 | return true
17 | }
18 | if strings.HasSuffix(suff, "lvim") {
19 | return true
20 | }
21 | return false
22 | }
23 |
24 | func runVim(vimpath string, extra []string, args ...string) error {
25 | a := make([]string, 0, len(extra)+3+len(args))
26 | a = append(a, extra...)
27 | if isNeovimPath(vimpath) {
28 | a = append(a, "--headless")
29 | } else {
30 | a = append(a, "--not-a-term")
31 | }
32 | if runtime.GOOS == "windows" {
33 | if p, err := exec.LookPath(vimpath); err == nil {
34 | if filepath.Base(p) == "vim.exe" {
35 | a = append(a, "-e")
36 | }
37 | }
38 | }
39 | a = append(a, "-c", "qall!")
40 | a = append(a, args...)
41 |
42 | cmd := exec.Command(vimpath, a...)
43 | out, err := cmd.CombinedOutput()
44 | if err != nil {
45 | for i, b := range out {
46 | if b == '\n' || b == '\r' {
47 | out[i] = ' '
48 | }
49 | }
50 | return fmt.Errorf("failed to run %q with args %v: %w. Output: %s", vimpath, a, err, string(out))
51 | }
52 | return nil
53 | }
54 |
55 | func runVimStartuptime(vimpath, tmpdir string, id int, extra []string) (*os.File, error) {
56 | outfile := filepath.Join(tmpdir, strconv.Itoa(id))
57 | if err := runVim(vimpath, extra, "--startuptime", outfile); err != nil {
58 | return nil, err
59 | }
60 |
61 | f, err := os.Open(outfile)
62 | if err != nil {
63 | return nil, fmt.Errorf("could not open --startuptime result file '%s': %w", outfile, err)
64 | }
65 | return f, nil
66 | }
67 |
--------------------------------------------------------------------------------
/vim_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 | "testing"
9 | )
10 |
11 | func TestRunVimOK(t *testing.T) {
12 | for _, exe := range []string{"vim", "nvim"} {
13 | t.Run(exe, func(t *testing.T) {
14 | dir, err := os.MkdirTemp("", "__vim_run_test_")
15 | if err != nil {
16 | panic(err)
17 | }
18 | defer os.RemoveAll(dir)
19 |
20 | f, err := runVimStartuptime(exe, dir, 3, []string{"-u", "NONE", "-N"})
21 | if err != nil {
22 | t.Fatal(err)
23 | }
24 | defer f.Close()
25 |
26 | _, fname := filepath.Split(f.Name())
27 | if fname != "3" {
28 | t.Error("Invalid result file name", fname, "Wanted '3'")
29 | }
30 | bytes, err := io.ReadAll(f)
31 | if err != nil {
32 | t.Fatal("Cannot open startup result file", err)
33 | }
34 | content := string(bytes)
35 | if !strings.Contains(content, "--- "+strings.ToUpper(exe)+" STARTING ---") {
36 | t.Error("Invalid result file:", content)
37 | }
38 | })
39 | }
40 | }
41 |
42 | func TestRunVimError(t *testing.T) {
43 | for _, exe := range []string{"vim", "nvim"} {
44 | t.Run(exe, func(t *testing.T) {
45 | dir, err := os.MkdirTemp("", "__vim_run_test_error_")
46 | if err != nil {
47 | panic(err)
48 | }
49 | defer os.RemoveAll(dir)
50 |
51 | _, err = runVimStartuptime(exe, dir, 3, []string{"--foo", "--bar"})
52 | if err == nil {
53 | t.Fatal("Invalid extra args should cause an error")
54 | }
55 | want := "failed to run \"" + exe + "\" with args [--foo --bar"
56 | if !strings.Contains(err.Error(), want) {
57 | t.Errorf("Wanted %q but got %q", want, err)
58 | }
59 | })
60 | }
61 | }
62 |
--------------------------------------------------------------------------------