0

Podwójne przyspieszenie

Posted by M on 00:16 in ,
Ostatnio w jednym projekcie zajmuje się ujednoliceniem sposobu generowania odpowiedzi w formacie XML. W większości miejsc używany już był lxml, jednak w pewnych miejscach wykorzystane były szablony Django. Największy dokument przed wygenerowaniem miał blisko 600 linii po ładnym sformatowaniu, a i tak zawierał miejsca na podstawienie dodatkowych wstawek XML.
Szablon zawierał także sporą dawkę instrukcji warunkowych i kilka pętli.

W końcu udało się go przepisać, choć finalne rozwiązanie wymagało trochę pracy. Początkowo używane były funkcje Element i SubElement. Pisania było przy tym sporo.

user = Element('user', uid=str(user.id)) # rzutowanie do typu znakowego
login = SubElement(user, 'login')
login.text = user.username # przypisanie do zmiennej, tylko zeby text ustawić
nick = SubElement(user, 'nick')
nick.text = profile.nick
if data.get('options'):
    options = SubElement(user, 'options')
    for k,v in data['options'].items():
        SubElement(options, 'option', key=k, value=v or '') # z None też sobie nie radzi

Kolejnym etapem przemian było zrobienie aliasów E i SE do pełnych nazw funkcji budujących obiekty. To jednak było wciąż zbyt małe usprawnienie. Mając przed sobą jeszcze pewnie ponad 500 linii do przerobienia pojawił się pomysł zrobienia wrappera, lub jak kto woli DSLa.
Jak już całkowicie własną otoczkę robić, to niech eliminuje to co spowalnia pracę.
Jedną z pierwszych funkcji było automatyczne rzutowanie typów liczbowych na typ znakowy - zawsze to 5 znaków mniej do napisania (lxml sam tego nie robił). Kolejną rzeczą była obsługa wartości None - zamiana na pusty ciąg znaków. Poza obsługą różnych typów parametrów mile widziane było wyeliminowanie konieczności przypisywania elementu do zmiennej, aby dodać mu jakiś tekst.
Tutaj pojawiła się propozycja stworzenia metody add, która może przyjąć tekst, jak i inny element.
Po tych zmianach było już łatwiej, ale nadal trzeba było rozbijać na drobne kawałki ze względu na różne instrukcje warunkowe czy pętle.
Ułatwić to miały kolejne dwie metody add_if(condition, *nodes) oraz add_for(iterable, callback).
Dzięki temu możliwe było zapisanie w spójny sposób chyba wszystkich występujących w projekcie przypadków. Poniżej jest odpowiednik przedstawionego wczesniej kodu.

user = E('user', uid=user.id).add(
           E('login').add(user.username),
           E('nick').add(profile.nick),
       ).add_if(data.get('options'),
           E('options').add_for(data.get('options',{}).items(),
               lambda item: E('option', key=item[0], value=item[1])
           )
       )

Praktycznie wszystkie spowalniacze zostały wyeliminowane, a i czytelność na tym zyskała. Pamiętać należy jednak o tym, że w instrukcji warunkowej wszystko jest wykonywane niezależnie od podanego warunku. On służy jedynie do określenia czy dodać utworzone węzły. Jeśli mamy w zależności od warunku pobrać różne dane, nadal lepiej rozbić generowanie XMLa na mniejsze fragmenty kodu i wykorzystać normalną instrukcję if.

Zainteresowanych tym dodatkiem odsyłam do repozytorium.

Po zaimplementowaniu generowania całego długiego dokumentu, z wykorzystaniem przedstawionego wyżej wspomagacza i biblioteki lxml, przyszedł czas na porównanie wydajności. Użyty został jeden serwer testowy, na którym porównane zostały czasy generowania odpowiedzi w stary i nowy sposób.

Testy z użyciem siege, pokazały spadek czasu generowania całej odpowiedzi (łącznie z dostępem do baz danych, cache, etc) z 0.53 sekundy, w przypadku ładowania i parsowania szablonu przez Django, do 0.32 sekundy. Dzięki powyższym zabiegom przyspieszeniu uległo nie tylko pisanie kodu generującego dokument XML, ale także sam czas przygotowywania odpowiedzi dla klienta. Teraz dzieje się to około 1.6x szybciej, co niewątpliwie jest miłym zaskoczeniem.

Żeby nie było tak optymistycznie, napiszę, że nie było użyte cache'owanie szablonów wprowadzone w Django 1.2. Poza tym, istnieją szybsze systemy szablonów, których można użyć razem z wykorzystywaną u nas ramówką. Pewne jest, że w obydwu przypadkach, zysk ze zrezygnowania z szablonu będzie mniejszy, choć w przypadku cache w nowej wersji Django różnica ta pewnie będzie znikoma. Wynika to z faktu, że w tym konkretnym przypadku szablon jest znajdowany i tak w pierwszej próbie (zainteresowanych cache szablonów odsyłam do Django Advent).

Copyright © 2009 tech.matee.net All rights reserved. Theme by Laptop Geek. | Bloggerized by FalconHive.