firesidewing

firesidewing

Any idea why the Tails library may be contributing to longer compile time?

I’ve been using the SaladUI library a bit and one of the things that I’ve noticed is that compiling depencies has significantly slowed down. Just based on watching the console logs I can tell that the Tails library is likely causing the issue.

I come from C# (work) and I like pursuing/fixing performance issues/bottlenecks, but I feel like my knowledge doesn’t transfer to elixir. I was wondering what is causing this relatively small library to take so much time compiling, not out of malice for the library but only out of curiosity. I’ve heard macros could cause these issues, but the macro seems benign?

If anyone has any insight into this specific library or any tools that I could use to found out myself and maybe play around with attempting to make it faster, I’d love the help.

Marked As Solved

ruslandoga

ruslandoga

:wave: @firesidewing

I was curious so I tried it out and it’s indeed slow:

iex(1)> :timer.tc fn -> Mix.install([:tails], force: true) end
{9605421, :ok} # nine seconds

Skimming the files, it seems to do attribute and function generation from external (?) resources in tails/lib/custom.ex at main · zachdaniel/tails · GitHub, so I guess that’s what is causing the long compilation time.

With profiler on:

[profile]     15ms compiling +      0ms waiting while compiling lib/color_classes.ex
[profile]     21ms compiling +      0ms waiting while compiling lib/doc.ex
[profile]     26ms compiling +      0ms waiting for module Tails.ColorClasses while compiling lib/colors.ex
[profile]    151ms compiling +      0ms waiting while compiling lib/custom.ex
[profile]   7878ms compiling +    143ms waiting for module Tails.Custom while compiling lib/tails.ex
[profile] Finished compilation cycle of 8 modules in 8024ms
[profile] Finished group pass check of 8 modules in 41ms

And by “attribute and function generation” I mean something like this

# try running it in IEx
defmodule SlowModule do
  for i <- 1..100_000 do
    def to_string(unquote(i)), do: unquote(to_string(i))
  end
end

Tails doesn’t do that exact thing, but I think it’s something similar. One possible fix is to do less of that, maybe splitting this work across modules, or using “smaller” data structures. And I think Tz had a similar problem in the past, and now it doesn’t. So it might be useful :slight_smile:

Also Liked

frankdugan3

frankdugan3

Yes, as @ruslandoga pointed out, it generates all the variants at compile time. The idea is to ensure it runs extremely fast at runtime, since it will be called many times during every render. There are other possible approaches, but the choice to do the work at compile time was intentional.

Tails is currently in need of a maintainer, as Zach no longer has time to work on it with all the other things he has going on.

firesidewing

firesidewing

For future reference I was able to compare a quick wrapper NIF vs Tails for a few different sizes of class lists. Got about a 2-8x speed boost depending on size with identical outputs and without the slow compile. I’ve never written a hex package but might just throw it up when I have time.

I do agree there should just be a universal tool for this, but right now it’s useful to me without having to mess around with layers too much.

LostKobrakai

LostKobrakai

Mix.install([:tails, :beam_file])
ex = BeamFile.elixir_code!(Tails)
File.write!("…/tails.ex", ex)

This module gets blown up to 70k lines (formatted) once macros are expanded.

firesidewing

firesidewing

All super experimental, probably missed a few things and could have made things nicer (the weird duplicate functions), but I just wanted to get a quick comparison.

The rust code

#[rustler::nif]
fn merge2<'a>(env: Env<'a>, class1: &str, class2: &str) -> NifResult<Term<'a>> {
    let result = tw_merge!(class1, class2);

    Ok(result.encode(env))
}

#[rustler::nif]
fn merge<'a>(env: Env<'a>, class: &str) -> NifResult<Term<'a>> {
    let result = tw_merge!(class);

    Ok(result.encode(env))
}

#[rustler::nif]
fn merge_list<'a>(env: Env<'a>, classes: Vec<&str>) -> NifResult<Term<'a>> {
    let result = tw_merge!(classes.join(" "));

    Ok(result.encode(env))
}

rustler::init!("Elixir.TailwindMerge");

The elixir wrapper

defmodule TailwindMerge do
  use Rustler, otp_app: :tailwind_merge, crate: "tailwind_merge"

  def merge2(_base, _classes), do: :erlang.nif_error(:nif_not_loaded)
  def merge(_classes), do: :erlang.nif_error(:nif_not_loaded)
  def merge_list(_classes), do: :erlang.nif_error(:nif_not_loaded)
end

And the benchmark

def run_benchmark do
    simple_classes = ["p-4 text-red-500", "p-6 text-blue-500"]

    medium_classes = [
      "p-4 m-2 text-red-500 bg-blue-300 flex items-center",
      "p-6 m-4 text-blue-500 bg-green-400 justify-between"
    ]

    complex_classes = [
      "p-4 px-6 py-2 m-2 mx-4 text-red-500 bg-blue-300 flex items-center justify-start rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-all duration-200",
      "p-6 px-8 m-4 mx-6 text-blue-500 bg-green-400 justify-between items-start rounded-xl border-2 border-blue-300 shadow-md hover:shadow-lg",
      "p-8 py-4 m-6 my-8 text-green-600 bg-yellow-200 flex-col items-end rounded-2xl border-4 border-green-400"
    ]

    large_classes = Enum.map(1..100, fn _ -> Enum.random(complex_classes) end)

    arbitrary_classes = [
      "text-[#custom] p-[14px] w-[232.5px] grid-cols-[1fr,auto,30%]",
      "text-[rgb(24,25,26)] p-[3vh] w-[calc(100%-3rem)] grid-cols-[repeat(auto-fit,minmax(200px,1fr))]"
    ]

    Benchee.run(
      %{
        "TailwindMerge - Simple" => fn ->
          TailwindMerge.merge_list(simple_classes)
        end,
        "Tails - Simple" => fn ->
          Tails.classes(simple_classes)
        end,
        "TailwindMerge - Medium" => fn ->
          TailwindMerge.merge_list(medium_classes)
        end,
        "Tails - Medium" => fn ->
          Tails.classes(medium_classes)
        end,
        "TailwindMerge - Complex" => fn ->
          TailwindMerge.merge_list(complex_classes)
        end,
        "Tails - Complex" => fn ->
          Tails.classes(complex_classes)
        end,
        "TailwindMerge - Large" => fn ->
          TailwindMerge.merge_list(large_classes)
        end,
        "Tails - Large" => fn ->
          Tails.classes(large_classes)
        end,
        "TailwindMerge - Arbitrary" => fn ->
          TailwindMerge.merge_list(arbitrary_classes)
        end,
        "Tails - Arbitrary" => fn ->
          Tails.classes(arbitrary_classes)
        end
      },
      time: 10,
      memory_time: 2,
      pre_check: true,
      formatters: [
        {Benchee.Formatters.Console, extended_statistics: true}
      ]
    )
  end

Which, on my machine, results in:

Name                                ips        average  deviation         median         99th %
TailwindMerge - Simple         523.26 K        1.91 μs   ±995.11%        1.83 μs        2.09 μs
TailwindMerge - Arbitrary      335.41 K        2.98 μs   ±634.92%        2.89 μs        3.07 μs
TailwindMerge - Medium         224.60 K        4.45 μs   ±276.75%        4.33 μs        4.79 μs
TailwindMerge - Complex         67.16 K       14.89 μs    ±42.19%       14.60 μs       21.65 μs
Tails - Simple                  54.74 K       18.27 μs    ±31.61%       18.04 μs       24.12 μs
Tails - Arbitrary               49.74 K       20.11 μs    ±31.14%       19.38 μs       37.77 μs
Tails - Medium                  47.43 K       21.08 μs    ±33.35%       20.70 μs       28.98 μs
Tails - Complex                 17.56 K       56.96 μs    ±13.94%       55.72 μs       82.48 μs
TailwindMerge - Large            2.75 K      363.23 μs     ±2.78%      360.60 μs      401.51 μs
Tails - Large                    0.84 K     1191.36 μs     ±3.54%     1184.49 μs     1291.30 μs

Comparison: 
TailwindMerge - Simple         523.26 K
TailwindMerge - Arbitrary      335.41 K - 1.56x slower +1.07 μs
TailwindMerge - Medium         224.60 K - 2.33x slower +2.54 μs
TailwindMerge - Complex         67.16 K - 7.79x slower +12.98 μs
Tails - Simple                  54.74 K - 9.56x slower +16.36 μs
Tails - Arbitrary               49.74 K - 10.52x slower +18.19 μs
Tails - Medium                  47.43 K - 11.03x slower +19.17 μs
Tails - Complex                 17.56 K - 29.80x slower +55.05 μs
TailwindMerge - Large            2.75 K - 190.07x slower +361.32 μs
Tails - Large                    0.84 K - 623.39x slower +1189.45 μs

Extended statistics: 

Name                              minimum        maximum    sample size                     mode
TailwindMerge - Simple            1.71 μs    21580.94 μs         4.64 M                  1.82 μs
TailwindMerge - Arbitrary         2.67 μs    18349.99 μs         3.13 M                  2.89 μs
TailwindMerge - Medium            4.04 μs     8567.07 μs         2.14 M                  4.30 μs
TailwindMerge - Complex          13.83 μs     3345.28 μs       660.40 K                 14.56 μs
Tails - Simple                   17.50 μs     2992.43 μs       539.68 K                    18 μs
Tails - Arbitrary                18.52 μs     2546.79 μs       490.53 K                 19.34 μs
Tails - Medium                   19.79 μs     3187.23 μs       468.31 K                 20.65 μs
Tails - Complex                  53.40 μs     1980.29 μs       174.36 K                 54.67 μs
TailwindMerge - Large           346.85 μs      654.30 μs        27.49 K                357.53 μs
Tails - Large                  1140.16 μs     2437.37 μs         8.39 K               1173.54 μs

Memory usage statistics:

Name                              average  deviation         median         99th %
TailwindMerge - Simple          0.0625 KB     ±0.00%      0.0625 KB      0.0625 KB
TailwindMerge - Arbitrary       0.0625 KB     ±0.00%      0.0625 KB      0.0625 KB
TailwindMerge - Medium          0.0625 KB     ±0.00%      0.0625 KB      0.0625 KB
TailwindMerge - Complex         0.0625 KB     ±0.00%      0.0625 KB      0.0625 KB
Tails - Simple                    5.55 KB     ±0.00%        5.55 KB        5.55 KB
Tails - Arbitrary                 7.70 KB     ±0.00%        7.70 KB        7.70 KB
Tails - Medium                    9.27 KB     ±0.00%        9.27 KB        9.27 KB
Tails - Complex                  28.65 KB     ±0.00%       28.65 KB       28.65 KB
TailwindMerge - Large           0.0625 KB     ±0.00%      0.0625 KB      0.0625 KB
Tails - Large                   680.54 KB     ±0.00%      680.54 KB      680.54 KB

Comparison: 
TailwindMerge - Simple          0.0625 KB
TailwindMerge - Arbitrary       0.0625 KB - 1.00x memory usage +0 KB
TailwindMerge - Medium          0.0625 KB - 1.00x memory usage +0 KB
TailwindMerge - Complex         0.0625 KB - 1.00x memory usage +0 KB
Tails - Simple                    5.55 KB - 88.88x memory usage +5.49 KB
Tails - Arbitrary                 7.70 KB - 123.25x memory usage +7.64 KB
Tails - Medium                    9.27 KB - 148.38x memory usage +9.21 KB
Tails - Complex                  28.65 KB - 458.38x memory usage +28.59 KB
TailwindMerge - Large           0.0625 KB - 1.00x memory usage +0 KB
Tails - Large                   680.54 KB - 10888.62x memory usage +680.48 KB

Extended statistics: 

Name                              minimum        maximum    sample size                     mode
TailwindMerge - Simple          0.0625 KB      0.0625 KB       185.28 K                0.0625 KB
TailwindMerge - Arbitrary       0.0625 KB      0.0625 KB       161.84 K                0.0625 KB
TailwindMerge - Medium          0.0625 KB      0.0625 KB       144.94 K                0.0625 KB
TailwindMerge - Complex         0.0625 KB      0.0625 KB        77.44 K                0.0625 KB
Tails - Simple                    5.55 KB        5.55 KB        46.88 K                  5.55 KB
Tails - Arbitrary                 7.70 KB        7.70 KB        32.52 K                  7.70 KB
Tails - Medium                    9.27 KB        9.27 KB        38.40 K                  9.27 KB
Tails - Complex                  28.65 KB       28.65 KB        14.86 K                 28.65 KB
TailwindMerge - Large           0.0625 KB      0.0625 KB         5.26 K                0.0625 KB
Tails - Large                   680.49 KB      680.54 KB         1.25 K                680.54 KB

Where Next?

Popular in Questions Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New

Other popular topics Top

lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
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
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
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
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New

We're in Beta

About us Mission Statement