nvim-neorg / neorg

Modernity meets insane extensibility. The future of organizing your life in Neovim.
GNU General Public License v3.0
6.33k stars 209 forks source link

Startup time is relatively gigantic, and doubled again in the 4.5.0 version. #898

Open Aumnescio opened 1 year ago

Aumnescio commented 1 year ago

Prerequisites

Neovim Version

NVIM v0.9.0

Neorg setup

return {
    {   -- Knowledge management and organization.
        "nvim-neorg/neorg",
        lazy = true,
        version = "*",
        ft = "norg",
        cmd = "Neorg",
        build = ":Neorg sync-parsers",

        dependencies = {
            "nvim-lua/plenary.nvim",
            {
                "lukas-reineke/headlines.nvim",
                dependencies = {
                    "nvim-treesitter/nvim-treesitter",      -- This way, treesitter should load first, and there should be no problems.
                },
            },
        },

        opts = {
            load = {
                [ "core.defaults" ] = {},       -- Loads default behaviour.
                [ "core.concealer" ] = {        -- Adds pretty icons to your documents.
                    config = {
                        -- NOTE: `diamond` best out of these, but need to disable preset to use custom icons.
                        -- icon_preset = "diamond",     -- `basic` | `diamond` | `varied`
                        icons = {
                            definition = {
                                enabled = true,
                                multi_prefix = {
                                    enabled = true,
                                    icon = "⋙ ",
                                    query = "(multi_definition_prefix) @icon",
                                },
                                multi_suffix = {
                                    enabled = true,
                                    icon = "⋘ ",
                                    query = "(multi_definition_suffix) @icon",
                                },
                                single = {
                                    enabled = true,
                                    icon = "≡",
                                    query = "[ (single_definition_prefix) (link_target_definition) @no-conceal ] @icon",
                                },
                            },

                            delimiter = {
                                enabled = true,
                                horizontal_line = {
                                    enabled = true,
                                    highlight = "@neorg.delimiters.horizontal_line",
                                    icon = "─",
                                    query = "(horizontal_line) @icon",
                                },
                                strong = {
                                    enabled = true,
                                    highlight = "@neorg.delimiters.strong",
                                    icon = "⟪",
                                    query = "(strong_paragraph_delimiter) @icon",
                                },
                                weak = {
                                    enabled = true,
                                    highlight = "@neorg.delimiters.weak",
                                    icon = "⟨",
                                    query = "(weak_paragraph_delimiter) @icon",
                                },
                            },

                            footnote = {
                                enabled = true,
                                multi_prefix = {
                                    enabled = true,
                                    icon = "⁑",
                                    query = "(multi_footnote_prefix) @icon",
                                },
                                multi_suffix = {
                                    enabled = true,
                                    icon = "⁑",
                                    query = "(multi_footnote_suffix) @icon",
                                },
                                single = {
                                    enabled = true,
                                    icon = "⁎",
                                    query = "[ (single_footnote_prefix) (link_target_footnote) @no-conceal ] @icon",
                                },
                            },

                            heading = {
                                enabled = true,
                                level_1 = {
                                    enabled = true,
                                    highlight = "@neorg.headings.1.prefix",
                                    icon = " ",        -- Options: `⟐ `, ` `, `󰺕 `, `󱥸 `, `◈`, `⟁ `, `⯈`
                                    query = "[ (heading1_prefix) (link_target_heading1) @no-conceal ] @icon",
                                },
                                level_2 = {
                                    enabled = true,
                                    highlight = "@neorg.headings.2.prefix",
                                    icon = " 󰺕 ",
                                    query = "[ (heading2_prefix) (link_target_heading2) @no-conceal ] @icon",
                                },
                                level_3 = {
                                    enabled = true,
                                    highlight = "@neorg.headings.3.prefix",
                                    icon = "  󱥸 ",
                                    query = "[ (heading3_prefix) (link_target_heading3) @no-conceal ] @icon",
                                },
                                level_4 = {
                                    enabled = true,
                                    highlight = "@neorg.headings.4.prefix",
                                    icon = "   ◈",
                                    query = "[ (heading4_prefix) (link_target_heading4) @no-conceal ] @icon",
                                },
                                level_5 = {
                                    enabled = true,
                                    highlight = "@neorg.headings.5.prefix",
                                    icon = "    ⟐ ",
                                    query = "[ (heading5_prefix) (link_target_heading5) @no-conceal ] @icon",
                                },
                                    level_6 = {
                                    enabled = true,
                                    highlight = "@neorg.headings.6.prefix",
                                    icon = "     ⯈",
                                    query = "[ (heading6_prefix) (link_target_heading6) @no-conceal ] @icon",
                                },
                            },

                            list = {
                                enabled = true,
                                level_1 = {
                                    enabled = true,
                                    icon = "•",
                                    query = "(unordered_list1_prefix) @icon",
                                },
                                level_2 = {
                                    enabled = true,
                                    icon = " •",
                                    query = "(unordered_list2_prefix) @icon",
                                },
                                level_3 = {
                                    enabled = true,
                                    icon = "  •",
                                    query = "(unordered_list3_prefix) @icon",
                                },
                                level_4 = {
                                    enabled = true,
                                    icon = "   •",
                                    query = "(unordered_list4_prefix) @icon",
                                },
                                level_5 = {
                                    enabled = true,
                                    icon = "    •",
                                    query = "(unordered_list5_prefix) @icon",
                                },
                                level_6 = {
                                    enabled = true,
                                    icon = "     •",
                                    query = "(unordered_list6_prefix) @icon",
                                },
                            },

                            markup = {
                                enabled = true,
                                spoiler = {
                                    enabled = true,
                                    highlight = "@neorg.markup.spoiler",
                                    icon = "•",
                                    query = '(spoiler ("_open") _ @icon ("_close"))',
                                },
                            },

                            quote = {
                                enabled = true,
                                level_1 = {
                                    enabled = true,
                                    highlight = "@neorg.quotes.1.prefix",
                                    icon = "│",
                                    query = "(quote1_prefix) @icon",
                                },
                                level_2 = {
                                    enabled = true,
                                    highlight = "@neorg.quotes.2.prefix",
                                    icon = "│",
                                    query = "(quote2_prefix) @icon",
                                },
                                level_3 = {
                                    enabled = true,
                                    highlight = "@neorg.quotes.3.prefix",
                                    icon = "│",
                                    query = "(quote3_prefix) @icon",
                                },
                                    level_4 = {
                                    enabled = true,
                                    highlight = "@neorg.quotes.4.prefix",
                                    icon = "│",
                                    query = "(quote4_prefix) @icon",
                                },
                                level_5 = {
                                    enabled = true,
                                    highlight = "@neorg.quotes.5.prefix",
                                    icon = "│",
                                    query = "(quote5_prefix) @icon",
                                },
                                level_6 = {
                                    enabled = true,
                                    highlight = "@neorg.quotes.6.prefix",
                                    icon = "│",
                                    query = "(quote6_prefix) @icon",
                                },
                            },

                            todo = {
                                enabled = true,
                                undone = {
                                    enabled = true,
                                    icon = "−",         -- Options: `֎ `, `¤`, `⊟`, ``, `−`.
                                    query = "(todo_item_undone) @icon",
                                },
                                urgent = {
                                    enabled = true,
                                    icon = "",         -- Options: ` `, ` `, `⚠`, ``.
                                    query = "(todo_item_urgent) @icon",
                                },
                                done = {
                                    enabled = true,
                                    icon = "✓",         -- Options: `✓`.
                                    query = "(todo_item_done) @icon",
                                },
                                uncertain = {
                                    enabled = true,
                                    icon = "",         -- Options: `⯑ `, ` `, ``.
                                    query = "(todo_item_uncertain) @icon",
                                },
                                pending = {
                                    enabled = true,
                                    icon = "",        -- Options: ``, ``.
                                    query = "(todo_item_pending) @icon",
                                },
                                on_hold = {
                                    enabled = true,
                                    icon = "",
                                    query = "(todo_item_on_hold) @icon",
                                },
                                cancelled = {
                                    enabled = true,
                                    icon = "",         -- Options: ``, ``, `󰩺`, ``.
                                    query = "(todo_item_cancelled) @icon",
                                },
                                recurring = {
                                    enabled = true,
                                    icon = "↺",
                                    query = "(todo_item_recurring) @icon",
                                },
                            },
                        },

                        folds = false,              -- Disabling this solves some visual issues related to folds.
                        dim_code_blocks = {
                            enabled = true,
                            content_only = true,
                            conceal = true,
                            adaptive = false,
                            padding = {
                                left = 2,
                                right = 4,
                            },
                            width = "fullwidth",    -- `fullwidth` | `content`
                        },
                    },
                },  -- End `core.concealer`

                [ "core.dirman" ] = {      -- Manages Neorg workspaces.
                    config = {
                        workspaces = {
                            vault = "~/Secondbrain/Vault",
                        },
                        default_workspace = "vault"
                    },
                },  -- End `core.dirman`
            },
        },

        config = function(_, opts)
            -- |> Setup, and config icons after.
            require("neorg").setup(opts)

            -- `ordered list icons` config for Neorg.
            local neorg = require("neorg")

            -- Fetch the concealer config table for modification.
            local neorg_ordered_config = neorg.modules.get_module_config("core.concealer").icons.ordered
            local ordered_concealer_module = neorg.modules.get_module("core.concealer").concealing.ordered

            -- Set `ordered.level_<n>.icon` functions.
            neorg_ordered_config.level_1.icon = ordered_concealer_module.punctuation.unicode_dot(ordered_concealer_module.enumerator.numeric)
            neorg_ordered_config.level_2.icon = ordered_concealer_module.enumerator.latin_lowercase
            neorg_ordered_config.level_3.icon = ordered_concealer_module.punctuation.unicode_dot(ordered_concealer_module.enumerator.numeric)
            neorg_ordered_config.level_4.icon = ordered_concealer_module.enumerator.latin_lowercase
            neorg_ordered_config.level_5.icon = ordered_concealer_module.punctuation.unicode_dot(ordered_concealer_module.enumerator.numeric)
            neorg_ordered_config.level_6.icon = ordered_concealer_module.enumerator.latin_lowercase
        end,
    },
}

Actual behavior

Neovim startuptime when opening *.norg files with Neorg installed is 10 to 20 times my normal startuptime. From ~50 ms to 500-1000ms.

The most recent Neorg version 4.5.0 increased my startuptime in *.norg files from ~500ms to ~950ms. I tested reverting back to 4.4.1, and my startuptime went back to ~500ms.

Even though lazy.nvim claims that the startuptime listed in the Profile tab of the UI is more accurate than nvim --startuptime, I strongly believe this to be false in this case. lazy.nvim seems to cut the startuptime off before something happens that seems to stall for quite a while. The difference between 500ms and ~1000ms can be felt very easily, as well.

Here are small snippets from the end of the files generated by nvim --startuptime:

v4.4.1:

270.122  000.222: UIEnter autocommands
270.451  000.243  000.243: sourcing /home/aum/Executables/Neovim/nvim-linux64/share/nvim/runtime/autoload/provider/clipboard.vim
270.463  000.098: before starting main loop
270.560  000.034  000.034: sourcing /home/aum/.local/share/nvim/lazy/nvim-treesitter/autoload/nvim_treesitter.vim
270.704  000.117  000.117: require('nvim-treesitter.fold')
529.198  000.086  000.086: require('vim.inspect')
529.922  000.076  000.076: sourcing /home/aum/.local/share/nvim/lazy/nvim-web-devicons/plugin/nvim-web-devicons.vim
531.187  001.193  001.193: require('nvim-web-devicons')
533.667  004.318  003.049: require('nvim-web-devicons')
534.428  259.410: first screen update
534.432  000.004: --- NVIM STARTED ---

v4.5.0:

271.051  000.225: UIEnter autocommands
271.397  000.258  000.258: sourcing /home/aum/Executables/Neovim/nvim-linux64/share/nvim/runtime/autoload/provider/clipboard.vim
271.408  000.099: before starting main loop
271.506  000.035  000.035: sourcing /home/aum/.local/share/nvim/lazy/nvim-treesitter/autoload/nvim_treesitter.vim
271.638  000.103  000.103: require('nvim-treesitter.fold')
943.537  000.097  000.097: require('vim.inspect')
944.419  000.077  000.077: sourcing /home/aum/.local/share/nvim/lazy/nvim-web-devicons/plugin/nvim-web-devicons.vim
945.742  001.203  001.203: require('nvim-web-devicons')
948.218  004.370  003.089: require('nvim-web-devicons')
948.988  672.975: first screen update
948.993  000.004: --- NVIM STARTED ---

Very awkwardly, I do not think this clearly explains what is causing the delay.

Expected behavior

I would expect the startuptime penalty from loading Neorg to be at most around twice my base startuptime, so 100ms in my case. I would also expect startuptime to not fluctuate so wildly between versions.

I would of course hope that the startuptime could be even lower, but I'm aware of a previous startuptime related issue which was closed as wontfix. However, I do believe this issue to be separate from the base startuptime being large. The startuptime should not increase this much from what are seemingly quite small changes.

Steps to reproduce

  1. Create a file "test.norg" with at least some Neorg content.
  1. Run nvim --startuptime time-v441.txt test.norg using Neorg Version 4.4.1.
  2. Run nvim --startuptime time-v450.txt test.norg using Neorg version 4.5.0.
  3. Compare the startup-times.

Potentially conflicting plugins

  1. Folds being enabled, especially if used with the nvim-treesitter foldexpr. This can cause dramatic increases in startup time. (Up to for example 20 seconds in a non-norg plain text file with 500k lines.) Edit: I did test with the foldexpr disabled, and startuptime was still the same in .norg files.
  2. headlines.nvim, I'm using this, and it has an awkwardly large startup or load time (50ms), but I don't particularly think it is conflicting with Neorg.
  3. nvim-treesitter, I'm not sure whats going on with it, but it has a tendency to be slow.

Other information

No response

Help

No

Implementation help

No response

vhyrro commented 1 year ago

Hey, could you please update to the latest version on main and see if the startup still remains as large? On my hardware, Neorg used to take 80 milliseconds to load on startup with all the modules except the integration modules for truezen or nvim-cmp. After the latest fixes Neorg now starts up in 28ms, which is a large decrease. Metrics from different hardware configurations is something useful to have :)

Aumnescio commented 1 year ago

The Neorg package itself seems to load relatively fast. I'm getting about 20 to 50ms (up to 50'ish if including the treesitter dependency) in non-norg files, when not even attemption to lazy-load Neorg.

image

But in .norg files, something silly is happening. My startuptime is still 830+ms with the latest main, while it's around 500ms on 4.4.1. The norg file I have is only 700 lines long. Even in an extremely minimal norg file, the startuptime is 765ms.

image

I've confirmed that some plugins are somehow interfering with this, but I haven't tracked down specifics yet, except for what you can see in the first screenshot. I disabled nearly all plugins I had, and am still not able to get below 500ms (or 800 on main) startuptime in a norg file if Neorg is loading. Without loading Neorg, the norg files open just fine of course.

Other than Lazy, Neorg, and Treesitter, there's only like 8 actual plugins loading there, which all should be quite minimal and not cause issues. But somehow they do cause issues. Tried disabled folding completely, too, but no real effect in startuptime.

I'm working on a separate from-scratch Neovim config with Neorg that I'll use to track down specific issues.

The concealer speedup is noticeable and very welcome. Though it still doesn't quite fully activate when Neorg is lazy-loaded, until the cursor moves.

As for modules, I only have the defaults, concealer, and dirman modules enabled.

katawful commented 1 year ago

Does it happen with any norg file?

Aumnescio commented 1 year ago

Think so.

Even with:

* Title
   - Things

Edit:

Also even with:

test

or a completely empty file.

Aumnescio commented 1 year ago

I just tested on latest Nightly Neovim:

image

It takes almost 3 seconds.

vhyrro commented 1 year ago

Damn. I'll do some detailed profiling tomorrow in that case - I'm really interested what is actually causing the large slowdown. I also feel like there's a large lag when opening norg files, but i am not testing on a minimal config, which I will definitely try too. I'll post my findings then!

vhyrro commented 1 year ago

Okay so an update on the whole profiling situation - it seems like most of the time spent is parsing the highlights.scm file. This is a really tricky scenario because there's no nice way of making these highlights shorter without making norg files look bland. I'll try to think of a solution but it's not an easy fix sadly lol

For reference, when loading a norg file ~420 milliseconds are spent on this process alone: image

Aumnescio commented 1 year ago

Figured its a treesitter related thing.

The thing is, in a very minimal setup, the total startuptime is still way less. I'm getting around 80ms. Which would imply maybe about only 20ms spent on that treesitter highlight parsing stuff. (Edit: in this case specifically)

image

Aumnescio commented 1 year ago

Minimal config. Place into ~/.config/nvim_minimal/ as init.lua.

-- File:        `~/.config/nvim_minimal/init.lua`

-- NOTE: When using `NVIM_APPNAME=nvim_minimal` to launch into this alternate Neovim config,
-- this data path will not conflict with or pollute the primary data folders.
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"

-- Lazy Bootstrap:
if not vim.loop.fs_stat(lazypath) then
    vim.fn.system({
        "git",
        "clone",
        "--filter=blob:none",
        "https://github.com/folke/lazy.nvim.git",
        "--branch=stable", -- latest stable release
        lazypath,
    })
end

vim.opt.rtp:prepend(lazypath)

-- Lazy Setup:
require("lazy").setup({
    {
        "nvim-neorg/neorg",
        lazy = false,
        version = false,
        cmd = {
            "Neorg",
        },
        build = ":Neorg sync-parsers",
        dependencies = {
            "nvim-lua/plenary.nvim",
            "nvim-treesitter/nvim-treesitter",
        },

        opts = {
            load = {
                [ "core.defaults" ] = {},
                [ "core.concealer" ] = {},
            },
        },

        config = function(_, opts)
            require("neorg").setup(opts)
        end,
    },
})

-- End of File

Then launch Neovim using: NVIM_APPNAME=nvim_minimal nvim <some-norg-file>

NOTE: I'm on NVIM v0.9.0, which seems to be load the fastest right now, for whatever reason.

vhyrro commented 1 year ago

Weird, difficult to pinpoint the exact cause here but I know that for larger configs treesitter is happily eating up most of the startup time. Indeed it runs pretty fast on the minimal config. As a side note, I'm testing in a test.lua file and making it parse highlights.scm, which I'm using as a benchmark. You can really feel it take a solid second on the default highlights.scm. What's causing the problem mainly boils down to this part:

((todo_item_undone) @neorg.todo_items.undone
 .
 (_) @neorg.todo_items.undone.content)

((todo_item_done) @neorg.todo_items.done
 .
 (_) @neorg.todo_items.done.content)

((todo_item_pending) @neorg.todo_items.pending
 .
 (_) @neorg.todo_items.pending.content)

((todo_item_on_hold) @neorg.todo_items.on_hold
 .
 (_) @neorg.todo_items.on_hold.content)

((todo_item_cancelled) @neorg.todo_items.cancelled
 .
 (_) @neorg.todo_items.cancelled.content)

((todo_item_uncertain) @neorg.todo_items.uncertain
 .
 (_) @neorg.todo_items.uncertain.content)

((todo_item_urgent) @neorg.todo_items.urgent
 .
 (_) @neorg.todo_items.urgent.content)

((todo_item_recurring) @neorg.todo_items.recurring
 .
 (_) @neorg.todo_items.recurring.content)

Which causes most of the slowdown. I'll change it to be faster by cutting some features.

vhyrro commented 1 year ago

Alright, after modifying the queries the startup time has dropped, but the performance issues are not fixed by any means lol, it's still very slow. I'll do more profiling, more benchmarking, maybe i'll magically come up with a good solution :D