2011. december 28., szerda

RssUtils refactor

A ValósKilométer oldalán régóta működik RSS feed olvasó modul, így az oldalmenüben megjelenik több autós oldal hírlistája. A híreket tartalmazó xml-ek beolvasásához a SUN ősrégi RssUtils modulját használom, ami (bár 2003-ban készült) tökéletes az alap adatok beolvasásához (cím, elérhetőség, leírás).

Nemrég egy újabb fejlesztés kapcsán készítettem egy összefoglaló híroldalt, ami több autós portál híreit egészíti ki és csinál belőlük egy összefésült hírcsatornát. Itt már az RSS teljes szolgáltatásrendszerét igénybe vettem volna, de közbejött néhány hiba (pl: "skipDays does not exist or method signature is incorrect"). Ezek egy részét már más megoldotta (author beolvasási hiba és a System.out-ba logolás), de maradt még két apróság. Egyrészt hiányzik a comments elem beolvasása (DocumentHandlerFactory.java), másrészt az alap timeout időnként kevés lehet (RssParserImpl.java), így annak alapbeállítását is megnöveltem (10-15 mp-re).

ValósKilométer hírek oldal


Az elkészült autós hírcsatorna megtekinthető Hírek a oldalon, a módosítások letölthetőek a innen. A hírolvasó jelenleg 15-20 oldalról gyűjt be naponta 100-150 hírt. Sorba rendezi őket és feltölti a hiányzó adatokat (enclosure, author, stb).

2011. november 21., hétfő

Weboldal betöltésének gyorsítása

A valoskilometer.hu oldal kezdőlapjának betöltési ideje időnként fájóan lassú volt, így megnéztem milyen lehetőségeim vannak az oldalbetöltődés gyorsítására. A lassulás egyik oka, hogy az AppEngine (szinte) minden kérésre új context-et hoz létre (ami kb 1 percig él), de ezen csak a fizetős szolgáltatások igénybevételével lehet jelentősen javítani ("Always-on"). Trükk elvileg van rá, de sokat nem segít az sem, bár ártani sem fog. Minden percben csinálunk egy servlet hívást, így mindig van élő context és nem kell újratölteni.


   
    /ping
     Ping a blank page
     every 1 minutes
   


Sajnos ez önmagában nem jelent számottevő gyorsítást, de a hívások átlag válaszideje csökkenhet tőle. A módosítás után még mindig lassú az oldal:

Analytics - Webhelysebesség
/analytics/web/#report/content-site-speed
"Oldal átlagos betöltési ideje (másodperc): 18,47 s (/home)"


Labs - Webhely teljesítése
/webmasters/tools/labs-site-performance
"Az Ön webhelyének oldalai átlagosan 5,2 másodperc alatt töltődnek be (frissítve: 2011.11.06.). Ez lassabb, mint a webhelyek 76%-ának esetében."



Diagnosztika - Feltérképezési statisztika
/webmasters/tools/crawl-stats
"Átlagos: 517 ms"



A kiértékelések közötti sebességkülönbséget a HTML oldalba beágyazott egyéb (vizuális vagy háttér) elemek letöltésének lassúsága adja. Ezek az elemek jellemzően az oldalhoz közvetlenül kapcsolódó és az iFrame-ekbe ágyazott css, js és képfájlok (jpg, png). A lassúlás további kiértékeléséhez a Google Online webhely-tartalom analizáló eszközét használom.

PageSpeed
Az elemzés eredménye az optimalizálás előtt:
"The page Valós Kilométer: Online szervizkönyv ... got an overall Page Speed Score of 76 (out of 100)."

High priority. Egyesítsen képeket CSS sprite elemek formájában, Használja ki a böngésző gyorsítótárazását
Medium priority. Erőforrások kiszolgálása ugyanarról az URL-ről, Engedélyezze a tömörítést, JavaScript-elemzés elhalasztása, Átméretezett képek kiszolgálása
Low priority. Optimalizálja a képeket, Soron belüli kis JavaScript, Átirányítások számának csökkentése, Stílusok és szkriptek sorrendjének optimalizálása, JavaScript csökkentése, Gyorsítótár-érvényesítő megadása, HTML lekicsinyítése, Kicsinyítse le a CSS-t, Tegye a CSS-t a dokumentum fejlécébe, Adjon meg Vary: Accept-Encoding fejlécet, Távolítsa el a lekérdezési karakterláncokat a statikus erőforrásokból

Ebből a lényeges az "Egyesítsen képeket CSS sprite elemek formájában" feladat, mert a többi főként a kiszolgáló oldalon jelentkezik (AppEngine), másrészt a harmadik féltől bevont tartalomnál (Google, YouTube, Libri). A főoldalon 27 db kép van, ami ebben a formában 27 db plusz requestet generál (ez kb 27 * 100-200 ms = 2700 - 5400 ms), ami valószínűleg a lassú betöltés fő okozója.

Ez azt jelenti, hogy a HTML kód 500 ms-os betöltési ideje átlagosan 5000 ms-ra lassul a járulékos letöltések miatt. Ez valójában csak elsőre tűnik nagy problémának, ugyanis az oldal fejlesztésekor is előjött már ez a gond, de akkor nem a CSS sprite-ok, hanem a gyorsítótárazás felé mentem el ("Használja ki a böngésző gyorsítótárazását"). Így ez a lassú betöltés csak az első alkalommal jelentkezik, utána a böngésző már nem tölti le csak a HTML lapot (ha változott). Ez a beállítás az "appengine-web.xml" fájlba került be:


 valoskilometer
 1
 
  
  
  
  
 

 
  
  
  
  
  
  
  
  
  
  
  
 



JMeter

A JMeterrel kimérve látszik, hogy a HTML oldalak nem töltődnek sokáig (417 ms), de a kapcsolt tartalom igen (11901 ms). A kezdőoldalon 37 db kép van amit az első alkalommal egyesével tölt le a böngésző. Ezeket 3 csoportra bontva (nagyságuk szerint) és 3 képfájlba egyesítve (CSS sprite) sokkal gyorsabb eredményre számíthatunk. A CSS sprite alkalmazása után az 51 request-ből 17 lett (azaz 37 képből 3 db), így az 5 mp-es átlagos betöltési idő jelentősen lerövidülhet.

A módosítás előtt majdnem 12 mp volt a főoldal betöltési ideje (a context betöltéssel együtt), ha minden képet külön töltött le a böngésző.


A módosítás után kevesebb kérésszám, gyorsabb betöltési idő eredményezett (7 mp) és javult a PageSpeed értékelés is (76 --> 78).


A végleges eredményt hónapok múlva lehet látni a módosult statisztikából, de várhatóan 30-50% gyorsulás következhet. A következőkben a PageSpeed által körülírt problémákat igyekszem javítani.

2011. október 29., szombat

Google Analytics valós időben

Szeptember végén elindult a Google Analytics valós idejű statisztikai szolgáltatása. Így nemcsak másnap végezhetünk elemzéseket, hanem az adott pillanatban jelenlévő felhasználók adatait és viselkedését is nyomon követhetjük.



A valós idejű riportok jelentősége elsősorban a közösségi média használatakor jöhet elő. A folyamatos felmérhetjük, hogy az adott időpontban megjelenő blogbejegyzés hogyan hat az oldal forgalmára vagy hogy milyen időközönként érdemes újabb posztot írni, milyen forgalmat generálnak a kommentek és a válaszok.


A szolgáltatás jelenleg béta teszt alatt van, de a főoldalról (dashbord) a "Valós idejű" link alatt elérhető (a tesztelőknek). Jelentkeztem a tesztre és szerencsém volt, így kipróbálhattam a Valós Kilométer oldalon. Sajnos a napi látogatószám még nem mutat minden pillanatban elemezhető adatokat, de néha elcsípem a látogatókat.



A kezdeményezés jónak tűnik, de számomra inkább csak látványos és érdekes, mint használható.

2011. október 9., vasárnap

JSTL és a controller az AppEngine-ben

Az eddigi JDO példában a lekért adatokat egy egyszerű szervleten keresztül jelenítettem meg. A jobb használhatóság miatt érdemes szétválasztani a controller és view részeket, így a szervletek mellett megjelennek a JSP lapok, ahol a JSTL-t használom a jobb átláthatóság miatt. Az AppEngine-ben van néhány minimális korlátozás ezért a JSTL-hez is kell egy-két apró trükk. Az első, hogy a JSP lapokon használni kell az isELIgnored="false" kiegészítést.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>

A másik módosítás, hogy az Eclipse projekt build path-jébe fel kell venni a JSTL lib-eket, de ezek nem lehetnek az alap JSTL könyvtárak, hanem a telepített plugin könyvtárban lévőket kell használnunk. Ezeket másoljuk a WEB-INF\lib könyvtárba és adjuk az Eclipse build path-hez a projektben.

\plugins\com.google.appengine.eclipse.sdkbundle_1.5.4.r37v201109211906\appengine-java-sdk-1.5.4\lib\tools\jsp\repackaged-appengine-jakarta-jstl-1.1.2.jar
\plugins\com.google.appengine.eclipse.sdkbundle_1.5.4.r37v201109211906\appengine-java-sdk-1.5.4\lib\tools\jsp\repackaged-appengine-jakarta-standard-1.1.2.jar
http://someprog.blogspot.com/2010/08/google-app-engine-can-not-find-tag.html

Az átalakítás után az alap belépési ponton két irányba irányítjuk át a request-et: home és user.

index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.google.appengine.api.users.UserServiceFactory"%>
<%
    if (UserServiceFactory.getUserService().getCurrentUser() == null) {
		response.sendRedirect("/home.jsp");
    } else {
    	response.sendRedirect("/user/vehicles");
    }
%>

A home.jsp oldalról be lehet lépni és tovább lehet menni a felhasználói oldalra, ahogyan eddig is. A user/vehicles átirányítás a járműlistához megy, ha már belépett a felhasználó. A járműlista controllere a web.xml alapján a VehicleServlet, majd feldolgozás után onnan a vehicle.jsp-re ugrik.


VehicleServlet.java
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
		throws IOException, ServletException {

	UserService userService = UserServiceFactory.getUserService();
	User user = userService.getCurrentUser();

	PersistenceManager pm = null;
	try {

		pm = PMF.get().getPersistenceManager();
		AppUser currentUser = UserDao.getUser(pm, user);
		if (currentUser == null) {
			currentUser = initDatabase(pm, user);
		}
		
		request.setAttribute("appUser", currentUser);
		request.getRequestDispatcher("/WEB-INF/jsp/vehicles.jsp").forward(request, response);

	} finally {
		if (pm != null) {
			pm.close();
		}
	}
	
}

A request-be tesszük az appUser-t, átirányítjuk a megjelenítő oldalra, majd a JSP lapon kivesszük a járműveit és azok bejegyzéseit.

vehicles.jsp

A megjelenített adatok halmazának szebb ábrázolása miatt CSS elemeket is használok, így a JSP lapot nem kell telepakolni style elemekel. A létrejött fájlok nagy száma miatt átcsoportosítottam az elemeket, így külön mappába kerülnek a css és a js elemek is. A JSP lapok is kéttéválnak, ezért a gyökérben lesznek a publikus lapok, a WEB-INF\jsp mapában pedig a közvetlenül nem elérhető lapok. Ez utóbbiakat a szervleteken keresztül lehet elérni, így mindig a megfelelő adat is rendelkezésre fog állni.



A frissített kód itt elérhető, a módosítások eredménye pedig ez lett:


2011. október 8., szombat

Kódformázás és kiemelés

A JDO bejegyzés írásakor szembesülnöm kellett azzal, hogy a Blogger nem tartalmaz automatikus kódformázást, így külső alkalmazást kell használni erre. Az http://alexgorbatchev.com/SyntaxHighlighter/ oldalon van is egy jól használható.


 
// SyntaxHighlighter makes your code snippets beautiful without tiring your servers.
// http://alexgorbatchev.com
var setArray = function(elems) {
    this.length = 0;
    push.apply(this, elems);
    return this;
}

2011. október 7., péntek

JDO és az öröklődés

Az AppEngine egyik erőssége a JDO támogatás. A valós kilométer projekt kapcsán 3 alap osztályon keresztül mutatom be hogyan használható a JDO: AppUser, Vehicle és Issue. A bejelentkezett felhasználó adatait az AppUser, az ő járműveit a Vehicle, és a járművekhez tartozó bejegyzéseket az Issue tartalmazza.

Az adatszerkezet kialakításánál kihasználom az öröklődést és az annotációkkal megvalósítható egy-több kapcsolatot. Ennek részletei az AppEngine JDO oldalán és néhány az öröklődést bemutató oldalon részletesen megtalálhatóak.

Források:
Creating, Getting and Deleting Data in JDO
Google App Engine for Java, Part 3: Persistence and relationships
Google App Engine + JAVA + JDO + Inheritance + One-To-Many Relationships
Unindexed Properties

Mivel a további leírásokhoz kódrészletek is tartoznak, így a Google Code oldalán létrehoztam egy fiókot, ahonnan letölthető a folyamatosan bővített forráskód. A kód eléréséhez érdemes telepíteni az Eclipse-hez a Subclipse plugint, így kicsekkolhatjuk és folyamatosan frissíthetjük a kódot.

Az eddigi egyszerű OpenId belépési rendszert bővítettem tovább, így a http://valoskmdev.appspot.com/valoskmdev oldalon, belépés után a felhasználóhoz feltöltött mock adatokat láthatjuk listázva:


Az adatok létrehozása után a helyi gépen létrejön a "\WEB-INF\appengine-generated" könyvtár és benne két fájl. Az egyik az adatokat tartalmazza (local_db.bin), a másik az automatikusan létrehozott indexeket (datastore-indexes-auto.xml).


 \WEB-INF\appengine-generated\datastore-indexes-auto.xml
<!-- Indices written at Thu, 6 Oct 2011 18:57:53 UTC -->

<datastore-indexes>

    <!-- Used 1 time in query history -->

    <datastore-index kind="AppUser" ancestor="false" source="auto">

        <property name="user" direction="asc"/>

        <property name="timeCreated" direction="asc"/>

    </datastore-index>

    <!-- Used 1 time in query history -->

    <datastore-index kind="Vehicle" ancestor="true" source="auto">

        <property name="timeCreated" direction="desc"/>

    </datastore-index>

    <!-- Used 1 time in query history -->

    <datastore-index kind="Issue" ancestor="true" source="auto">

        <property name="issues_INTEGER_IDX" direction="asc"/>

    </datastore-index>

</datastore-indexes>


Az automatikusan létrehozott indexek a fejlesztői környezetben és a fejlesztés kezdetekor jól használhatók, de éles környezetben érdemes saját, manuális beállításokat használni.

A bemutatóban használt kód nem szép, csak a JDO használatát mutatja. A teljes funkciónalításhoz kellenek controller szervletek, megjelenítő jsp lapok is. A statikus DAO-kat is érdemes Spring IoC-vel injektálva helyettesíteni. A bemutatott kód a JDO könnyebb érthetősége miatt ilyen egyszerű.


// JDO
PersistenceManager pm = null;
try {

	pm = PMF.get().getPersistenceManager();
	AppUser currentUser = UserDao.getUser(pm, user);
    	log.info("currentUser in DB: " + currentUser);
    	if (currentUser == null) {
    		currentUser = initDatabase(pm, user);
    	}
    	req.setAttribute("currentUser", "currentUser");
    	out.println("*******************************");
    	out.println("currentUser: " + currentUser);
    	for (Vehicle vehicle : currentUser.getVehicles()) {
    		out.println(" - vehicle: " + vehicle);
    		for (Issue issue : vehicle.getIssues()) {
        		out.println(" + issue: " + issue);
        	}
    	}

} finally {
    	if (pm != null) {
    		pm.close();
    	}
}


A példában látható, hogy elég a felhasználói adatokat betölteni, utána a kapcsolódó collection-öket már egy egyszerű getter-rel beolvashatjuk (lazy loading). A kód további részletei a code.google.com oldalon böngészhetők.

2011. október 4., kedd

Az első hónap statisztikája

Egy hónap éles üzem után az oldalt 1400-an látogatták meg, akik majdnem 4000 oldalletöltést generáltak. Az AdWords kampány 10 napja alatt az 50 fős látogatói létszám ideiglenesen 150 fősre nőtt.

A látogatók áttekintése:
  • 1 711 Látogatások 
  • 1 416 Egyedi látogatók 
  • 3 940 Oldalmegtekintések 
  • 2,30 Oldal/látogatás 
  • 00:01:45 Átlagos idő a webhelyen 
  • 62,24% Visszafordulások aránya 
  • 81,82% % Új látogatások


Minden forgalmi forrás összesen 1 711 látogatást eredményezett

  • 68,91%Keresési forgalom
  • 1 179 Látogatások

  • 15,72%Hivatkozási forgalom
  • 269 Látogatások

  • 15,31%Közvetlen forgalom
  • 262 Látogatások

  • 0,06%Kampányok
  • 1 Látogatások
A látogatások túlnyomó része a keresésekből származik, a 69 hivatkozó oldal ellenére (269 látogatás). Így a további optimalizálást a keresőmotorok számára érdemes elvégezni, azaz több leírás és releváns adatok használata nagyobb forgalmat generálhat, mint a társoldalak linkelése.