Repository navigation
perf: stop tracing zpack's startup code - #52
Merged
Merged
Conversation
zuqini
force-pushed
the
zpack-startup-perf-2
branch
from
November 1, 2026 21:06
0929d23 to
5d67966
Compare
- fix(commands)!: `:ZPack clean` and `:ZPack sync` list what they would delete and ask, defaulting to No; `!` skips the prompt
- fix(import)!: a `nil` in a spec list no longer ends the list, so what follows it installs
- fix(lazy)!: a plugin whose function-form trigger throws stays lazy instead of loading at startup
- fix(setup): `setup()`'s options no longer count as spec fields, so specs written beside them all install
- fix(import): a bare string in a spec list installs as a plugin, rather than being reported as invalid and skipped
- fix(import): an `import` with no plugin of its own finds its module in a listed plugin named after it, installing it if missing, on the first start
- refactor(module_loader)!: remove the unreachable profiler; `setup({ profiling = ... })` is ignored, and `print_profile` and `get_profile` are gone
- fix(commands): `:ZPack clean` leaves a plugin active this session for next time rather than raising
- fix(utils): a `version` or `tag` of the wrong type is dropped with a warning instead of failing every plugin
- fix(validate): warn about a `sem_version` that isn't a semver range, and fall through to `branch` and `defaults.version`
- fix(pack): a soft version installs from the default branch after another plugin's clone failed
- fix(pack): restore a locked plugin at its revision when no tag fits its soft version
- fix(pack): say when a plugin needs a restart to reach a tag or to update
- fix(pack): read a tag as `vim.pack` does when deciding if one fits
- fix(loader): report a `plugin/` script's error without failing the load
- fix(startup): run an eager plugin's `config` once when a lazy dependent loads it
- fix(lazy): no proxy over what a function-form trigger already loaded
- fix(merge): removals resting on other removals no longer depend on import order
- fix(module_loader): load the plugin holding a module in a shared namespace, including a compiled one
- fix(cmd): keep a numeric first argument on a lazy command's first use
- fix(import): take a plugin's `lua/` off `package.path` after startup
- fix(setup): a string is the module to import, in `setup('custom.plugins')` and in `spec`, rather than reported and replaced by the default import
- fix(setup): a single spec with no array part, such as `{ import = ..., defaults = ... }`, is read as a spec rather than ignored for the default import
- fix(setup): any other `setup()` argument that isn't a table is reported, and the default import goes on
- fix(module_loader)!: `require` no longer loads a plugin whose name resembles a module it doesn't provide; a plugin registering a module from `plugin/` needs a trigger of its own
- fix(setup): `setup()` with no `spec` and no `lua/plugins`, or one holding only dotfiles, is quiet, as in 2.x
- fix(hooks): a module brought in by an in-session update is found without a restart
- refactor(ft): an `event = 'FileType ...'` plugin closes every open `ft` group, at the cost of one more `FileType` re-fire there
- docs(tips): add Upgrading to 3.0
zuqini
force-pushed
the
zpack-startup-perf-2
branch
from
November 2, 2026 19:11
1d2c923 to
ffa1492
Compare
This was referenced Nov 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Started as the second round of startup work after #50 and #51. Review of it found bugs on main that delete plugins, so this now changes behaviour.
docs/tips.md"Upgrading to 3.0" lists the changes that can affect a working config, and the override below lists every user-visible one.Startup: zpack's startup loops run a few hundred times and never again, so LuaJIT spends longer compiling them than they would take to interpret: about 8.4 ms of a 35 ms sampled startup with 150 plugins, 3.3 ms after this. Each module that runs at startup opts its own functions out with
jit.off(true, true). User configs and plugins keep the JIT, and it is guarded for non-LuaJIT builds.Paired d-cpu in ms, 150 plugins, median with bootstrap 95% CI, 400 rounds in shuffled order, measured at 1e6b70e under load 6-12, against lazy.nvim and against f5767ae, the state before the import search and the clean prompt:
Machine load inflates these numbers, so only the paired deltas mean anything. Rechecked at fe3803e under load 6-9, 400 rounds, against lazy.nvim: realistic -5.0 [-6.8, -3.6], open a file -7.2 [-8.5, -5.1], all eager -116 [-121, -111]. At the final state: realistic -5.3 [-6.5, -4.1], open a file -7.4 [-9.5, -5.4].
None of the benches above imports from a listed plugin. With AstroNvim's community shape added to the realistic one,
{ 'AstroNvim/astrocommunity' }beside{ import = 'astrocommunity.pack.lua' }, zpack pays +5.9 [+4.5, +7.0] ms per start over putting the import on the plugin's spec (a trial resolve and a second pass), which leaves it level with lazy.nvim on that shape: -0.1 [-1.4, +1.3] (fe3803e, load 7). An import no plugin has costs +4.3 [+3.3, +5.4] ms.Clean and sync: a failed
vim.pack.addemptied the registry, and:ZPack cleanand:ZPack syncthen deleted every installed plugin without asking. A spec module, import or list entry zpack can't read did the same to what it declares. Both now list what they would delete and ask with avim.fn.confirmthat defaults to No, so a headless run deletes nothing, and:ZPack! cleanand:ZPack! syncskip it for scripts.defaults.confirm = falsestill only means don't ask before installing. Sync asks once its update has finished, and skips clean if the update raises, as when interrupted. Every problem is reported at startup, andsetup()with nospecand nolua/pluginsreports nothing, since nothing was written.:ZPack syncupdates even when a spec can't be read: a plugin withpin = truein that spec moves, itsversionortagholds, and:ZPack restoredoesn't undo the move unless the lockfile is in git.Imports: an
importwith no plugin of its own that your config and 'runtimepath' don't have is looked for in the listed plugins that stay and are named after the module's top level, by name or repository, installing what's missing, so{ { 'u/dist' }, { import = 'dist.plugins.extra' } }and AstroNvim's template shape import on the first start and later ones, where 2.x imported nothing. lazy.nvim tries those first and then every other plugin; zpack stops there, so a missing gitignored module such asplugins.machine_localdoesn't install every listed plugin from inside the import, and the warning says to put the import on its plugin's spec. Unlike lazy.nvim it finds a plugin listed after theimportand doesn't read a disabled one. A bare string in a spec list installs as a plugin, where it was reported as invalid and skipped. Anilin a spec list no longer ends the list: LazyVim's AI extras putvim.g.ai_cmp and { ... } or nilmid-list. lazy.nvim stops at it (decision zpack_nvim-77y). A function-formcondat an import gets no plugin, as in lazy.nvim, in place of the unreleased stand-in from #51, so one that reads its argument throws, which reports the error and skips the import.Versions: one
version = trueortag = 3no longer fails every plugin in thevim.pack.addcall. A soft version that follows the default branch says when a restart is needed to reach a tag, and a plugin restored from the lockfile says when it needs a restart to update.Setup:
setup()'s options no longer count as spec fields.{ 'u/a', 'u/b', defaults = ... }, as documented, installed onlyu/aand warned, and an{ import = ... }beside the string was skipped; now it's a list.{ 'u/a', config = fn }stays one spec. A string is the module to import, insetup('custom.plugins')and{ spec = 'custom.plugins' }, where it reported an error and:ZPack cleandeleted that module's plugins.A single spec with no array part,
{ import = 'custom.plugins', defaults = ... }or{ url = ..., name = ... }, is now read as a spec, where it silently fell back to the default import with the same result.Loading: an error in a
plugin/script is reported and the load carries on toconfig. An eager plugin a lazy dependent loads runsconfigonce. A function-form trigger that throws leaves its plugin lazy, and one that loaded the plugin doesn't get a proxy over its own key or command.requirematches a module's exact top-level name against each plugin'slua/, and loads the plugin holding the file, sorequire('dap.ui.widgets')no longer loads nvim-dap-ui by name. A plugin is no longer loaded because its name resembles a module it doesn't provide.Structure:
merge.lua,utils.luaandimport.luaare split by concern as pure moves, and state only one module reads moves there.lua/is 128 lines shorter than main, with the fixes above included. The unreachable profiler is gone:setup({ profiling = ... })is ignored without a word, andmodule_loader'sprint_profileandget_profileare removed.Checked against real configs: LazyVim's spec files (core, all 115 extras, 40 seeded subsets) and my own config (the live one too, through 1e6b70e, with identical runtime state at startup), between the state before review and this one, and again for the import search and the prompt at 1e6b70e: no
lazy=flips and noPorEline changes, only the missing-importWlines gaining the fix and five DEBUG notes on all-extras going away. The prompt in:ZPack cleanand:ZPack syncchanged no line of that output. Against lazy.nvim's resolver, the plugin set and edges differ only by the harness's<expr>placeholders. The final state against 013dbbe (before the last import changes) gives the same result, with only the missing-import hint text changing, and the live config's output is byte-identical. The suite passes on nightly and v0.12.0.BEGIN_COMMIT_OVERRIDE
perf: stop tracing zpack's startup code
fix(commands)!:
:ZPack cleanand:ZPack synclist what they would delete and ask, defaulting to No;!skips the promptfix(import)!: a
nilin a spec list no longer ends the list, so what follows it installsfix(lazy)!: a plugin whose function-form trigger throws stays lazy instead of loading at startup
fix(setup):
setup()'s options no longer count as spec fields, so specs written beside them all installfix(import): a bare string in a spec list installs as a plugin, rather than being reported as invalid and skipped
fix(import): an
importwith no plugin of its own finds its module in a listed plugin named after it, installing it if missing, on the first startrefactor(module_loader)!: remove the unreachable profiler;
setup({ profiling = ... })is ignored, andprint_profileandget_profileare gonefix(commands):
:ZPack cleanleaves a plugin active this session for next time rather than raisingfix(utils): a
versionortagof the wrong type is dropped with a warning instead of failing every pluginfix(validate): warn about a
sem_versionthat isn't a semver range, and fall through tobranchanddefaults.versionfix(pack): a soft version installs from the default branch after another plugin's clone failed
fix(pack): restore a locked plugin at its revision when no tag fits its soft version
fix(pack): say when a plugin needs a restart to reach a tag or to update
fix(pack): read a tag as
vim.packdoes when deciding if one fitsfix(loader): report a
plugin/script's error without failing the loadfix(startup): run an eager plugin's
configonce when a lazy dependent loads itfix(lazy): no proxy over what a function-form trigger already loaded
fix(merge): removals resting on other removals no longer depend on import order
fix(module_loader): load the plugin holding a module in a shared namespace, including a compiled one
fix(cmd): keep a numeric first argument on a lazy command's first use
fix(import): take a plugin's
lua/offpackage.pathafter startupfix(setup): a string is the module to import, in
setup('custom.plugins')and inspec, rather than reported and replaced by the default importfix(setup): a single spec with no array part, such as
{ import = ..., defaults = ... }, is read as a spec rather than ignored for the default importfix(setup): any other
setup()argument that isn't a table is reported, and the default import goes onfix(module_loader)!:
requireno longer loads a plugin whose name resembles a module it doesn't provide; a plugin registering a module fromplugin/needs a trigger of its ownfix(setup):
setup()with nospecand nolua/plugins, or one holding only dotfiles, is quiet, as in 2.xfix(hooks): a module brought in by an in-session update is found without a restart
refactor(ft): an
event = 'FileType ...'plugin closes every openftgroup, at the cost of one moreFileTypere-fire theredocs(tips): add Upgrading to 3.0
END_COMMIT_OVERRIDE