Thomas Leonard's work notes

(see roscidus.com for my main blog)

(Eio paths, Prometheus, OxCaml)

Eio

I announced the new Eio 1.4 release on discuss:

Investigated an interesting bug report from Thomas Gazagnaire:

  • Eio.Path.mkdirs fails on a relative path whose first component doesn't exist #906.

This affected the eio_posix backed when using unconfined paths, because e.g. stat fs would do fstatat(AT_FDCWD, "", ...) and that doesn't allow empty paths. That prompted me to look for similar problems and I found a few more places that confused "" and ".":

  • Fix handling of empty paths when using fs unconfined #907.

I also wondered why the CI tests often failed with ENOMEM trying to initialise uring. Mark Elvers investigated and found that RLIMIT_MEMLOCK is shared by all builds, so busy CI workers tended to fail. He's now reconfigured them to fix that, and builds are passing again.

I've also been working with Anil on adding support for Windows paths. Mostly I've been splitting his original PR into smaller pieces and reviewing and merging them. The first step here was moving the join and split operations to the backends:

  • Move path operations to backends #913.

  • Add some more tests for POSIX paths #918.

For example, on Linux the path a/b\c represents the item b\c inside the directory a, whereas on Windows it's the item c inside b inside a. Once the operations were per-backend, we could add support for NT paths:

  • Implement Eio.Path.native as a Pi backend and add Windows support #916.

(Anil has been sharing with me many interesting things about Windows paths, such as the difference between \??\ and \\?\, and the illusion of Why does each drive have its own current directory?).

I added a simple helper function to create a new path:

  • Add Eio.Path.of_dir #919.

This is perhaps slightly clearer than creating a (dir, "") tuple manually, and avoids having to think about whether to use "." or "".

While reviewing that, Anil wondered about why the backends sometimes use "" and sometimes "." for the native paths, which prompted me to clean that code up a bit:

  • Clean up dir_path handling #920.

Debian packager Stéphane Glondu reported that Eio no longer built on 32-bit platforms. I fixed that, but it's unclear whether we want to support them; both ocaml-ci and opam-repo-ci have stopped testing on 32-bit platforms, so it's hard to notice regressions:

  • eio_posix: fix build on 32-bit platforms #917.

I asked about this change of policy on opam-repo-ci:

  • Retire arm32 runs #466

Some other simpler PR reviews:

  • unix: optimise the posix split to allocate much less by scanning #915.

  • Use a bigger stack buffer for getdents to speed up bigger dirs #910.

  • Make Runtime_events.User.write non-allocating #14984.

  • posix: add device mknod support #911.

Finally, I took advantage of the new Eio release to remove a hack from capnp-rpc:

  • Revert "Work around FreeBSD close problem" #322.

Prometheus

Mark made a large PR adding Eio support to prometheus. To unstick it, Anil then split out the bit moving Lwt out of the core:

  • Move the Lwt logic out of the prometheus core #65.

However, this was a breaking change and that can be annoying when multiple libraries in an application might be using it, since they all have to be upgraded at the same time. So, we started by adding a new Prometheus_lwt package that just provides aliases back to the existing core. Libraries can update to that incrementally, and then we can remove Lwt support from core:

  • Add prometheus-lwt as a forward-compatible Lwt interface #66.

The functions that needed moving included all the time-based ones (e.g. Summary.time metric gettime fn measures how long fn takes to run and records it). The gettime argument is there because Prometheus could be used in libraries that run in browers or unikernels and therefore don't have access to Unix.gettimeofday, but it's a bit annoying having it there. Since we were deprecating the old functions, this was a good time to fix it. Initially, I wanted to make it a labelled argument, which would allow making it optional later:

  • Make gettime a labelled argument in the new API #67.

But then I decided to remove it completely and add a new system for registering a time function:

  • Remove gettime function from new API #68.

I've cut a new release of this, and as part of this I tested the upgrade on OCluster, which does some unusual things with Prometheus (such as forwarding metrics over capnp-rpc):

  • Prepare release #69.

And as part of that, I also discovered a small problem with OCluster itself, which Mark has fixed:

  • README instructions don't work #263.

OCurrent

Benjamin Chabanne noticed that OCurrent was stuck on mirage-crypto < 1:

  • Old version of mirage-crypto dependency #476.

I found I'd made a patch for this a couple of years ago but it couldn't be applied because OCurrent used ocaml-session and my PR there had never been accepted! (another example of API breaks blocking upgrades)

  • Update to mirage-crypto-rng 1.0 API #36.

Mark is planning to fix this by removing the use of ocaml-session completely.

OxCaml

I also had a quick go at using OxCaml, though got off to a bad start:

  • Installation instructions don't work (utop fails to install) #56.

David Allsopp got that fixed quickly.

Previously, the lack of MDX support has made trying OxCaml difficult, so I was keen to try out the new (unreleased) MDX 2.6.0, which has OxCaml support, but opam was failing to find a solution.

OxCaml seems to be using a rather complicated system of fake packages to make sure you only install compatible software. In this scheme:

  • oxcaml-compiler depends on
  • oxcaml-patch-guards, which depends on
  • oxcaml-alcotest-patches, which depends on
  • oxcaml-backoff-patches, which depends on
  • oxcaml-chrome-trace-patches, which depends on
  • oxcaml-ctypes-patches, which depends on
  • oxcaml-ctypes-foreign-patches, which depends on
  • oxcaml-dot-merlin-reader-patches, and so on.

I found this all quite puzzling and in the end just overrode oxcaml-dune-patches.ox with an empty package, which seems to let me install whatever I want.

With the new MDX installed, I was able to run the tests for ocaml-uring under OxCaml. I then tried to use memtrace to check for allocations, but using that under OxCaml seemed pretty difficult (it produces traces with a format that memtrace_viewer can't read, and installing the Git version of the viewer looked difficult). In the end, I ran memtrace under OCaml 5.5.0 and grabbed a recording from that to analyse instead, which worked to show me where the existing allocations were.

I experimented with using @local annotations in ocaml-uring to move allocations to the stack. I was able to reduce the number of words allocated per test batch of noop submissions from 140 to 70 using annotations and exclave_, and then down to 40 using caml_alloc_local in the C stubs. It didn't make the benchmark noticably faster, but it probably would if combined with other allocations that needed promoting.

I also came across Patrick's uring/OxCaml work, which optimised a different part of the code.

Finally, I had a go at making Eio.Buf_read parser arguments local, which revealed an interesting problem:

val take_while : (char -> bool) @ local -> string parser      (* FAILS *)
val take_while : (char -> bool) @ local -> t -> string        (* OK *)
val take_while : (char -> bool) @ local -> (t -> string)      (* FAILS *)

-> is normally associative, but not with OxCaml. If we think of take_while as a function that takes two arguments and returns a string then the first argument can be local. But as a one-argument function that takes a predicate and returns a parser, the predicate can't be stack-allocated because it must be stored in the parser.

It doesn't really matter; optimising the construction of parsers isn't particularly useful, but it revealed something I hadn't thought about with types.