BennyHallett / obelisk

Static Site Generator written in Elixir.
MIT License
392 stars 34 forks source link

v1.0.0 Performance Goal: Build 10000 documents in around 10 seconds #7

Open BennyHallett opened 9 years ago

BennyHallett commented 9 years ago

10 seconds is totally arbitrary, but I'd like to set some lofty performance goals.

There's a mix task to run this test:

$ mix perf
dch commented 9 years ago

It would be interesting to compare to hugo, also if you have Joe Armstrong's 2nd "Programming Erlang" book, Appendix 3 "A simple Execution Environment" has a whole lot of interesting details on improving startup time.

BennyHallett commented 9 years ago

Thanks for the recommendation, I'll check it out.

liveforeverx commented 9 years ago

I've started to make my own static site generator in elixir.

Benchmarks from obelisk on my notebook:

obelisk % mix perf
2015-07-19-12:29:07
2015-07-19-12:34:07

It takes 5 minutes to build and use only 1 logical CPU on my machine.

Benchmarks for very little bit heavier template (with partials, as I've designed my own similar to hugo, for easy porting theme, in which I'm love in https://github.com/azmelanar/hugo-theme-pixyll and I love hugo partials, toml and many other design decisions).

I've ported your benchmark to my framework and get now this:

lime % mix perf
copy assets: 115 ms
compile layouts: 260 ms
compile posts: 66249 ms

To get better performance, you need compile layouts, with which you will get rid of Agent. For example, there exists build-in function too: EEx.function_from_file/5 which you can use.

You do not need to accumulate all documents and store it somewhere, it will be better, if you write it directly, without accumulate 10000 element lists.

Next thing, that I've done to utilize all cores, start parallel renders:

  def start_pool(conf, publishdir) do
    size = :erlang.system_info(:schedulers)
    for i <- 1..size, do: :proc_lib.spawn_link(Lime.Page, :worker, [conf, publishdir])
  end

  def load_workers([], files, pids), do: load_workers(pids, files, pids)
  def load_workers(_, [], pids) do
    for pid <- pids, do: send(pid, {:close, self})
    Enum.map(1..length(pids), fn(_) ->
      receive do
        :ok -> :ok
      end
    end)
  end

  def load_workers([pid | rest], [file | files], pids) do
    send(pid, {:file, file})
    load_workers(rest, files, pids)
  end

  def worker(conf, publishdir) do
    receive do
      {:file, file} ->
        render(file, conf, publishdir)
        worker(conf, publishdir)
      {:close, pid} ->
        send(pid, :ok)
    end
  end

I simple start per scheduler (erlang starts 1 scheduler per logical core), that I have the same count of processes, as cores, and send filenames around. With that, I utilize all my cores(and it is a reason, why it is 4 times better). You need to do something like that too.

One problem in my approach is, that I send {:file, file} quicker as process works on file, that means I get receiver queue, what is not good too. I'll experiment, what I can do, and which influence it has on performance. But, by now, if you will use all cores at least with this approach you will get much better performance, as obelisk has now.

Hopefully, that information helps you.

liveforeverx commented 9 years ago

On which hardware you want get around 10 seconds? (on better hardware, than my, I guess it can be possible). On the Hardware, which Hugo use for example(5000 in 1 Second), I should get ca. 5000 in 5-7 seconds with my, and will be very close to 10k in 10 seconds.

liveforeverx commented 9 years ago

I've used sbroker as pooling library for my experiments, that is my implementation now:

https://gist.github.com/liveforeverx/0e3146d8e8921b9916be

In my code, I use it simple sequential:

Pool.set_state(pool, ....all arguments at once)
Enum.each(files, &Pool.run(pool, __MODULE__, :render, [&1]))
Pool.sync_all(pool)

And, after that, I've gotten little bit less as 100% CPU(on all cores), and definitely better speed(ca. 15%):

lime mix perf
copy assets: 106 ms
compile layouts: 306 ms
compile posts: 56517 ms
BennyHallett commented 9 years ago

Wow, thanks for all this info! I haven't touched Obelisk in quite a while but this will sure help me reach some of my goals with it quicker!

I like the idea of compiling the templates, that should help a lot.

As for the agents, I completely agree, that's where most of the slowness I experience at the moment is coming from. Initially Obelisk was a single process, but as I learned more about processes, agents and genservers I wanted to make use of them, and didn't really understand whether or not building it this was was right or wrong. It's probably the main place that I'd like to refactor and fix.

Once again, thanks so much for sharing what you've learned in building your own static site generator. Is the code available on GitHub or somewhere else? I'd love to check out what you've built!

guido4000 commented 8 years ago

Is the code available on GitHub or somewhere else? I'd love to check out what you've built!

Seems to be this: https://github.com/liveforeverx/lime