Skip to content
This repository has been archived by the owner on Jan 25, 2024. It is now read-only.

Enhance completion details for builtins if Nix 2.4 is available #27

Merged
merged 2 commits into from
Mar 16, 2021

Conversation

Ma27
Copy link
Member

@Ma27 Ma27 commented Feb 19, 2021

Note: this isn't completed ready yet:

  • There are a bit too many .unwrap()-calls where better handling may be useful if e.g. the format of nix __dump-builtins changes in the future.
  • I didn't test this very thoroughly. I guess I'll use this for a bit in my neovim setup to check if I encounter any issues.

A hidden feature of Nix 2.4+ (a.k.a. nixUnstable) is nix __dump-builtins which returns a JSON giving details about built in
functions.

This change basically does the following things:

  • It checks whether Nix 2.4 is available on $PATH[1]. In that case
    built-in functions are retrieved via nix __dump-builtins[2].

    If not, the hard-coded list of builtins is used as a fallback.

  • If builtins can be retrieved from Nix, the following additional things
    are added to CompletionItem:

    • The documentation[3] field containing markdown documentation for the
      function is added. This will be shown in editors as one is used to
      it from e.g. doc-block comments in other languages.

    • A very basic parameter info is provided. E.g. for
      builtins.removeAttrs the popup shows Lambda: set -> list -> Result. While this is fairly basic, it's IMHO a small benefit to
      make sure one's not forgetting the order of parameters.

      Please note that this will currently display wrong info if
      a built-in is called via lib.flip.

    • If **DEPRECATED.** is at the beginning of documentation, the
      built-in is marked in the LSP response as deprecated. While this
      is a gross hack, it ensures that the only deprecated builtin
      (builtins.toPath) is actually marked as such.

[1] Please note that this means that you can wrap $PATH for rnix-lsp
to have this feature without being forced to use nixUnstable on
your system.
[2] As this is only an optional feature (and hence a loose dependency to
Nix) I decided against using an FFI and thus keeping the build
process rather simple.
[3] https://microsoft.github.io/language-server-protocol/specification#textDocument_completion


cc @jD91mZM2

Ma27 and others added 2 commits February 19, 2021 16:56
A hidden feature of Nix 2.4+ (a.k.a. `nixUnstable`) is `nix
__dump-builtins` which returns a JSON giving details about built in
functions.

This change basically does the following things:

* It checks whether Nix 2.4 is available on `$PATH`[1]. In that case
  built-in functions are retrieved via `nix __dump-builtins`[2].

  If not, the hard-coded list of builtins is used as a fallback.

* If builtins can be retrieved from Nix, the following additional things
  are added to `CompletionItem`:

  * The `documentation`[3] field containing markdown documentation for the
    function is added. This will be shown in editors as one is used to
    it from e.g. doc-block comments in other languages.

  * A very basic parameter info is provided. E.g. for
    `builtins.removeAttrs` the popup shows `Lambda: set -> list ->
    Result`. While this is fairly basic, it's IMHO a small benefit to
    make sure one's not forgetting the order of parameters.

    Please note that this will currently display *wrong* info if
    a built-in is called via `lib.flip`.

  * If `**DEPRECATED.**` is at the beginning of `documentation`, the
    built-in is marked in the LSP response as `deprecated`. While this
    is a gross hack, it ensures that the only deprecated builtin
    (`builtins.toPath`) is actually marked as such.

[1] Please note that this means that you can wrap `$PATH` for `rnix-lsp`
    to have this feature without being forced to use `nixUnstable` on
    your system.
[2] As this is only an optional feature (and hence a loose dependency to
    Nix) I decided against using an FFI and thus keeping the build
    process rather simple.
[3] https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
@Ma27
Copy link
Member Author

Ma27 commented Feb 22, 2021

Okay.... I'll really have to do some memory profiling here. I just left a neovim window open for a few days (with a Nix-based side-project in it) and now when I resumed to work on it, rnix-lsp from this branch ate up wayyyy too much RAM.

@jD91mZM2
Copy link
Member

Do you think this branch is to blame, or rnix-lsp as a whole? We might have a leak somewhere, not sure where but I wouldn't be surprised

@Ma27
Copy link
Member Author

Ma27 commented Feb 23, 2021

I have no idea. Just realized that rnix-lsp from this branch ate up a lot of my memory, but I didn't investigate it further (was already too late in my TZ :))

Since I haven't tested master with rnix/nixpkgs-fmt/rowan updates before, that might be the issue as well, of course. But first, I'll need to find a reliable way to reproduce this.

@Ma27
Copy link
Member Author

Ma27 commented Feb 26, 2021

Damn, I begin to regret that I pkilled rnix-lsp recently since I cannot reproduce this anymore :/
Do you have an idea how to proceed?

@jD91mZM2
Copy link
Member

jD91mZM2 commented Feb 27, 2021

If you have patience, try opening a large file like all-packages.nix and leave it for some time. Otherwise, let's ignore it and fix it later if needed. My motto is, it's not a bug if it only happens once ;)

@Ma27
Copy link
Member Author

Ma27 commented Mar 12, 2021

@jD91mZM2 so I can occasionally reproduce this, but I didn't find a reasonable pattern yet. Also, sometimes neovim crashed with an out-of-memory error (which shouldn't be caused by rnix-lsp itself though as it's running in its own process IIRC). I'm using nightly though, so it may be because of that. However, editing e.g. all-packages.nix is also pretty painful when having rnix-lsp at latest master rather than on my branch.

Since I'm currently missing time & energy for a technical deep-dive in another project, I'm not really sure if I find something meaningful. Could you check the performance of current master (and probably this PR) to confirm my assumption?

If that's true, we could consider this mergable (and investigate the issues in another ticket), though I think we may want to add some caching to the builtins implementation first :)

@jD91mZM2
Copy link
Member

Could you check the performance of current master (and probably this PR) to confirm my assumption?

I'm already aware all-packages.nix is painfully slow, if that's what you're asking. I don't know if I need to implement incremental parsing or if we should ask rowan to use arenas instead of simple Box<T>s everywhere. Once I made an experiment which implemented Rowan's API using arenas, and it was like 10x as fast lol

Give your mark I understood your question properly, and I'll merge it!

@Ma27
Copy link
Member Author

Ma27 commented Mar 14, 2021

Once I made an experiment which implemented Rowan's API using arenas, and it was like 10x as fast lol

That sounds nice :)

Regarding the merge: we may want to do some caching for the builtins here, but I didn't check whether this makes a difference and I shouldn't start another procrastination-project while actually learning for exams :D

So if you're fine with the current state, we could merge this, see if your optimizations are sufficient and then decide whether this needs optimization or not. If you agree with this, feel free to merge :)

@jD91mZM2 jD91mZM2 merged commit fd1ad49 into nix-community:master Mar 16, 2021
@jD91mZM2
Copy link
Member

Massive thanks for this, man! You're amazing

@Ma27 Ma27 deleted the unstable-builtins branch March 23, 2021 09:57
@Ma27
Copy link
Member Author

Ma27 commented Mar 25, 2021

@jD91mZM2 so I guess incremental parsing is the way to go:

  • I did some strace-debugging of this and found out that nvim spams the socket opened by lsp-server with contents if the file is very big, so we don't even get to the point where completion happens.
  • As we re-request the full content on sync, this seems to happen on many keystrokes (I've seen that multiple blocks where transmitted several times).
  • Occasionally attr-path completion happens there (though I guess that this might be due to a "coincidence" - an older request sends stuff back that matches to the current cursor position), so this also contributes to the high load.

I think that incremental parsing wouldn't just save memory, it could also make rnix-lsp more responsive. As you probably know, just switching the sync strategy to Incremental isn't sufficient.

Do you want to take care of this at some point or should I? Though I'm not sure when/whether I'll get to this :)

@ony
Copy link

ony commented Mar 29, 2021

I think I was able to reproduce memory leak. See #33 . Testing now on prev commit 6c12d7f .

@Ma27
Copy link
Member Author

Ma27 commented Mar 29, 2021

@jD91mZM2 okay, even though I can't reproduce it too often, @ony had probably more luck, hence I'd suggest to revert for now and see if it gets better (otherwise the problem may be related to the most recent crate updates).

We should probably work on incremental parsing here first before we re-integrate this.

@ony
Copy link

ony commented Mar 29, 2021

Just reviewed this PR and I see no change that can be suspected for memory leak. My only concern is #load_builtins() that might be called multiple times resulting in many spawned nix --version (I'm on 2.3.10) and some mem thrashing with regexp re-allocation/compilation.

I don't think leak is caused by this PR.

@Ma27
Copy link
Member Author

Ma27 commented Mar 29, 2021

Not so sure about that, let me explain:

  • Your editor notifies rnix-lsp about changes (i.e. sends stuff on (almost) each keystroke)
  • There are two strategies, an incremental notification (i.e. only changes) or a full one (the entire file is written)
  • Only the latter option is supported atm, so parsing etc. has to be re-done

What I'm not so sure about is if this change made things significantly worse or whether updates of the parser and stuff like this are the root-cause.

That's why I filed #34 to give you a revision without this change to test. As you seem to hit this more often than I am, it'd be awesome if you could check whether this change makes things better.

Ma27 added a commit to Ma27/rnix-lsp that referenced this pull request Apr 22, 2021
With incremental parsing, the editor only sends a "diff" of what has
changed rather than the full file. For instance, it's pretty pointless
to send all of `all-packages.nix` from `nixpkgs` for a single-letter
change.

Given the benchmark of `rnix-parser`[1] it doesn't seem as the parsing
itself is the bottleneck a few people have reported[2], so this change
only replaces affected lines and re-parses everything using
`rnix-parser`.

While the current change seems usable on local tests, it has (at
least) two known issues:

* As soon as you type `inherit`, the LSP stalls and it seems it's due to
  `rnix-parser` and steadily allocates more memory according to
  `strace(1)`. I confirmed that it's indeed a problem of `rnix-parser`
  by creating a file with `{ inherit` as content and the LSP refuses to
  respond (with incremental parsing disabled).

* Given an expression like this:

  ``` nix
  let a = 1; in

  {
    b = a;
  }
  ```

  When deleting line one and then line 2, the state ends up as `{{ }`.
  This is because the range passed to LSP looks like this:

  ```
  Range {
    start: Position {
      line: 1,
      character: 0
    },
    end: Position {
      line: 2,
      character: 0
    }
  }
  ```

  This also kills the second line of the expression above and as soon as
  you delete this as well, the state will end up with bogus results.

  It's a bit weird though since I got an equal message when deleting
  more lines, so I'm not entirely sure if that's just a bug in the LSP
  implementation of my neoVIM (and also the reason why I didn't just
  insert a `\n` at the right vec entries).

[1] https://github.com/nix-community/rnix-parser/blob/v0.9.0/benches/all-packages.rs
[2] nix-community#27 (comment)
Ma27 added a commit that referenced this pull request Jun 19, 2021
* Enable incremental parsing via the LSP protocol

With incremental parsing, the editor only sends a "diff" of what has
changed rather than the full file. For instance, it's pretty pointless
to send all of `all-packages.nix` from `nixpkgs` for a single-letter
change.

Given the benchmark of `rnix-parser`[1] it doesn't seem as the parsing
itself is the bottleneck a few people have reported[2], so this change
only replaces affected lines and re-parses everything using
`rnix-parser`.

While the current change seems usable on local tests, it has (at
least) two known issues:

* As soon as you type `inherit`, the LSP stalls and it seems it's due to
  `rnix-parser` and steadily allocates more memory according to
  `strace(1)`. I confirmed that it's indeed a problem of `rnix-parser`
  by creating a file with `{ inherit` as content and the LSP refuses to
  respond (with incremental parsing disabled).

* Given an expression like this:

  ``` nix
  let a = 1; in

  {
    b = a;
  }
  ```

  When deleting line one and then line 2, the state ends up as `{{ }`.
  This is because the range passed to LSP looks like this:

  ```
  Range {
    start: Position {
      line: 1,
      character: 0
    },
    end: Position {
      line: 2,
      character: 0
    }
  }
  ```

  This also kills the second line of the expression above and as soon as
  you delete this as well, the state will end up with bogus results.

  It's a bit weird though since I got an equal message when deleting
  more lines, so I'm not entirely sure if that's just a bug in the LSP
  implementation of my neoVIM (and also the reason why I didn't just
  insert a `\n` at the right vec entries).

[1] https://github.com/nix-community/rnix-parser/blob/v0.9.0/benches/all-packages.rs
[2] #27 (comment)

* Don't swallow empty lines if the previous one (including the `\n` separator) gets deleted

* Simplify (& hopefully fix) incremental parsing

Rather than trying to mess around in an existing datastructure and thus
running into tons of mean special cases since you have to take care of
removing/adding lines on your own, we just write everything to a fresh
string and use this for parsing and inside the HashMap containing the
AST of each known Nix expression.

Not sure if it works out the way I hope, but it seems a little bit
better now. Will test it a while on my setup before I consider this
ready.

* Fix newlines
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants