runtime-analysis.nvim · Debug & inspect · vimdoc

:help runtime-analysis

Runtime truth for Neovim plugins

doc/runtime-analysis.txt — rendered from the plugin's own vimdoc

*runtime-analysis.txt*  Runtime truth for Neovim plugins    *runtime-analysis.nvim*

CONTENTS *runtime-analysis-contents*

  1. Introduction ................................... |runtime-analysis-intro|
  2. Requirements ............................ |runtime-analysis-requirements|
  3. Installation ............................ |runtime-analysis-installation|
  4. Configuration .................................. |runtime-analysis-config|
  5. Commands ...................................... |runtime-analysis-commands|
  6. Telemetry ................................... |runtime-analysis-telemetry|
  7. Integration with documentation.nvim ....... |runtime-analysis-integration|
  8. Health check .................................. |runtime-analysis-health|
  9. Further reading .............................. |runtime-analysis-roadmap|

1. INTRODUCTION *runtime-analysis-intro*

runtime-analysis.nvim answers what documentation.nvim
(https://github.com/StefanBartl/documentation.nvim) cannot: not what a
codebase *declares*, but what actually happens when it runs. See that
plugin's docs/ECOSYSTEM.md for the architecture this pairing is part of.

The main pieces:

  - An in-editor HTTP request runner (|:RARequest|, |:RASend|) — no browser,
    no server, no CORS, no token, because none of those problems exist for a
    request Neovim itself sends via curl.
  - runtime-analysis.telemetry (|:RATelemetry|) — opt-in call counting and
    usage statistics for any Lua/Neovim plugin, moved here from lib.nvim.
  - runtime-analysis.loaded — what package.loaded actually holds right
    now, the live half of a loaded-vs-declared diff (see
    |runtime-analysis-loaded|).
  - Main-loop stall detection (|:RA-startup|) — a libuv timer measuring its
    own lateness, so a freeze shows up whatever caused it.
  - Startup profiling (|:RA-startup| profile) — repeated --startuptime
    runs, averaged, so a per-file cost is a median rather than one sample.

Plus |:RA-provenance|, |:RA-inspect|, |:RA-usage| and runtime-analysis.bench
(|runtime-analysis-bench|) — see the sections below.

2. REQUIREMENTS *runtime-analysis-requirements*

  - Neovim 0.10 or later (vim.system)
  - lib.nvim (https://github.com/StefanBartl/lib.nvim) — hard dependency:
    net.curl's fetch_raw/fetch_raw_blocking for the request runner,
    usercmd.composer for |:RA|, progress for the sending/cancel
    indicator and telemetry's own reports, fs.project_key + cache.disk
    for request history (and telemetry's own persistence), fs.find_root +
    fs.json for environment files (|:RA-env|), plus git, ui.kit,
    usercmd, notify and autocmd.
  - curl on PATH — every |:RASend| shells out to it via lib.nvim.

Optional:

  - mdview.nvim (https://github.com/StefanBartl/mdview.nvim) — renders a
    telemetry report as a live browser tab (report_style = "mdview").
    Soft dependency throughout, pcall-guarded.

Run |:checkhealth-runtime-analysis| to verify all of the above.

3. INSTALLATION *runtime-analysis-installation*

lazy.nvim:

  {
    "StefanBartl/runtime-analysis.nvim",
    dependencies = { "StefanBartl/lib.nvim" },
    cmd = { "RARequest", "RASend", "RATelemetry" },
    opts = {
      -- split = "vsplit",
      -- request_filetype = "http",
      -- telemetry = { plugins = {...}, lib_nvim = {...} },  -- lazy.nvim only
    },
  }
opts = {...} calls require("runtime-analysis").setup(opts) for you, the
same as:

  require("runtime-analysis").setup({...})

4. CONFIGURATION *runtime-analysis-config*

  require("runtime-analysis").setup({
    split = "vsplit",           -- default; "split" for a horizontal one
    request_filetype = "http",  -- default
    deps_popup = true,          -- default
    history_max_entries = 200,  -- default
    telemetry = nil,            -- see |runtime-analysis-telemetry|
  })
                                                    *runtime-analysis-opts.split*

opts.split

  Type: string. Default: "vsplit".
  Where the response pane opens relative to the request buffer: "vsplit"
  or "split".

                                        *runtime-analysis-opts.request_filetype*

opts.request_filetype

  Type: string. Default: "http".
  Filetype set on a new |:RARequest| buffer. Left at "http" rather than a
  plugin-specific name so VS Code's REST Client / IntelliJ's HTTP Client
  syntax highlighting, already installed by most readers, applies unchanged.

                                                *runtime-analysis-opts.deps_popup*

opts.deps_popup

  Type: boolean. Default: true.
  The one-time "which CLI tools does this plugin want, and why" popup on
  the first setup() after install, via lib.nvim's deps module. false
  disables it for this plugin alone, right here in its own spec.

                                     *runtime-analysis-opts.history_max_entries*

opts.history_max_entries

  Type: integer. Default: 200.
  How many past sends |:RA-history| keeps, per project. A ring: the file's
  size stays a function of how much the plugin is used, not of how long ago
  it was first started.

                                               *runtime-analysis-opts.telemetry*

opts.telemetry

  Type: table|nil. Default: nil (no auto-instrumentation).
  Forwarded verbatim to runtime-analysis.telemetry.lazy's setup() — see
  |runtime-analysis-telemetry| and that module's own README for the full
  shape. A no-op if lazy.nvim is not the caller's plugin manager.

5. COMMANDS *runtime-analysis-commands*

:RA is a compound verb built via lib.nvim.bindings.usercmd.composer — the same
verb-first shape |:DocMap| and |:MDView| already use, <Tab>-completed.
:RARequest/:RASend are flat aliases for :RA request/:RA send, kept
because they predate :RA and are this plugin's most-referenced surface —
see docs/BINDINGS.md in the repository for the full reasoning.

                                                                    *:RA*
:RA {subcommand}                Dispatch to one of the subcommands below.
                                <Tab> after :RA  completes them.

                                                            *:RA-request*
                                                              *:RARequest*
:RA request                     Open a new request buffer, in the shape VS
:RARequest                     Code's REST Client / IntelliJ's HTTP Client
                                already use:
  METHOD url
  Header-Name: value

  {"optional": "body", "after": "one blank line"}
  Always a fresh, unnamed scratch buffer — for a committed file with
  several requests, open it directly with :e instead, see
  |runtime-analysis-multi-request| below.

  One header name is a shorthand: Auth: Bearer <token> or
  Auth: Basic <user>:<pass> resolve into a real Authorization header,
  base64-encoding Basic credentials for you. See
  |runtime-analysis-auth-shorthand| below.

                                                               *:RA-send*
                                                              *:RASend*
:RA send                        Parses and sends whichever ### block the
:RASend                         cursor is in (or nearest above it — see
                                |runtime-analysis-multi-request| below),
                                sends it via lib.nvim.net.curl, and shows
                                the response (status, headers, body) in a
                                persistent split beside it.

  Non-blocking: the editor stays responsive while curl runs. A
  "-> sending METHOD url ..." placeholder appears immediately, replaced by
  the real response, an error, or a cancelled message once one of those
  happens. A visible sending/cancel indicator via lib.nvim.progress if
  available (soft dependency — sending still works with no spinner if it
  is not). Firing a second |:RA-send| before the first replies supersedes
  it — a monotonic token, not a queue, not an "already in flight" refusal;
  see |:RA-cancel| below, which does the same thing on request.

  The response pane is reused across sends and never steals focus from
  the request buffer. A Content-Type: application/json body is
  pretty-printed with real json filetype/folding, reset to plain on the
  next call if that response is not JSON.

                                                *runtime-analysis-multi-request*

Multiple requests, and real files

  A buffer (or a real, committed .http/.rest file) may hold more than
  one request, ###-separated — the same delimiter both VS Code's REST
  Client and IntelliJ's HTTP Client use:

  GET https://api.example.com/users

  ###

  POST https://api.example.com/users
  Content-Type: application/json

  {"name": "Bob"}
  runtime-analysis.parse.split cuts the buffer on every ### line
  (excluded from both sides — a pure separator); parse.block_at then
  resolves which block a cursor line belongs to. |:RA-send|/|:RASend|
  always run this, even on a single-block buffer (no ### at all is one
  block covering everything) — one code path, not a special case for the
  common single-request buffer. Always the block under the cursor, never
  the whole buffer, never a picker.

  *.http resolves to filetype http natively in Neovim; *.rest
  (IntelliJ HTTP Client's own extension for the identical shape) via this
  plugin's own ftdetect/runtime-analysis.lua. Either way, a real file
  opened directly with :e works exactly like a scratch buffer from
  |:RA-request| — :RA send reads "the current buffer", origin-agnostic.

                                              *runtime-analysis-assertions*

Response assertions

  # @expect status 200 (or // @expect status 200), a comment line
  anywhere in a ### block, checked once |:RA-send|'s response arrives.
  Deliberately narrow — one directive, one thing it checks ("is this
  endpoint still 200"), not a general assertion language. At most one per
  block: a second is a real error, not "last one wins" silently.

  A match is a plain vim.notify (-> expect status 200). A mismatch —
  including a transport failure, itself an automatic mismatch when a
  status was expected — replaces the quickfix list (:copen to see it,
  never auto-opened) with one entry pointing at the directive's own line,
  naming both the expected and actual status. Module:
  lua/runtime-analysis/assertions.lua.

                                                               *:RA-yank*
:RA yank                        Yank just the response body (not the status
                                line or headers) to the unnamed register.
                                Warns rather than erroring when there is no
                                response yet, or the last one had no body.

                                                             *:RA-cancel*
:RA cancel                      Cancel the in-flight request, if any: shows
                                "x cancelled" in the response pane
                                immediately and marks its eventual result
                                as stale, so a late reply never overwrites
                                that message. Warns rather than erroring
                                when nothing is in flight.

  A logical cancel, not a process kill: lib.nvim.net.curl.fetch_raw does
  not hand back the vim.SystemObj a hard kill would need, so the
  underlying curl process keeps running to completion in the background —
  only this plugin's own interest in its result is withdrawn. Extending
  lib.nvim.net.curl for a real kill is separate work in a different
  repository, not attempted here.

                                                            *:RA-history*
:RA history                     Open a vim.ui.select picker (whichever
                                picker UI is already configured) over
                                every send this project has recorded,
                                newest first: "date  status  METHOD  url".
                                Picking one reopens it via open_request —
                                see |runtime-analysis-history| below for
                                exactly what is and is not recorded.
                                Reports (does not error) when nothing has
                                been recorded yet.

                                                      *:RA-history-clear*
:RA history clear               Clear this project's recorded history. No
                                confirmation prompt.

                                                                *:RA-env*
:RA env [name]                  With name (<Tab>-completed), select it
                                as the active environment {{var}}
                                placeholders resolve against. With none,
                                vim.ui.select over every name the project's
                                env files define. See
                                |runtime-analysis-environments| below.
                                Session-scoped, not persisted across
                                restarts.

                                                             *:RA-import*
:RA import                      Parse a curl command line into a new
                                request buffer. Reads the system clipboard
                                by default, falling back to the unnamed
                                register; given a real range instead
                                (e.g. '<,'>RA import after visually
                                selecting one), reads those lines instead.
                                See |runtime-analysis-curl-import-export|
                                below.

                                                             *:RA-export*
:RA export                      The reverse: format whichever ### block
                                the cursor is in as a shareable curl
                                command line, yanked to the unnamed
                                register — the same convention |:RA-yank|
                                already uses.

                                                         *:RA-provenance*
:RA provenance {path}           Who wrapped this function right now.
                                {path} is a dotted string, e.g.
                                vim.notify or
                                lib.nvim.notify.create — resolved via a
                                global-table walk first, then require() of
                                the whole prefix. Exact for this plugin's
                                own telemetry wraps (named by namespace);
                                best-effort otherwise (a debug.getinfo
                                source location). See
                                |runtime-analysis-provenance| below.

                                                             *:RA-usage*
:RA usage                       Report keymap/command press counts
                                collected since |:RA-usage-start|. A kit
                                float if ui.kit is available,
                                vim.notify otherwise.

                                                       *:RA-usage-start*
:RA usage start                 Start counting — opt-in, nothing runs
                                before this.

                                                        *:RA-usage-stop*
:RA usage stop                  Stop counting. Collected counts stay
                                readable via |:RA-usage|.

                                See |runtime-analysis-usage| below.

                                                            *:RA-inspect*
:RA inspect {module}            Walk a live package.loaded[{module}] table
                                and render it: functions (upvalue counts,
                                source location), nested tables (their own
                                shape), metatables, and which direct keys
                                *shadow* a table __index (reported, never
                                called). <Tab>-completes against
                                package.loaded, live. The deeper companion
                                to |:RA-provenance| ("who wrapped this one
                                function") — this answers "what does this
                                whole table contain, right now".

                                                            *:RA-startup*
:RA startup start               Watch the main loop for stalls and report
                                after 12s. A libuv timer measures its own
                                lateness, so a block is caught wherever it
                                comes from — including libuv callbacks,
                                which :profile cannot see.
:RA startup watch               The same measurement, kept running until
                                |:RA-startup| report.
:RA startup report              Stop measuring and show the timeline:
                                plugin loads (with lazy's load time AND the
                                reason it loaded), VimEnter, VeryLazy,
                                LspAttach, LSP progress, all on one clock.
                                Written to ra-startup.log in the cwd.
:RA startup probe               Print and yank the --cmd line that
                                measures a startup from before the config
                                runs — the timer has to tick before any
                                plugin manager exists, which a lazily
                                loaded plugin cannot arrange for itself.
:RA startup profile [runs]      Start Neovim {runs} times (default 5,
                                sequentially — parallel starts inflate each
                                other) under --startuptime, parse every
                                log, and report the per-file cost as a
                                median with its spread beside it. The one
                                measurement here that runs --startuptime
                                rather than working around it: it sees
                                every file sourced from the first
                                millisecond, where the wrapper in
                                |:RATelemetry| startup can only see what
                                loads after it armed. A row whose spread
                                rivals its median is scatter, not a
                                finding. gf opens the file on the row.

                                                     *:RA-loaded-snapshot*
:RA loaded snapshot {prefix} [name]
                                Persist every currently-loaded module
                                under {prefix} (itself, or anything
                                beginning {prefix}. — the same scoping
                                wrap_loaded(prefix) already uses) as a
                                named snapshot, so it can be read later —
                                or from a different process entirely, e.g.
                                documentation.nvim's own Loaded Analysis
                                panel over :DocMap serve — rather than
                                only in the live session that took it.
                                {name} defaults to a timestamp. Always
                                explicit: nothing here ever snapshots on
                                its own.

                                                    *:RA-loaded-snapshots*
:RA loaded snapshots {prefix}   List every saved snapshot for {prefix},
                                newest first.

                                See |runtime-analysis-loaded| below.

                                                   *runtime-analysis-history*

Request history

  Every |:RA-send|/|:RASend| records one entry via
  runtime-analysis.history: **method, url, status, timestamp — nothing
  else, on either side.** No headers, no body. A header is very often
  where the real secret actually lives (an Authorization: Bearer ...
  value — the whole reason |runtime-analysis-auth-shorthand| exists), so
  it is left out on the request side too.

  Persisted via lib.nvim.cache.disk, namespaced per project by
  lib.nvim.fs.project_key() (the Git root, or the cwd) — a .http
  collection in one repository never shows up in another's history.
  Capped at history.MAX_ENTRIES (200) total, oldest dropped first.

  Every outcome is recorded exactly once: a real response, a transport
  failure, an explicit |:RA-cancel|, and even a superseded send's real
  eventual result once it is known. Never recorded twice for the same
  send.

  Honest limit: the url is stored verbatim — a secret embedded in a query
  string is not stripped, since doing so generically and correctly is a
  real, separate problem.

                                              *runtime-analysis-environments*

Variables and environments

  {{name}} inside a request buffer's url, header values or body — e.g.
  {{baseUrl}}/users/:id — resolves against the environment selected by
  |:RA-env|. Names and values come from two per-project JSON files at the
  project root, the same split IntelliJ's HTTP Client already uses:

  http-client.env.json          shared, safe to commit — non-secret
                                 defaults (a baseUrl, a tenant id)
  http-client.private.env.json  gitignored, per-machine — the file a
                                 real token belongs in
  Both optional, merged per environment name with the private file's own
  keys winning on overlap. runtime-analysis.env warns once per session
  if the private file exists on disk but its name is not found anywhere in
  the project's own .gitignore (a substring check, not a real gitignore
  matcher).

  Resolution happens exactly once, immediately before a request is handed
  to curl: request history (above) and the "-> sending ..." placeholder
  both keep the literal {{token}}, never the value it resolved to. A
  request with no placeholders at all is unaffected even with no
  environment selected. Referencing an undefined variable, or one with no
  environment selected at all, is a vim.notify error naming exactly what
  is missing — never a silent {{name}} sent to a real server as a
  literal string.

                                      *runtime-analysis-curl-import-export*

curl import/export

  |:RA-import| parses a real curl command line — the same shape every
  API's own docs and every browser's "copy as cURL" already produce — via
  runtime-analysis.curl.parse: a genuine, if bounded, tokenizer (single-
  and double-quoted strings, an unquoted backslash escaping the next
  character, bash line continuations joined first), not string
  templating. Recognizes -X/--request, -H/--header (repeatable),
  -d/--data/--data-raw/--data-binary (repeatable, joined with &,
  curl's own behavior), -u/--user (a real base64-encoded
  Authorization: Basic header), -b/--cookie, -A/--user-agent,
  -e/--referer, and drops flags meaningless to a request this plugin
  sends itself (-s, -v, -L, -o, --compressed, timeouts, TLS
  material, …) without ever mistaking one's value for the URL. Any
  -d/--data with no explicit -X implies POST, mirroring real
  curl's own default.

  |:RA-export| is the reverse, runtime-analysis.curl.format: headers
  sorted for deterministic output, single quotes escaped with the
  standard POSIX '\'' trick — deliberately not vim.fn.shellescape,
  which escapes for Neovim's own &shell (cmd.exe/PowerShell syntax on
  Windows), the wrong grammar for a curl command meant to be pasted into
  any real shell or shared verbatim in a doc.

  **Neither resolves {{var}} placeholders** — the identical trap
  |runtime-analysis-environments| above names, closed the identical way:
  both are handed the raw, unresolved request runtime-analysis.parse
  produces, the same one request history and the "sending ..." placeholder
  already keep unresolved. Exporting is sharing, and a {{token}} must
  render as {{token}} there too.

                                              *runtime-analysis-provenance*

Wrapper provenance

  |:RA-provenance| — "who wrapped this function": the
  narrow slice of :RA inspect (§5.1) worth shipping on
  its own first. {path} is a dotted string, resolved via a global-table
  walk first, then require() of the whole prefix — and stops there; it
  does not guess where a module boundary sits inside a longer path.

  Two different answers, and the output states which one it is giving:
  this plugin's own telemetry wrapper is **exact**
  (runtime-analysis.telemetry.registry is the one shared wrap layer
  every instance goes through, so it genuinely knows every subscribing
  namespace by name); anyone else's wrapper (lib.nvim.system.proc_trace,
  any plugin that monkey-patches vim.notify) is **best-effort** — a
  debug.getinfo source location, the only honest signal available with
  no registry to consult. It cannot say who installed a wrapper or when,
  only where the function currently there was actually defined.

                                                    *runtime-analysis-usage*

Keymap and command usage

  |:RA-usage| — which of your own keymaps and typed
  commands you actually press. The one feature in this plugin that records
  *what the person did* rather than *what the code did*, and it ships with
  exactly the posture that caveat demands: opt-in
  (|:RA-usage-start|/|:RA-usage-stop|, nothing runs on setup() alone) and
  local-only, no account, no upload, no "share this" feature.

  Built on runtime-analysis.telemetry itself, the same wrap mechanism
  §7.2's cost-vs-use report already reads for code — one real instance
  underneath. :RA usage start wraps vim.keymap.set so a function-
  callback mapping's own callback is wrapped exactly once, at the moment
  it is set, not per press; a CmdlineLeave autocmd counts a typed
  command once it actually commits (an <Esc>-aborted line counts
  nothing).

  Honest limits: only mappings set after :RA usage start are ever
  seen; a string-rhs mapping (`vim.keymap.set('n', 'x',
  ':SomeCommand<CR>')`) has no function to wrap; a buffer-local mapping
  sharing the same mode+lhs as a different mapping elsewhere is combined
  into one count, not tracked per buffer; a typed command's name is read
  heuristically and can occasionally misparse an unusual range.

                                                    *runtime-analysis-auth-shorthand*

Auth: shorthand

  Auth: Bearer <token> passes through verbatim as `Authorization: Bearer
  <token> — the shorthand's value is that Bearer/Basic` read as a
  matched pair, not that Bearer itself got shorter.

  Auth: Basic <user>:<pass> base64-encodes the credentials into
  Authorization: Basic <...> — the actual value-add: RFC 7617 requires
  the base64 form, and computing it by hand is the annoyance this removes.
  Auth: Basic <already-base64> (no : in the value) passes through
  unencoded, since base64's own alphabet never contains one.

  Any other scheme after Auth: passes through as the Authorization
  value unmodified — a generic fallback, not an error. A literal
  Authorization: header, written directly, is untouched by any of this.

                                                              *:RATelemetry*
:RATelemetry [args]             See |runtime-analysis-telemetry| below — a
                                second, separate compound command rather
                                than :RA telemetry ... (the same split
                                documentation.nvim draws between |:DocMap|
                                and |:DocBrowse|). Registered by the same
                                setup() call above, opt-in in the sense
                                that requiring the telemetry module alone
                                registers nothing; setup() always wires
                                the command up.

6. TELEMETRY *runtime-analysis-telemetry*

runtime-analysis.telemetry counts how often functions are called and
persists the counts across restarts. Full API — instances, wrap/wrap_loaded,
auto(), the lazy.nvim adapter, argument profiling, report_style, the
lifecycle reminder — is documented in this repository's own:

  lua/runtime-analysis/telemetry/README.md

rather than duplicated here; that file is long and changes independently of
the rest of this plugin. Command surface:

  :RATelemetry                 report across every live instance
  :RATelemetry lsp.nvim        report for one namespace
  :RATelemetry status          the fleet board: one aligned row per namespace
                               this plugin knows about -- live this session or
                               only ever persisted -- with state, mode,
                               counts, size on disk and start date. <CR> on a
                               row opens its full report; keys are listed in
                               the window's winbar and under ?
  :RATelemetry start [ns]      every instance, or just one
  :RATelemetry stop [ns]       every instance, or just one
  :RATelemetry reset [ns]      back up (one prompt, only if anything would
                               be lost), then drop collected data -- every
                               instance, or just one
  :RATelemetry disable [ns]    stop + persist "off" across restarts
  :RATelemetry enable [ns]     clear a persisted disable, resume now
  :RATelemetry disabled        list namespaces currently disabled
  :RATelemetry coverage        which wrapped functions were never called
  :RATelemetry export [path]   JSON; Markdown if .md; PDF if .pdf (needs
                               pdfport.nvim, optional dependency)
  :RATelemetry export-all <dir>  one Markdown file per namespace found on
                               disk into <dir> — not limited to this
                               session's live instances
  :RATelemetry open [ns]       render + open externally
  :RATelemetry compare [ns] [days]  "this window vs the one before it"
  :RATelemetry startup [top]   which module a plugin's startup cost sits in
  :RATelemetry cost            startup cost vs. call count, worst first

  :RATelemetry snapshot <ns> [name]  save a named, device-tagged capture
  :RATelemetry snapshots <ns>  list ns's saved snapshots, newest first
  :RATelemetry snapshot-compare <ns> <a> <b>  diff two named snapshots'
                               call counts directly (not a calendar window)

  :RATelemetryStartAll         standalone alias for :RATelemetry start (bare)
  :RATelemetryStopAll          standalone alias for :RATelemetry stop (bare)
  :RATelemetryResetAll         standalone alias for :RATelemetry reset
                               (bare) -- same backup prompt

  :RATelemetrySetupAll         for every plugin opts.telemetry.plugins
                               configures that is loaded right now: back up
                               existing data (one prompt for the whole run),
                               reset, re-wrap (picks up any submodule loaded
                               after the first wrap -- the fix if some
                               functions never show argument data), start
                               with that plugin's own profile_args/timing
  :RATelemetrySetupAllFull     same as SetupAll, forcing profile_args and
                               timing on for every plugin regardless of its
                               own configured policy
Startup attribution (|:RATelemetry| startup) is opt-in, from this
plugin's own lazy.nvim spec — init runs for every plugin before lazy
loads any of them, so it is early enough, and it disappears with the spec
if you ever remove the plugin:

  {
    "StefanBartl/runtime-analysis.nvim",
    init = function()
      require("runtime-analysis.telemetry.startup").autostart()
    end,
  }
It wraps the global require, times every cache miss (a package.loaded
hit is not a load), and reports self-vs-total time per module and per
module root, then stops itself at UIEnter. Only modules required after
it starts are ever seen. See the telemetry README for the full set of
honest limits, and for why this is an init hook rather than a line in
your own init.lua.

:RATelemetry cost combines startup with a live instance's own call
count into one number: what a plugin costs to load versus how much it is
used. The join is on real module paths (resolved_modules()), never a
name comparison between a startup module root and a telemetry namespace
— those are only sometimes the same string, and guessing would risk
attributing one plugin's cost to another. startup_ms reads "unknown",
not "0", whenever no real module path can be matched. See the telemetry
README's own "Plugin cost vs. use" section for the full reasoning.

Reading a namespace does not need an editor session at all —
scripts/telemetry.lua in the repository is a headless CLI counterpart,
built on the same telemetry.load() a live instance never had to exist
for:

  nvim --headless -l scripts/telemetry.lua report <namespace>
  nvim --headless -l scripts/telemetry.lua export <namespace> <path>
export's format is inferred from <path>'s own extension, the same rule
|:RATelemetry| export uses. No coverage subcommand here: "which
registered functions were never called" needs a live instance's own
wrap() calls to know the registered set, which a cold telemetry.load()
read cannot answer.

Measuring instrumentation's own overhead — a separate, one-time script,
never a runtime feature (see the "not a general
profiler" thesis for why that distinction matters):

  nvim --headless -l scripts/bench_overhead.lua
  nvim --headless -l scripts/bench_overhead.lua --calls=1000000
Run it yourself for numbers specific to your own machine — see the
telemetry README's "Off costs nothing — literally" section for what it
measures and why it is shaped as a script rather than a command.

                                                      *runtime-analysis-bench*

Benchmark comparisons

runtime-analysis.bench answers a different question from telemetry
itself: not "how often was this called", but "which of these candidate
functions is actually faster, right now, on this machine". Deliberately
NOT built on telemetry's own wrap/count machinery — the overhead numbers
above (~10-700ns per call depending on features) would swamp the very
comparison a benchmark exists to make. Every candidate is called
directly, unwrapped.
  local bench = require("runtime-analysis.bench")

  local result = bench.compare({
    { name = "impl_a", fn = impl_a },
    { name = "impl_b", fn = impl_b },
  }, { iterations = 10000 })

  print(result.fastest)
  for _, line in ipairs(bench.lines(result)) do
    print(line)
  end
opts.warmup (default min(100, iterations)) runs each candidate
untimed first, so comparison order does not bias the result. Pure Lua
API, no usercmd — a candidate is a real function value, and there is no
command-line shape for "type the two closures you want compared".
Compare-now only: no persistence, no named runs. See
docs/FEATURES/BENCH.md and the decision record in docs/FEATURE_LOG.md
(§3.6).

7. INTEGRATION WITH documentation.nvim *runtime-analysis-integration*

M.open_request(lines) is this plugin's one public integration surface,
written against a small named interface rather than another plugin reaching
into this one's internal files (per documentation.nvim's docs/ECOSYSTEM.md
§7). documentation.nvim's |:DocBrowse| Endpoints mode is the first consumer:
pressing gs on a route it found by static analysis calls
require("runtime-analysis").open_request({"METHOD path", ""}) — a soft
dependency (pcall(require, "runtime-analysis")), absent with a clear
message when this plugin is not installed.

Deliberately not an immediate send: a route's path (/users/:id) is
relative and may contain unfilled :params — genuinely nothing
documentation.nvim's static analysis could send correctly on its own — so
the method and path are pre-filled and the reader completes the base URL
(and any params) before running |:RASend| themselves.
  local M = {}

  ---@field opts { split: string, request_filetype: string, deps_popup: boolean, history_max_entries: integer }
  ---@field open_request fun(lines?: string[])
  ---@field setup fun(opts?: table)
                                                    *runtime-analysis-loaded*

Loaded-vs-declared

runtime-analysis.loaded answers what documentation.nvim's own static
scan structurally cannot: what is actually on a module's table right now,
in the current process — as opposed to what the source *declares*.
M.functions(module_id)/M.is_loaded(module_id) are the live read;
documentation.nvim's own core/loaded_diff.lua joins them against its IR
for |:DocBrowse| loaded mode.

The one honest limit that shapes all of it: this reads package.loaded in
the current process only. A tree analyzed from a process that never loaded
it sees nothing, which must render as "not loaded here", never as
"declared but dead".

|:RA-loaded-snapshot| persists what a live read like the above would say,
under a name, so it can be read later — or from a different process
entirely. documentation.nvim's Loaded Analysis panel (:DocMap serve,
GET /api/loaded) is exactly that second process: a server route
answering a browser tab has no live package.loaded of its own, so it
only ever reads named snapshots, never a live aggregate the way its
Telemetry panel counterpart can.

8. HEALTH CHECK *runtime-analysis-health*

                                            *:checkhealth-runtime-analysis*
  :checkhealth runtime-analysis
Reports: Neovim version, curl on PATH, every required lib.nvim module
loading, live telemetry instances and persistently disabled namespaces in
this session, the telemetry cache directory and whether it is writable,
this project's history entry count, this project's defined environments
and which one (if any) is active, whether keymap/command usage tracking
is running and how many presses it has counted so far, whether mdview.nvim
and lib.nvim.progress are available, and whether setup() has registered
its commands yet in this session.

9. FURTHER READING *runtime-analysis-roadmap*

What has shipped, and why: docs/FEATURE_LOG.md. Ideas
that only exist between this plugin and its siblings (documentation.nvim,
mdview.nvim, lib.nvim): docs/IDEAS.md.