JohnDoneth

JohnDoneth

Discovering source files using `:filelib.wildcard/1` is slow for large Elixir projects

The :filelib.wildcard/1 function used by the Elixir compiler for discovering Elixir source files is relatively slow for projects with a lot of files. The project I work on at the time of writing has 1570 Elixir files. Using that project I was able to profile and see that iterating to find source files is taking a substantial amount of time using eflambe and speedscope.

This led me to see if I could improve the performance. I wrote a very small Rust NIF using Rustler to see if I could outperform :filelib.wildcard/1.

Rust NIF

use rustler::Term;
use rustler::Env;
use rustler::OwnedBinary;
use walkdir::WalkDir;
use std::ffi::OsStr;

#[rustler::nif]
fn walkdir(env: Env, dir: &str, extension: &str) -> Vec<String> {
    let mut files = Vec::new();

    for entry in WalkDir::new(dir) {
        let entry = entry.unwrap();

        if entry.path().extension() == Some(OsStr::new(extension)) {
            files.push(entry.path().display().to_string());
        }
    }
    
    files
}

rustler::init!("Elixir.Walkdir.Native");

Bench Results

Comparing this NIF to the :filelib.wildcard/1 function results in a substantial amount of time that could be saved when compiling a project with no changes. Roughly a third of the compile time for a no-op compile is spent iterating to find the source files (~100ms of ~300ms). This is most notable when hot reloading a Phoenix project as the compiler is invoked on page load even if there are no files changed (See Phoenix.CodeReloader — Phoenix v1.8.3).

Benchee.run(
    %{
      "nif" => fn -> Walkdir.Native.walkdir("lib", "ex") end,
      "elixir" => fn -> :filelib.wildcard('lib/**/*.ex') end
}
Name             ips        average  deviation         median         99th %
nif            81.88       12.21 ms     ±8.98%       11.98 ms       16.20 ms
elixir          9.49      105.35 ms    ±16.43%      105.28 ms      151.09 ms

Comparison: 
nif            81.88
elixir          9.49 - 8.63x slower +93.14 ms

I do not see a way forward to get these changes into the Elixir compiler easily since this code relies on a NIF. I do not see a more performant set of functions from the BEAM that would help either. The issue seems to be that iterating over every file, checking if it’s a directory, iterating further, etc has a bit of overhead using the :filelib and by extension :file module. It seems like creating a specialized BIF for returning matching files in a directory in the BEAM would be the most straight-forward way to include these potential performance improvements without using a NIF. I would appreciate any feedback or additional ideas on how this could be improved or included into Elixir proper.

First Post!

hauleth

hauleth

I think we could start with trying to find more optimised native solutions. I think that there is quite a room for improvements. Additionally Elixir could try to implement their own implementation that would do concurrent tree walk (as the compilation is concurrent), which should help with that performance issue.

Where Next?

Popular in Questions Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New

We're in Beta

About us Mission Statement