Skip to content

Replace HAML with Hamlit

Connor Shea requested to merge (removed):hamlit-test into master

What this change does:

  • It replaces HAML with Hamlit, a much more efficient implementation of HAML.
  • It removes haml-rails.
  • It adds hamlit.rb and removes haml.rb.
  • It makes things faster and less memory-hungry!

I used the rack-mini-profiler, flamegraph, stackprof, and memory_profiler gems to do this testing and get the results shown below. Thanks to Nate Berkopec's fantastic post on Rack Mini Profiler.


These results are from rendering the route /connorshea/gitlab-ce, which was imported from https://gitlab.com/gitlab-org/gitlab-ce.git. The user is not logged in (because for whatever reason I can only be logged in on one of the locally-hosted GitLab instances at a time, despite using separate ports).

I set up two GDKs, ran them side-by-side under ports 3000 and 3001, one using the master branch (Before) and one using this branch (After). I then opened the above route in both. Note that both have the performance analysis gems I mentioned above, that's the only real difference between the actual master branch and the one tested.

Before:

allocated memory by gem
-----------------------------------
  21737597  binding_of_caller-0.7.2
   6515235  activesupport-4.2.6
   5168664  sprockets-3.6.0
   3247421  attr_encrypted-1.3.4
   2947358  gitlab/lib
   2503399  ruby-2.1.8/lib
   2382604  nokogiri-1.6.7.2
   2115132  activerecord-4.2.6
   1031632  redis-3.2.2
    899678  sanitize-2.1.0
    668542  gitlab_git-10.0.0
    549896  actionview-4.2.6
    392643  haml-4.0.7
    371232  actionpack-4.2.6
    324777  gitlab/app
    134824  rugged-0.24.0
     91059  addressable-2.3.8
     80576  better_errors-1.0.1
     70416  html-pipeline-1.11.0
     68685  default_value_for-3.0.1
     48937  rouge-1.10.1
     48616  arel-6.0.3
     38248  warden-1.2.4
     28372  i18n-0.7.0
     25173  devise-3.5.4
     24145  gon-6.0.1
     23248  font-awesome-rails-4.5.0.1
     22570  rack-1.6.4
     11505  activemodel-4.2.6
     10099  redis-store-1.1.7
      9149  json-1.8.3
      9143  task_list-1.0.2
      5216  redis-activesupport-4.1.5
      4917  sprockets-rails-3.0.4
      3800  thread_safe-0.3.5
      3360  six-0.2.0
      2195  railties-4.2.6
      2136  multi_json-1.11.2
      2064  state_machines-0.4.0
      1248  paranoia-2.1.4
       794  browser-1.0.1
       761  rack-attack-4.3.1
       744  carrierwave-0.10.0
       704  github-linguist-4.7.5
       672  teaspoon-1.1.5
       600  other
       504  web-console-2.3.0
       489  state_machines-activerecord-0.3.0
       480  sentry-raven-0.15.6
       256  tzinfo-1.2.2

After:

allocated memory by gem
-----------------------------------
  21582740  binding_of_caller-0.7.2
   6498356  activesupport-4.2.6
   5269792  sprockets-3.6.0
   3205405  attr_encrypted-1.3.4
   2946893  gitlab/lib
   2534290  ruby-2.1.8/lib
   2383029  nokogiri-1.6.7.2
   2115091  activerecord-4.2.6
   1063457  redis-3.2.2
    899678  sanitize-2.1.0
    665599  gitlab_git-10.0.0
    595189  actionview-4.2.6
    371320  actionpack-4.2.6
    243847  gitlab/app
    134824  rugged-0.24.0
    105876  haml-4.0.7
     91059  addressable-2.3.8
     81000  better_errors-1.0.1
     70376  html-pipeline-1.11.0
     68685  default_value_for-3.0.1
     48937  rouge-1.10.1
     48616  arel-6.0.3
     38208  warden-1.2.4
     28372  i18n-0.7.0
     25173  devise-3.5.4
     24145  gon-6.0.1
     23248  font-awesome-rails-4.5.0.1
     22570  rack-1.6.4
     11712  redis-store-1.1.7
     11225  activemodel-4.2.6
      9149  json-1.8.3
      9143  task_list-1.0.2
      5216  redis-activesupport-4.1.5
      4917  sprockets-rails-3.0.4
      3360  six-0.2.0
      3224  thread_safe-0.3.5
      2195  railties-4.2.6
      2136  multi_json-1.11.2
      2064  state_machines-0.4.0
      1248  paranoia-2.1.4
      1172  hamlit-2.2.3
       794  browser-1.0.1
       761  rack-attack-4.3.1
       744  carrierwave-0.10.0
       704  github-linguist-4.7.5
       672  teaspoon-1.1.5
       600  other
       560  redis-rack-1.5.0
       504  web-console-2.3.0
       489  state_machines-activerecord-0.3.0

As you can see, less memory is used by Hamlit.

before  392643  haml-4.0.7
-----------------------------
after     1172  hamlit-2.2.3
        105876  haml-4.0.7

392643 before, 107048 after. That's a 72.7% decrease in the memory used by HAML! Note that the above stats can be accessed at /USERNAME/gitlab-ce?pp=profile-memory if you have the rack-mini-profiler and memory_profiler gems installed.

So why is haml still there? Brakeman requires haml, and for whatever reason the haml gem monkey-patches in ActionView#render_with_haml causing the haml gem to still technically "control" the end-result. The hamlit gem actually rendering things, however, which is why there's such a difference in memory allocation.


What difference does this make for rendering views?

I don't have solid numbers, but based on rack-mini-profiler's data it seems like it's at least 150+ ms faster to render the gitlab-ce repository view, but that's based on a handful of tests, so I'll need to put together a benchmark with benchmark-ips and follow up with the data from that.

Are there any issues that break things?

Maybe! Not sure yet. I know Hamlit doesn't implement any of HAML's helpers, so that could be a problem.

Will this break in a production environment?

There's a good chance it will, since I know we use the HAML preserve helper, which is not in Hamlit (See https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/helpers/application_helper.rb#L217). The reason it doesn't break right now in development is because Brakeman requires haml, as far as I can tell.


See also #13201 (closed).

cc: @yorickpeterse @rspeicher

Merge request reports