I dug a little deeper and have a few findings to report:
First, we have one extra compiler to extract verified routes to a JSON “lock file” for consumption by an internal JS linter. It was not updated in a while it was reading the manifest (using Mix.Compilers.Elixir.read_manifest) in an attempt to not serialize the routes if not necessary.
It turned out the manifest read was actually the most expensive operation, taking around 20ms, the manifest is 481K.
For me that’s a lot to read a file into structs, but since the call basically reads as “read file” (which is fast) and “call :erlang.binary_to_term”, I don’t know if we have a lot of leeway here.
In the end, it turns out it’s faster to write the JSON for each call (calling Phoenix.Router.routes and writing the file takes 1ms) rather than trying to check if we actually have work to do…
I’d say compilers should be run in parallel, that could potentially help in the case of multiple compiles takings tens of ms in normal operation.
We’d also need a better way to tell if there is work to do than reading the manifest for custom compilers as apparently that’s expensive
I don’t really like switching to an “always do the side effect” basis, as, it’s going to be called for each request…
The elixir compiler still times 50ms for a noop which I hope can be optimized too. Since the elixir compiler also reads the manifest I guess at least 20ms are spent there too but I didn’t dug there yet as it’s less convenient that just modifying a few .ex in the deps folder.
Then there’s an app compiler that takes 1ms.
Also, occasionally the compilers appear to run twice. I didn’t manage to find a pattern:
Compiler elixir time: 52 ms
Compiler app time: 3 ms
Compiler routes_js time: 1 ms
Compiler elixir time: 50 ms
Compiler app time: 3 ms
Compiler routes_js time: 1 ms
EDIT: the pipeline runs twice on app start, then twice on the first request, then once per request.
EDIT2: i think this is because of the LV connecting… maybe? let’s put this aside for now, not sure exactly of the pattern