Skip to content

feat: use compiler tracers for project indexing#764

Open
doorgan wants to merge 4 commits into
mainfrom
doorgan/compiler-tracers-2
Open

feat: use compiler tracers for project indexing#764
doorgan wants to merge 4 commits into
mainfrom
doorgan/compiler-tracers-2

Conversation

@doorgan

@doorgan doorgan commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

This is a re-publishing of #705, which was reverted due to issues we found after merging. You can read that PR for the original description of how this works.


Adds the following fixes on top of the previous attempt:

  • Uses GenServer.cast instead of GenServer.call in the TraceBuffer, this should make compilation faster by not serializing the work done with the tracer events. This was the original intent but I missed it in the original implementation, as the timing didn't noticeable change when testing it against expert itself.
  • Stops tracing dependency compilation. The dependency tracer forced us to limit the dependency worker threads to 1, slowing down its compilation significantly. We already index dependency by reading the .beam files, which in my tests is way faster than a dependency tracer and a single worker thread, so I removed dependency tracing entirely
  • fixes module attribute references. compiler tracers don't produce events for them, so I fixed that. indexing those would have required to read source files during compilation to extract them(very slow) or to do source indexing only for them(slow too). since references for a module attribute can only appear in the file they're defined in, I opted to move their resolution to the moment the user requests them, and extract them from the ast of the currently open document.

@doorgan
doorgan force-pushed the doorgan/compiler-tracers-2 branch 4 times, most recently from eb8fb1d to d8aa44f Compare July 15, 2026 03:25
@doorgan
doorgan marked this pull request as ready for review July 15, 2026 17:39
doorgan added 4 commits July 15, 2026 14:45
The main idea of this PR is that we can use Compiler Tracers for all
compiled files like NextLS does. This allows us to index definitions and
references *as the project compiles* rather than as a separate step. As
a consequence, the indexer no longer goes over .ex files, as those are
compilable, and indexes all .beam files instead if a compiler tracer
missed them.

Compiler tracers provide all the information we need: aliases, alias
references, remote/imported functions/macros, local functions/macros,
struct expansion, and definitions(via the module's compiled bytecode),
which in principle should allow us to skip indexing from source code, or
in the worst case scenario reduce the source analysis to range
refinement, which is faster than full AST analysis.

For non-compiled files like .exs scripts and tests, we still run the
source code based indexer so we don't lose support for those.

At a high level this works as follows:
- File is saved, compilation is scheduled
- The compiler emits tracer events, we pick them up and update both the
index and the indexer manifest
- The indexer runs, skips the .beams from files already analyzed via
compiler tracer events, indexes the rest of the stale files if any

If the index is missing but the project was already compiled, we will
index from disk files instead: from .beam files for definitions since
those are generally way more complete, and from the .ex sources for
references.

---
This introduces two different compiler tracers(just like NextLS). This
is because mix compilation happens in two main stages:
1. Deps compilation via `deps.loadpaths`
2. Actual compilation of the user code

This happens both in Expert and in regular `mix compile` runs, as
`deps.loadpaths` is called internally by `mix compile`.
We need to both run a compiler tracer for dependencies, AND to
discriminate between dependency code and user code to decide wether or
not to index references. Having a DependencyTracer that is configured
only during `deps.loadpaths` and a ProjectTracer that is configured
afterwards allows us to do both things.

There is another quirk with `deps.loadpaths` as of elixir 1.20 where the
compiler options will not get propagated to the compiler workers if
`MIX_OS_DEPS_COMPILE_PARTITION_COUNT` is `>1`, fixed in elixir's main
branch with commit
`https://github.com/elixir-lang/elixir/commit/6f18bf70c07b118dc434b3fd478b420071bc487f`
and not yet released in a stable version. To workaround this issue, that
env var is forced to `1` for the duration of the compilation.

---
Later on for 0.4 we will want to investigate using the compiler locks to
share the expert build with the user builds and save repeated work. That
would be partially incompatible with compiler tracers because a user
triggered build will not send the compiler tracer events to expert. In
that case we want to also have the ability to index what's missing after
compilation, and this PR should set the foundations for that.

I'd like to know how this perform in real codebases too, as I see a
noticeable slowdown in cold compilation(mostly the `deps.loadpaths`
step), but I don't know how impactful this is on large codebases.
@doorgan
doorgan force-pushed the doorgan/compiler-tracers-2 branch from d8aa44f to 0c7992c Compare July 15, 2026 17:45
@doorgan

doorgan commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

I think the issues are fixed now, in particular find references which was reported as not working:

For a module
Screenshot 2026-07-15 at 2 49 40 PM

For a public function
Screenshot 2026-07-15 at 2 50 29 PM

For private function
Screenshot 2026-07-15 at 2 51 20 PM

For module attributes
Screenshot 2026-07-15 at 2 50 53 PM

For variables
Screenshot 2026-07-15 at 2 51 50 PM

For erlang modules
Screenshot 2026-07-15 at 2 53 02 PM

For guards
Screenshot 2026-07-15 at 2 54 22 PM

For macro generated functions
Screenshot 2026-07-15 at 2 57 46 PM

For all the above, go to definition also works.

One reported issue was that references for modules and functions were not working. This new PR does not have commits that specifically fix those, so I'm not really sure what happened in that case and I can't reproduce the problem as you see above. I'd appreciate more eyes testing this!

Comment on lines +268 to +272
defp timed(fun) when is_function(fun, 0) do
start = System.monotonic_time(:millisecond)
result = fun.()
{System.monotonic_time(:millisecond) - start, result}
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this just :timer.tc but with millis? Why not just use timed_log ?

@scohen

scohen commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Running this branch, I get "mix deps.loadpaths" as progress during deps compilation, not the actual files being compiled.

@scohen

scohen commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

again, there's a dramatic slowdown during compilation of our largest file. It's still taking ages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants