Na komfort korzystania z serwisu Internetowego wpływa wiele czynników. Niewątpliwie jednym z podstawowych, jest czas jaki musi odczekać użytkownik po kliknięciu w odnośnik do otrzymania odpowiedzi w formie wygenerowanego dokumentu. W dobie gdy bazy danych – podstawy ówczesnych systemów generowania treści, gromadzą kilkaset megabajtów danych, czas w jakim są odnajdowane oraz zwracane rekordy bezwzględnie rośnie.

Na dodatek systemy CMS wysyłają wygenerowany dokument na samym końcu. Tak też jeżeli widzimy w stopce czas generowania strony, to samo wysyłanie zaczyna się dopiero pod koniec pracy skryptu. Nie dość, że użytkownik musi odczekać czas pracy skryptu to jeszcze czas samego pobierania dokumentu. A dlaczego by nie wysyłać stopniowo dokumentu, wraz z postępami pracy skryptu?

Flush idea

Flush vs. Smarty graph

Moim celem było stworzenie systemu szablonów który wraz z dostępem do wartości zmiennych wysyłał by stopniowo generowany dokument. Aby to najprościej uzmysłowić, spójrzmy na poniższy szablon:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <user>
  3.   <name>{% $name; %}</name>
  4.   <age>{% $age; %}</age>
  5. </user>

System szablonów o mechanice takiej jak Smarty, po uzupełnieniu wartość zmiennej $name (metodą assign()) nic by nie wykonał dopóki nie została użyta metoda display(). W przypadku Flush'a jest inaczej. Po utworzeniu obiektu klasy (w którym w przeciwieństwie do Smarty należy podać ścieżkę do szablonu),

  1. $flush = new flush("./szablon0.htm");

skrypt zaczyna natychmiast wczytywać szablon oraz po kawałku wysyłać go klientowi. Tak długo aż napotka tag którego zmienne nie mają wprowadzonej wartości – w przykładzie jest to $name. W tym momencie proces wczytywania szablonu zostaje przerwany (deskryptory nie zostają zamknięte) i „pole do popisu” dostaje kod dalej.

  1. // Jakiś algorytm.
  2. $flush->assign("name",$session->get_user_name());

Przy każdym dodaniu zmiennej, skrypt sprawdza czy ostatni tag na którym parsowanie zostało przerwane ma już wszystkie zmienne. Jeżeli tak to proces parsowania szablonu zostaje wznowiony. Jeżeli nie to dalej „czekamy”.

Całkowite generowanie dokumentów

Jeżeli ostatecznie nie dostarczymy zmiennych, to nietrudno się domyślić, że szablon nigdy do końca się nie wygeneruje. Nic bardziej mylnego! Metodą display() na końcu skryptu - jeżeli proces parsowania się jeszcze nie skończył, wygenerujemy resztę szablonu nawet jeśli niektóre tagi nie mają uzupełnionych wartości wszystkich zmiennych (zostaną pominięte bez zatrzymywania).

  1. $flush->display();

Gdyby uzupełniono w powyższym szablonie wszystkie zmienne, ów metoda okazała by się niepotrzebna (szablon do końca wygenerowała by ostatnia metoda assign()). Chodź jej stosowanie wypadało by traktować, jako poprawność.

Problemy koncepcyjne

Ze względu na sposób generowania szablonu – który pozwoliłem sobie nazwać strumieniowym, Flush posiada kilka problemów o których należy wspomnieć.

Mechanika parsowania szablonu uniemożliwia tzw. "uzupełnianie wstecz". Tzn. jeżeli parser pominie kilka tagów do określonego fragmentu (kotwice coś takiego umożliwiają – ale zaraz o nich), w późniejszym czasie dodanie wartości do pominiętych tagów, nie spowoduje uzupełnienia ich w już wygenerowanych i wysłanych fragmentach.

Z tego względu uzupełnianie wartości jest procesem bardzo problematycznym - wystarczyło by aby programista zapomniał o uzupełnieniu tagu z początku szablonu, aby cała idea Flush'a poszła na marne. Dokument zaczął by wysyłanie dopiero po metodzie display() która to zapomniany tag by pominęła. Dlatego wpadłem na pomysł... kotwic.

Kotwice

Kotwice pozwalają na skoki do wyznaczonych miejsc, pomijając tagi które nie mają uzupełnionych wszystkich zmiennych (te z uzupełnionymi rzecz jasna zostają wygenerowane). Dla przykładu szablon:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <user>
  3.   <name>{% $name; %}</name>
  4.   {% hook('hook0'); %}
  5.   <age>{% $age; %}</age>
  6.   {% hook('hook1'); %}
  7.   <city>{% $city; %}</city>
  8. </user>

A teraz użycie kotwicy:

  1. $flush = new flush("./szablon1.htm");
  2.  
  3. if($flush->hook(„hook0”)) {
  4.  $flush->assign(„age”,$session->get_user_age());
  5. }
  6.  
  7. $flush->display();

Nie ważne, że tag ze zmienną $name nie miał uzupełnionej wartości. Użycie kotwicy – metodą hook(), spowodowało, że nastąpił skok do tagu hook('hook0'); i zwrócenie true na znak sukcesu (znalazłem kotwicę!).

Oczywiście zasada "uzupełniania wstecz" działa także w przypadku kotwic; nie można odwoływać się do kotwic we fragmentach już wygenerowanych i wysłanych. Dla tego parser zapamiętuje jakie już kotwice znalazł, i tak wywołanie metody hook() z nazwą kotwicy która już „przeleciała” zaskutkuje zwróceniem false. Metoda ta zwróci także false jeżeli nie będzie mogła odnaleźć kotwicy – co jest równoznaczne z wygenerowaniem do końca szablonu.

  1. $flush = new flush("./szablon1.htm");
  2.  
  3. // Zwroci true.
  4. $flush->hook("hook1");
  5.  
  6. // Zwroci false (za pozno! już kotwica byla!)
  7. $flush->hook("hook0");
  8.  
  9. // Zwroci false i wygeneruje dokument do konca.
  10. $flush->hook("hook-no-exist");
  11.  
  12. $flush->display();

Kod roboczy

Jako, że projekt jest w fazie koncepcji – i tak naprawdę zadaje sobie pytanie; czy warto go rozwijać, poniżej opublikowany kod roboczy nie powinien być implementowany w żadnym projekcie. Aktualny stan projektu wygląda mniej więcej tak;

  • Dostępne wszystkie metody oraz tagi z opisanymi dotąd funkcjonalnościami.
  • Nie przedstawione, a funkcjonalne tagi:
    • Instrukcje warunkowe - if(), else, endif (else if nie ma!)
    • Includowanie - include()
  • Nie przedstawione, a funkcjonalne metody:
    • Przypisanie kilka zmiennych - assign_array()
  • Nie przedstawione, a do zaimplementowania tagi:
    • Wywołania zwrotne – callback()
    • Pętle
  • Nie ustalona struktura składni
    • Zwalnianie wartości użytych zmiennych - !$tag

Flush 0.1 Download 13,4 KB

Wydajność

Jeszcze na sam koniec wspomnę o wydajności w porównaniu ze Smarty (w końcu całe wprowadzenie o tym poświęciłem). Szczerze? Nie wiem. Aktualnie projekt jest na takim etapie, że porównywanie wydajności nie ma sensu. Chodź sam fakt, że generowanie dokumentu przejmuje skrypt nie parser PHP ma ogromne znaczenie. Ale gdyby projekt naprawdę zdobył popularność, czy tak trudno było by go przepisać do C i podpiąć bibliotekę? Nie sądzę.

Komentarze do wpisu "Flush concept – strumieniowy system szablonów, kod roboczy":

1. BTM napisał(a):
26 marca 2008, 20:27:21

Chwilkę, nie zgodzę się z Tobą już we wstępie, zanim przeczytam całość ;-)

Oczywiście, że dane np. z PHP’owego echo wysyłane są strumieniowo – to, że na końcu masz czas egzekucji, oznacza, że tyle czasu zajęło skryptowi dojście do tego miejsca, nie oznacza to, że czekał aż tyle i dopiero wtedy wysłał.

Przykład? Proszę – zobacz sobie skrypt wysyłający plik „download” do przeglądarki – najpierw wysyła nagłówek a potem odczytuje plik i wysyła na standardowe wyjście – jeżeli będziesz miał wolne łącze to plik będzie leciał pomału i cały czas będzie żył wątek PHP który do Ciebie pcha plik!

2. b4rtaz napisał(a):
26 marca 2008, 20:30:33

BTM: Wysyłanie pliku ma się do tego nijak. Miałem na myśli systemy generowania treści (fora, portale itd). Proces wysyłania zaczyna się dopiero po wywołaniu metody display() klasy szablonów.

A dopiero w tej metodzie występuje echo, więc dopiero wtedy zaczyna się proces wysyłania sparsowanego szablonu na wyjście. Nie ważne czy ob_flush() jest włączone.

3. BTM napisał(a):
26 marca 2008, 20:32:11

Zakładając, że używasz takiej zbiedzonej klasy szablonów, to tak ;-)

Ale mówię – proste echo już wysyła strumieniowo – nawet najprostsza pętla while(1) { echo ‘!’; } będzie Ci wysyłać do przeglądarki wykrzykniki, a skrypt przecież nie zakończył działania ;-)

4. b4rtaz napisał(a):
26 marca 2008, 20:35:15

BTM: Otóż nie będzie. Wypróbuj sobie oto ten kod:

  1. echo "1"
  2. sleep(3); // Trzy sekundy pauzy.
  3. echo "2";

Należy włączyć ob_flush(), który domyślnie jest wyłączony.

PS. trochę nieładnie, ale edytowałem wcześniejszego posta – swojego ofc. Warto doczytać. ;)

5. BTM napisał(a):
26 marca 2008, 20:38:25

Nie wiem jak z CLI ale:

[btm@~] echo ‘<? while(1) { echo „1”; } ?>’ | php5

Zasypuje mi ekranik 1’dynkami a nie ma tu żadnego ob_start() – ale mówię, może CLI ma inaczej to rozwiązane.

Dla pewności (ob’owej):

[btm@~] echo ‘<? print_r(ob_get_status()); ?>’ | php5
Array
(
)
[btm@~] echo ‘<? ob_start(); print_r(ob_get_status()); ?>’ | php5
Array
( [level] => 1 [type] => 1 [status] => 0 [name] => default output handler [del] => 1
)

6. b4rtaz napisał(a):
26 marca 2008, 20:56:46

BTM: CLI to inna bajka. [:

Zakładając, że używasz takiej zbiedzonej klasy szablonów [...]

Nie do końca takiej zbiedzonej. Bo praktycznie wszystkie klasy szablonów jakie istnieją działają na tej samej zasadzie. Różnią się głównie implementacją – w poprzednim wpisie o tym pisałem.

7. talen napisał(a):
26 marca 2008, 23:08:05

Projekt jest jak najbardziej szczytny i doskonale rozumiem Twoje intencje, ale wydaje mi się, że gra nie jest warta świeczki. Dlaczego ?

1. Wprowadzenie kompresji Gzip jest niemożliwe.
2. Przechwytywanie wyjątków i wyświetlenie ładnej strony jest niemożliwe (choć aktualnie Smarty też tego nie robią).
3. Porównywalną poprawę wydajności uzyskasz przy wykorzystaniu cache’a stron – a korzyści będą z takiego rozwiązania większe.
4. Optymalizacja transferu (trochę ad.1) – zobacz w jaki sposób protokół TCP/IP wysyła dane, a powinieneś dojść do wniosku, że lepiej jest wysłać wszystko za jednym razem.

Opisywaną przez Ciebie funkcjonalność można łatwo zaimplementować w Smartach (zamiast stringów przekazać obiekty, które dopiero kiedy będziemy potrzebować konkretnych danych będą robiły zapytaniado bazy) i uzyskać bardzo podobną funkcjonalność.

Osobiście bardziej poszedł bym w kierunku eksperymentu z nagłówkiem ‘Expires’ i składaniu strony przez JavaScript (z opcją optymalizacji pod przeglądarki).

Generalnie – pomysł naprawdę dobry, ale pójście w kierunku nowego systemu szablonów jest chyba niepotrzebne – większą ‘skuteczność’ (popularyzację pomysłu) chyba uzyskał byś jeśli byłby to plugin do Smartów lub opis techniki korzystania z systemu szablonów który pozwoli na wcześniejsze rozpoczęcie zwracania kodu strony.

8. b4rtaz napisał(a):
27 marca 2008, 07:41:58

talen: Muszę się przyznać, że już od samego początku miałem takie wrażenie, że może być „nie warto”.

Samo istnienie takiej klasy raczej nie spowoduje, że nagle wszyscy zaczną przepisywać swoje aplikacje. Jak już to wypadało by ją zaprezentować z jakimś mechanizmem – np. CMS’em.

Zaimplementować do Smarty? Jest to nie możliwe – strasznie różniąca się mechanika. Góra to można by, nazwy metod czy tagów ala Smarty zaimplementować.

1. Z gzip’em to nie do końca tak wiadomo. Jako że jest to algorytm kompresji słownikowej z algorytmem Hoffmana. Na zasadzie własnego budowy drzewa oraz własnego słownika można by ten problem pominąć. Chociaż jakość kompresji by spadła.
2. Tak, to jest nie do ominięcia.
4. Tutaj nie wiem co masz na myśli. Przeglądarka zamyka socket połączenia dopiero po zakończeniu pobierania (w HTTP 1.0 to na znak zamknięcia socketu na serwerze) z HTTP 1.1 wystarczył by Keep-alive.

9. talen napisał(a):
27 marca 2008, 23:11:10

Ad1. O ile gzip jest realizowany z poziomu php, a nie gdzieś później (serwer WWW, serwer cache).
Ad4. Pakiet = nagłówki + ilość danych. Nagłówki idą zawsze, a ilość danych zmienia się. To trochę naciągana teoria – wpływ na szybkość transmisji jest marginalny, ale mimo wszystko: lepiej przegrywać jeden duży plik, niż wiele małych.

Dodaj komentarz: