Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dune build -w memory usage grows without bounds when using OCaml syntax in dune files #6900

Closed
copy opened this issue Jan 18, 2023 · 14 comments · Fixed by #7894 or ocaml/opam-repository#24013
Labels
Milestone

Comments

@copy
Copy link

copy commented Jan 18, 2023

Expected Behavior

dune build -w memory usage should stay below some upper bound over a longer period of time.

Actual Behavior

dune build -w grows without bounds.

Reproduction

Minimal example:
% cat test.ml 
% cat dune
(* -*- tuareg -*- *)

open StdLabels
open Jbuild_plugin.V1

let () =
  Printf.ksprintf send {|
(executable
 (libraries stdlib-shims)
 (name test))
|}
% cat dune-project 
(lang dune 3.0)
% dune build -w test.exe

In another terminal:

while true; do; echo >> test.ml; sleep 0.1; done
More realistic example:
  • In any bigger project, run git clone https://github.com/ocaml/stdlib-shims.git && git clone https://github.com/mirage/bigarray-compat.git (I believe the presence of dune files containing OCaml syntax is sufficient to trigger their execution and this memory leak)
  • Use dune build -w and make some normal edits
  • Observe that the memory usage increases on every edit (in my project, about 25MB per build)

Specifications

  • Version of dune (output of dune --version): 3.6.2
  • Version of ocaml (output of ocamlc --version): 4.14.0 or 5.0
  • Operating system (distribution and version): Linux 6.1.6

Additional information

dune --trace-file output:

Screenshot_2023-01-18_20-02-57

Memtrace output (added to dune 3.6.2 in bin/main.ml):

Screenshot_2023-01-18_20-04-14

@emillon
Copy link
Collaborator

emillon commented Jan 31, 2023

Thanks for the great report and repro case. I was able to reproduce and bisect this down to d3296a6 (#5877). I'm not familiar enough with the Memo API to go further - @rgrinberg do you know what could be causing this?

@emillon
Copy link
Collaborator

emillon commented Jan 31, 2023

I naively tried to replace ( == ) with structural equality and add ~cutoff arguments to both instances of Memo.create in Dune_load but this does not seem to fix this.

@rgrinberg
Copy link
Member

Thanks for taking a look. Changing equality doesn't help it's only for hash collisions.

I think the way to go about it is just to change the memoization store to stop storing old values. We're currently using a hash table, but I suppose we only need the last value for every dune file path we evaluate.

@emillon
Copy link
Collaborator

emillon commented Jan 31, 2023

Do you mean using Memo.create_with_store? I'll try to do that.

@rgrinberg
Copy link
Member

Do you mean using Memo.create_with_store? I'll try to do that.

Yes. The idea is to use a Context_name.Map.t as a store and only store the last result. Note that you still need to take and make sure the last result is up to date wrt your input. But that's easy enough with structural/physical equality.

@emillon
Copy link
Collaborator

emillon commented Feb 1, 2023

Before trying that I tried to print the sizes of the tables used by the two call sites of Memo.create in Dune_load and the tables do not have more than 1 entry. Is this expected? (I modified Memo.create to return the table, and inspect Table.length before each Memo.exec)

@rgrinberg rgrinberg added the bug label Feb 21, 2023
@rgrinberg
Copy link
Member

Before trying that I tried to print the sizes of the tables used by the two call sites of Memo.create in Dune_load and the tables do not have more than 1 entry. Is this expected? (I modified Memo.create to return the table, and inspect Table.length before each Memo.exec)

That would be expected. The hashing function for this function is poor, so all the entries would go into a single bucket.

@emillon
Copy link
Collaborator

emillon commented Feb 22, 2023

I don't think I understand how Table works. I thought that the hash part was just an optimization and that it allowed storing keys that have the same hash (but in a sort of linked list). What does length return? The number of buckets or the total number of bindings?

My assumption with this issue is that every time the file is modified, a new binding is added to the table, and the table is never cleared. This is what I wanted to check with length. Does that sound reasonable?

@Alizter
Copy link
Collaborator

Alizter commented Feb 22, 2023

Would we ever want to trim our memo table to fit some constraints? We could experiment with a cache replacement policy.

@rgrinberg
Copy link
Member

What does length return? The number of buckets or the total number of bindings?

Never mind, it's the total number of bindings.

My assumption with this issue is that every time the file is modified, a new binding is added to the table, and the table is never cleared. This is what I wanted to check with length. Does that sound reasonable?

Yes, that confirms it.

@emillon
Copy link
Collaborator

emillon commented Feb 22, 2023

We discussed this and yes there must be a growing table but we don't know which one.
I'm going to have a look using, the 3 following debugging techniques have been mentioned:

  • do what I did but for other tables
  • grow the heap to 2GB or similar and eyeball the heap
  • use statmemprof.

@emillon
Copy link
Collaborator

emillon commented Feb 23, 2023

I actually used another technique: I added the sizes of the memo tables to the trace-file output. It shows several growing tables:

  • dir-contents-get0 (1 entry per run)
  • execute-rule (8 entries per run)
  • lib-instantiate (1 entry per run)

(for comparison, the same test on dune itself does not show any growing table)

I'll have a look at the input/outputs of these tables.

@emillon
Copy link
Collaborator

emillon commented Feb 23, 2023

For some of these tables, the input is compared using physical equality (==) (ex: to compare Super_context.t). This means that the previous versions are kept in the table.

For rules (inputs of execute-rule) I'm surprised to see that all rules are kept, even the ones that correspond to former states of the dune file. What is the best way to fix this?

@rgrinberg
Copy link
Member

You can try switching the store for libraries and see if that helps. We don't really need old libs and they're quick enough to construct.

For rules (inputs of execute-rule) I'm surprised to see that all rules are kept, even the ones that correspond to former states of the dune file. What is the best way to fix this?

That's not necessarily bad for rules. It's often the case that people go back and forth between a few states.

@rgrinberg rgrinberg linked a pull request Jun 5, 2023 that will close this issue
@rgrinberg rgrinberg added this to the 3.9.0 milestone Jun 5, 2023
emillon added a commit to emillon/opam-repository that referenced this issue Jun 23, 2023
CHANGES:

- Validate file extension for `$ dune ocaml top-module`. (ocaml/dune#8005, fixes ocaml/dune#8004, @3Rafal)

- Include the time it takes to read/write state files when `--trace-file` is
  enabled (ocaml/dune#7960, @rgrinberg)

- Add `dune show` command group which is an alias of `dune describe`. (ocaml/dune#7946,
  @Alizter)

- Include source tree scans in the traces produced by `--trace-file` (ocaml/dune#7937,
  @rgrinberg)

- Cinaps: The promotion rules for cinaps would only offer one file at a time no
  matter how many promotions were available. Now we offer all the promotions at
  once (ocaml/dune#7901, @rgrinberg)

- Do not re-run OCaml syntax files on every iteration of the watch mode. This
  is too memory consuming. (ocaml/dune#7894, fix ocaml/dune#6900, @rgrinberg)

- Remove some compatibility code for old version of dune that generated
  `.merlin` files. Now dune will never remove `.merlin` files automatically
  (ocaml/dune#7562)

- Add `dune show env` command and make `dune printenv` an alias of it. (ocaml/dune#7985,
  @Alizter)

- Add additional metadata to the traces provided by `--trace-file` whenever
  `--trace-extended` is passed (ocaml/dune#7778, @rleshchinskiy)

- Extensions used in `(dialect)` can contain periods (e.g., `cppo.ml`). (ocaml/dune#7782,
  fixes ocaml/dune#7777, @nojb)

- Allow `(include_subdirs qualified)` to be used when libraries define a
  `(modules ...)` field (ocaml/dune#7797, fixes ocaml/dune#7597, @anmonteiro)

- `$ dune describe` is now a command group, so arguments to subcommands must be
  passed after subcommand itself. (ocaml/dune#7919, @Alizter)

- The `interface` and `implementation` fields of a `(dialect)` are now optional
  (ocaml/dune#7757, @gpetiot)

- Add commands `dune show targets` and `dune show aliases` that display all the
  available targets and aliases in a given directory respectively. (ocaml/dune#7770,
  grants ocaml/dune#265, @Alizter)

- Allow multiple globs in library's `(stdlib (internal_modules ..))`
  (@anmonteiro, ocaml/dune#7878)

- Attach melange rules to the default alias (ocaml/dune#7926, @haochenx)

- In opam constraints, reject `(and)` and `(or)` with no arguments at parse
  time (ocaml/dune#7730, @emillon)

- Compute digests and manage sandboxes in background threads (ocaml/dune#7947,
  @rgrinberg)

- Add `(build_if)` to the `(test)` stanza. When it evaluates to false, the
  executable is not built. (ocaml/dune#7899, fixes ocaml/dune#6938, @emillon)

- Add necessary parentheses in generated opam constraints (ocaml/dune#7682, fixes ocaml/dune#3431,
  @Lucccyo)
emillon added a commit to emillon/opam-repository that referenced this issue Jun 28, 2023
CHANGES:

- Validate file extension for `$ dune ocaml top-module`. (ocaml/dune#8005, fixes ocaml/dune#8004, @3Rafal)

- Include the time it takes to read/write state files when `--trace-file` is
  enabled (ocaml/dune#7960, @rgrinberg)

- Add `dune show` command group which is an alias of `dune describe`. (ocaml/dune#7946,
  @Alizter)

- Include source tree scans in the traces produced by `--trace-file` (ocaml/dune#7937,
  @rgrinberg)

- Cinaps: The promotion rules for cinaps would only offer one file at a time no
  matter how many promotions were available. Now we offer all the promotions at
  once (ocaml/dune#7901, @rgrinberg)

- Do not re-run OCaml syntax files on every iteration of the watch mode. This
  is too memory consuming. (ocaml/dune#7894, fix ocaml/dune#6900, @rgrinberg)

- Add `--all` option to `dune rpc status` to show all Dune RPC servers running.
  (ocaml/dune#8011, fix ocaml/dune#7902, @Alizter)

- Remove some compatibility code for old version of dune that generated
  `.merlin` files. Now dune will never remove `.merlin` files automatically
  (ocaml/dune#7562)

- Add `dune show env` command and make `dune printenv` an alias of it. (ocaml/dune#7985,
  @Alizter)

- Add additional metadata to the traces provided by `--trace-file` whenever
  `--trace-extended` is passed (ocaml/dune#7778, @rleshchinskiy)

- Extensions used in `(dialect)` can contain periods (e.g., `cppo.ml`). (ocaml/dune#7782,
  fixes ocaml/dune#7777, @nojb)

- Allow `(include_subdirs qualified)` to be used when libraries define a
  `(modules ...)` field (ocaml/dune#7797, fixes ocaml/dune#7597, @anmonteiro)

- `$ dune describe` is now a command group, so arguments to subcommands must be
  passed after subcommand itself. (ocaml/dune#7919, @Alizter)

- The `interface` and `implementation` fields of a `(dialect)` are now optional
  (ocaml/dune#7757, @gpetiot)

- Add commands `dune show targets` and `dune show aliases` that display all the
  available targets and aliases in a given directory respectively. (ocaml/dune#7770,
  grants ocaml/dune#265, @Alizter)

- Allow multiple globs in library's `(stdlib (internal_modules ..))`
  (@anmonteiro, ocaml/dune#7878)

- Attach melange rules to the default alias (ocaml/dune#7926, @haochenx)

- In opam constraints, reject `(and)` and `(or)` with no arguments at parse
  time (ocaml/dune#7730, @emillon)

- Compute digests and manage sandboxes in background threads (ocaml/dune#7947,
  @rgrinberg)

- Add `(build_if)` to the `(test)` stanza. When it evaluates to false, the
  executable is not built. (ocaml/dune#7899, fixes ocaml/dune#6938, @emillon)

- Add necessary parentheses in generated opam constraints (ocaml/dune#7682, fixes ocaml/dune#3431,
  @Lucccyo)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment