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

pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
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
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
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
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
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
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
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

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
Tee
can someone please explain to me how Enum.reduce works with maps
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
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

We're in Beta

About us Mission Statement