Skip to content

Conversation

zth
Copy link
Member

@zth zth commented Sep 4, 2025

This introduces OCaml multicore (Eio) to the analysis bin, and leverages it for 2 of the editor tooling commands:

  • rename
  • Find all references

Benching this on my local machine, in a repo of ~350k lines of ReScript and ~1800 files, both commands end up about 2.5x faster.

@zth zth requested a review from Copilot September 4, 2025 10:29
Copilot

This comment was marked as outdated.

Copy link
Member

@nojaf nojaf left a comment

Choose a reason for hiding this comment

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

Exciting stuff! Do you have any good resources on Eio?
I looked at it a while ago and didn't really find any beginner friendly stuff on it. The main repo was an overwhelming read for me.

I hope we can also integrate eio-trace and opentelemetry at some point.

print_endline
(if allLocs = [] then Protocol.null
else "[\n" ^ (allLocs |> String.concat ",\n") ^ "\n]")
Eio_main.run (fun env ->
Copy link
Member

Choose a reason for hiding this comment

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

Would a future goal be to have this Eio_main at the main entry point level of analysis tool?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup! I moved it there now as well, but just for the relevant sub commands.


let forLocalStamp ~full:{file; extra; package} stamp (tip : Tip.t) =
(* Single helper for parallel work distribution over a list. *)
let parallel_map ~domain_mgr ~items ~f =
Copy link
Member

Choose a reason for hiding this comment

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

A little wild that this isn't somewhere part of the Eio library.

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe it is. This one sprung from trying to tune the exact use case in this PR a bit. If something better exists we can switch to that as we uncover more types of "loads" as we expand using this.

Copy link

pkg-pr-new bot commented Sep 5, 2025

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript-lang/rescript@7840

@rescript/darwin-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-arm64@7840

@rescript/darwin-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-x64@7840

@rescript/linux-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-arm64@7840

@rescript/linux-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-x64@7840

@rescript/runtime

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/runtime@7840

@rescript/win32-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/win32-x64@7840

commit: 4d74fcc

Comment on lines -61 to -66
# Verify that the compiler still builds with the oldest OCaml version we support.
- os: ubuntu-24.04
ocaml_compiler: ocaml-variants.4.14.2+options,ocaml-option-static
node-target: linux-x64
rust-target: x86_64-unknown-linux-musl

Copy link
Member Author

Choose a reason for hiding this comment

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

This was discussed in Discord that it's OK for us to move to 5.3.0+.

dune-project Outdated
(synopsis "ReScript compiler")
(depends
(ocaml
(>= 5.3))))
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need that here now?

Copy link
Member Author

Choose a reason for hiding this comment

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

Turns out we don't. Removed.

run: opam exec -- dune build --display quiet --profile static
run: |
arch=$(dpkg-architecture -qDEB_HOST_MULTIARCH)
C_INCLUDE_PATH="/usr/include:/usr/include/$arch" CPATH="/usr/include:/usr/include/$arch" opam exec -- dune build --display quiet --profile static
Copy link
Member

Choose a reason for hiding this comment

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

Rather than duplicating these env var definitions, maybe we could have a single step that sets them in GITHUB_ENV?

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed.

@zth zth changed the title [WIP] Use Eio for parallelizing some analysis commands Use Eio for parallelizing some analysis commands Sep 5, 2025
@zth zth marked this pull request as ready for review September 5, 2025 10:35
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces OCaml multicore (Eio) parallelization to the ReScript analysis bin, targeting the rename and find all references commands for approximately 2.5x performance improvements on large codebases.

Key changes:

  • Updates OCaml version requirement from 4.14 to 5.3 across all packages
  • Adds Eio dependencies for multicore support
  • Implements parallel processing for reference finding operations
  • Adds thread-safe access to shared state using mutexes

Reviewed Changes

Copilot reviewed 13 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
dune-project Updates OCaml version requirement and adds Eio dependencies
*.opam files Generated package files with updated OCaml version and Eio deps
analysis/src/dune Adds Eio library dependency
analysis/src/SharedTypes.ml Adds StateSync module with mutex for thread-safe access
analysis/src/References.ml Implements parallel_map function and updates reference finding to use multicore
analysis/src/ProcessCmt.ml Adds thread-safe caching with double-checked locking
analysis/src/Packages.ml Updates package retrieval functions to use thread-safe access
analysis/src/Commands.ml Updates command functions to accept Eio environment and use parallel operations
.github/workflows/ci.yml Updates CI to use OCaml 5.3 and removes 4.14 compatibility check
CHANGELOG.md Documents the new multicore feature

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines 445 to 448
if doms <= 1 || len < small_threshold then f items
else
let chunk_count = min doms len in
let chunk_size = max 1 ((len + chunk_count - 1) / chunk_count) in
Copy link
Preview

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

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

The magic number 10 for small_threshold should be extracted to a named constant or configuration parameter to improve maintainability and make the threshold adjustable.

Copilot uses AI. Check for mistakes.

if: runner.os == 'Linux'
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
with:
# See https://github.com/ocaml/setup-ocaml/blob/b2105f9/packages/setup-ocaml/src/unix.ts#L9
Copy link
Preview

Copilot AI Sep 5, 2025

Choose a reason for hiding this comment

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

The addition of linux-libc-dev dpkg-dev packages should include a comment explaining why these specific packages are needed for the OCaml 5.3/Eio build requirements.

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you fix this Copilot?

Copy link
Member

@nojaf nojaf left a comment

Choose a reason for hiding this comment

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

Looks good to me, I don't know enough about eio, so I will let the others do the approval here.

| [_; "format"; path] ->
Printf.printf "\"%s\"" (Json.escape (Commands.format ~path))
| [_; "test"; path] -> Commands.test ~path
| [_; "test"; path] -> Eio_main.run (fun env -> Commands.test ~env ~path)
Copy link
Member

Choose a reason for hiding this comment

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

This is used because reference tests require the env now right?
The tests themselves are still sequential, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

The specific tests of commands that use Eio will still use it in the tests as well, but yeah, the tests themselves are sequential.

@cknitt
Copy link
Member

cknitt commented Sep 5, 2025

Could you address Copilot's comments? Other than that, looks good to me! 👍

Copy link
Collaborator

@cristianoc cristianoc left a comment

Choose a reason for hiding this comment

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

I thought it was all straightforward until I saw the locks.
Now the question:
why did you add locks and how do you know that we don't have deadlocks (locking too much) or data races (some locks are still missing)

Worth spending some time of this ahead of time before getting into bugs that are very hard to reproduce.

~pos:(int_of_string line, int_of_string col)
~debug
Eio_main.run (fun env ->
Commands.references ~env ~path
Copy link
Collaborator

Choose a reason for hiding this comment

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

why does references need to take an env now and not before?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because it's using Eio, and that requires the Eio env.

@zth
Copy link
Member Author

zth commented Sep 6, 2025

I thought it was all straightforward until I saw the locks. Now the question: why did you add locks and how do you know that we don't have deadlocks (locking too much) or data races (some locks are still missing)

Worth spending some time of this ahead of time before getting into bugs that are very hard to reproduce.

There were races in accessing/writing to the cmt cache, that caused random failures of the command. I'll do another round of reasoning about the locks etc.

@cristianoc
Copy link
Collaborator

I thought it was all straightforward until I saw the locks. Now the question: why did you add locks and how do you know that we don't have deadlocks (locking too much) or data races (some locks are still missing)
Worth spending some time of this ahead of time before getting into bugs that are very hard to reproduce.

There were races in accessing/writing to the cmt cache, that caused random failures of the command. I'll do another round of reasoning about the locks etc.

How about making sure that transitively no mutable global state is accessed? Is that too hard to establish?

@zth
Copy link
Member Author

zth commented Sep 6, 2025

How about making sure that transitively no mutable global state is accessed? Is that too hard to establish?

No that's probably a great idea. I'll give it a shot! It's great if we figure these things out now, because this type of Eio thing will hopefully be the foundation of quite a few things going forward.

@cristianoc
Copy link
Collaborator

How about making sure that transitively no mutable global state is accessed? Is that too hard to establish?

No that's probably a great idea. I'll give it a shot! It's great if we figure these things out now, because this type of Eio thing will hopefully be the foundation of quite a few things going forward.

So no need for locks and things would be much safer.
Though: one needs to be careful not to introduce a slowdown.

@zth
Copy link
Member Author

zth commented Sep 6, 2025

@cristianoc 4352708 removes mutexes in favor of domain local caches. No meaningful regression in perf from doing this. But can't help to wonder if there's a better way.

We'll of course continue improving on this as we integrate more features with the same functionality. This first version needs to be safe and clear/understandable though of course.

@@ -0,0 +1,7 @@
(* Helpers for domain-local caches *)
Copy link
Member

Choose a reason for hiding this comment

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

Any links to what a domain-local cache even is?

Copy link
Member Author

Choose a reason for hiding this comment

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

Domains in Eio are essentially threads but managed by Eio. So, a domain local cache is a cache that's valid just for a thread (domain).

Previously, everything was sequential and within a single thread. We can now use multiple threads (domains) for parallelizing work, but that means interacting with the cache isn't safe if it's shared between all threads (races etc). So, by doing domain local caches, we dodge that issue.

For the exact tasks in this PR (rename and find references), the cache doesn't matter that much I suspect. But since it was already in place, and it's used in a bunch of other places (where we don't use Eio yet, and maybe won't ever do), it made sense to make them domain specific.

This should be "backwards compatible" too from what I understand, in that the cache will work the same way as it did before for the non-Eio stuff.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks a bunch for explaining this!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Domains in Eio are essentially threads but managed by Eio. So, a domain local cache is a cache that's valid just for a thread (domain).

Previously, everything was sequential and within a single thread. We can now use multiple threads (domains) for parallelizing work, but that means interacting with the cache isn't safe if it's shared between all threads (races etc). So, by doing domain local caches, we dodge that issue.

For the exact tasks in this PR (rename and find references), the cache doesn't matter that much I suspect. But since it was already in place, and it's used in a bunch of other places (where we don't use Eio yet, and maybe won't ever do), it made sense to make them domain specific.

This should be "backwards compatible" too from what I understand, in that the cache will work the same way as it did before for the non-Eio stuff.

Can you try something: create intentionally some shared mutable state and use domain local cache and check that they don't interfere.
Maybe in a file of only a few lines.

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.

4 participants