Page 105 of 165

Przyśpieszanie ładowania Railsów

Wstęp

Tytułem wstępu powiem, że artykuł nie jest mój. Ostatnio korespondowałem trochę z Xavierem Shay i pojawił się pomysł tłumaczenia co ciekawszych wpisów na język polski - tak z myślą o młodych Railsowcach - zawsze to łatwiej zrozumieć w swoim języku. Chciałbym zaznaczyć, że nie jestem profesjonalnym tłumaczem i nie wszystko może być idealnie. Oryginał artykułu możecie znaleźć tutaj.

Wolno, wolniej, najwolniej...

Szybkość ładowania plików maleje jeśli chodzi o kolejne wersje Rubiego. Można to zaobserwować na poniższym wykresie:

Dla przykładu, nasza średniej wielkości aplikacja wymaga około 2200 plików. Zaczyna być to problematyczne, ponieważ dla Railsów 1.9.2 start aplikacji trwa około 20 sekund, zaś dla 1.9.3 ponad 46 sekund. Obie te wartości są zdecydowanie zbyt wysokie.

Dlaczego tak się dzieje? Powodów jest kilka, niemniej jednak jednym z głównych powodów jest algorytm includowania plików, który wygląda mniej więcej tak:

def require(file)
 $loaded.each do |x|
   return false if x == file
 end
 load(file)
 $loaded << file
end

Jak nietrudno zauważyć, tego typu pętla zwalnia wraz z każdym nowym plikiem. Aby temu zaradzić napisałem patch do 1.9.3 który zmienia ten algorytm aby wyglądał tak:

def require(file)
  return false if $loaded[file]
  load(file)
  $loaded[file] = true
end

Po tej zmianie, prędkości ładowania prezentują się następująco:

Dużo lepiej.

Wprawdzie jest to tylko benchmark, jednak moja główna Railsowa aplikacja ładuje się około 10 sekund (20 przy Rubym 1.9.2). Pusta aplikacja ładuje się około 1.1 sekund. Jest to mniej niż w 1.8.7.

Instalacja poprawki

Oto krótka instrukcja jak zainstalować z wykorzystaniem RVMa (w zaledwie 10 minut) moją poprawkę.

# Benchmark testowy
cd /your/rails/app
time script/rails runner "puts 1"

# Instalacja spatchowanego Rubiego
curl https://gist.github.com/raw/996418/e2b346fbadeed458506fc69ca213ad96d1d08c3e/require-performance-fix-r31758.patch > /tmp/require-performance-fix.patch
rvm install ruby-head --patch /tmp/require-performance-fix.patch -n patched
# ... czeeeekamy :)

# Test porównawczy
cd /your/rails/app
rvm use ruby-head-patched
gem install bundler --no-rdoc --no-ri
bundle
time script/rails runner "puts 1"

Chcesz pomóc?

Zanim poprawka ta zostanie umieszczona w trunku, potrzeba jeszcze wiele wysiłku. Doceniłbym gdybyście:

  • sprawdzili ten kod i zostawili komentarz z wynikami
  • przejrzeli ten patch
  • sprawdzili to na Windowsie
  • zgłaszali ew. błędy

Co dalej?

Zdaję sobie sprawę, że do dołączenia tego do 1.9.3 jeszcze daleka droga. Niemniej jednak jest to pierwszy z wielu kroków w drodze do przyśpieszenia Railsów. Wciąż mamy Bundlera oraz RubyGems które robią... coś - coś co chciałbym sprawdzić. Chciałbym też przeportować ew. zmiany także do JRubiego.

UUID-GUUID zamiast domyślnego ID w Ruby on Rails

Wstęp

W pewnym projekcie nad którym ostatnio pracuję, postanowiłem skorzystać z UUIDów (co to są UUIDy dowiesz się tutaj) zamiast wykorzystywać domyślne ID'ki. Realizacja tego zadania w Railsach wymaga zastosowania pewnych niewielkich zabiegów, jednak całość jest całkiem prosta.

Generowanie UUIDów

Do generowania UUIDów wykorzystamy gem uuidtools:

gem 'uuidtools'

Generowanie odbywa się z pomocą poniższej metody:

UUIDTools::UUID.random_create.to_s

Migracje

Aby wykorzystać UUIDy, zamiast standardowego pola ID skorzystamy z pola (a jakże) UUID. Aby tego dokonac, wyłączymy całkowicie generowanie IDków dla migracji. Stworzymy w tym celu klasę bazową dla naszych migracji (można oczywiście deklarować to w każdej migracji z osobna ale komu się chce). Tworzymy w katalogu db/ plik uuid_migration.rb zawierający jedną metodę (migracja ta jest migracją bazową więc nie ma up, down czy też (w R3.1) change:

class UuidMigration < ActiveRecord::Migration
  def create_uuid_table(name, options = {})
    create_table name, options.merge(:id => false) do |t|
      t.string :uuid, :limit => 36, :primary => true
      yield t
      t.timestamps
    end

    add_index name, :uuid, 'UNIQUE'
  end
end

Od teraz możemy wykorzystać naszą migrację edytując migracje danych modeli, np:

require File.expand_path(File.dirname(__FILE__))+'/../uuid_migration.rb'

class CreateProducts < UuidMigration
  def change
    create_uuid_table :products do |t|
      t.string   :name,  :null => false
      t.float    :price, :null => false
      t.timestamps
    end
  end
end

Taka migracja spowoduje, że oprócz pól zadeklarowanych powyżej, powstanie także (automatycznie) pole uuid oraz timestampy. Tworząc dużo modeli opartych na UUIDach, taka metoda jest wygodniejsza (nie zapomnimy np. przez przypadek wyłączyć automatycznie tworzonego pola id).

Klasa bazowa dla naszych modeli

Zanim zaczniemy, chciałbym zaznaczyć, że wykorzystamy klasę bazową zamiast modułu tylko dlatego, że w systemie nad którym pracuję, występuje takie rozwiązanie (powody pozostawiam dla siebie ;) ). Niemniej jednak, nic nie stoi na przeszkodzie aby przerobić rozwiązanie zaprezentowane poniżej wersję modułową.

W gruncie rzeczy cały "myk" sprowadza się do informowania klas o tym że zamiast id używamy uuida oraz do jego inicjowania. Inicjować uuida będziemy zaraz przed zapisem obiektu do bazy (before_create):

require 'uuidtools'

class UuidRecord::Base < ActiveRecord::Base

  self.abstract_class = true
  self.primary_key = :uuid

  before_create :generate_uuid

  private

  def generate_uuid
    self.uuid = UUIDTools::UUID.random_create.to_s
  end

end

Tworzymy klasę

Po utworzeniu naszej migracji oraz innych potrzebnych fragmentów kodu, możemy przystąpić do utworzenia naszej pierwszej klasy:

class Product < UuidRecord::Base

  set_table_name  :products
  set_primary_key :uuid

end

Wadą "uuidowania" jest to, że musimy deklarować nazwę tabeli (jeśli dziedziczymy, jako moduł includowany już nie), oraz że w każdym modelu (mimo że ustawiliśmy w nadrzędnym), musimy deklarować, że uuid jest naszym kluczem głównym:

set_table_name  :products
set_primary_key :uuid

Relacje pomiędzy obiektami

Opisując wykorzystanie UUIDów, warto wspomnieć także o tym, jak przebiega stosowanie ich w relacjach has, belongs_to, itd. Ogólnie rzecz ujmując, to przebiega... tak samo. Ale... należy pamiętać o tym, że nie mamy pola ID! Dlatego deklarując relacje, musimy podawać klucz obcy oraz klucz główny:

  belongs_to :product,
    :primary_key => :uuid,
    :foreign_key => :product_uuid

  has_many :ordereds,
    :primary_key => :uuid,
    :foreign_key => :order_uuid

Tyle jeśli chodzi o UUIDy. Ich wykorzystanie nie jest trudne, pod warunkiem, że będziemy pamiętać o tym co napisałem powyżej.

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑