Problém s výkonem při větším selectu a použití toArray

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
ares952
Člen | 16
+
0
-

Ahoj,
řeším problém s výkonem nette při provedené selectu z databáze a procházení všech prvků a jejich uložení do pole.

Mám následující kód v prezenteru (jedná se pouze o benchmark, kterým jsem si chtěl ověřit, proč je moje aplikace pomalá):

<?php
		Debugger::timer("select+foreach");
		$totalcount = 10000;

		$psize = 1000;
		$pcount = $totalcount/$psize;
		$items = array();
		$item = array();

		for($part=0; $part<$pcount; $part++)
		{
            $rows = $this->database->table("benchtest")->select("*")->limit($psize, $part*$psize)->fetchAll();
            $pit = array();
            foreach($rows as $row)
            {
                $item = $row->toArray();
                //$item["id"] = $row["id"];
                //$item["time"] = $row["time"];
                $pit[] = $item;
            }

            $items = array_merge($items, $pit);
		}
		Debugger::barDump(Debugger::timer("select+foreach")*1000, "select+foreach");


?>

Problém je následující:

  1. Na lokálním PC je výstupem měření času cca 40ms
  2. Na VPS je výstupem měření času cca 580ms

Procesory jsou cca stejné (XEON E3 @3.5GHz (local) vs. XEON E5 @2.5GHz (VPS)). VPS je sice pomalejší, ale většina aplikace obvykle běží o něco rychleji, navíc VPS není příliš vytížený. Proto mně zarazil tak výrazný rozdíl ve výkonu.

Oboje použivá apache 2.4.25 vs. 2.4.10. Oboje používá mariadb 10.1.20 vs. 10.0. Jediný výrazný rozdíl je v php: 7.1.1 vs. 5.6.29–0. Obojí má nastavený memory limit na 128MB.

V případě, že provedu následující úpravu:

<?php
//$item = $row->toArray();
$item["id"] = $row["id"];
$item["time"] = $row["time"];
?>

Potom se rozdíl změní na 45ms vs. 130ms, což sice není ideální, ale akceptovatelné.

V případě, že nastavím

<?php
$psize = 5000;
?>

Potom se dostávám na čas 3,1s.
2000 – 1s
2500 – 1,35s
3333 – 1,88s

Nastavení

<?php
$psize = 10000;
?>

už přesáhne časový limit 30s na skript. Z toho plyne, že závislost není lineární.
Na verzi nette nezáleží – zkoušeno na 2.2.7 i na dnešní staženou z composeru.

Bohužel problém dokážu v tuhle chvíli řešit jen tím, že nepoužívám ->toArray() na ActiveRow.

Otázky zní:

  1. Nedělám něco úplně blbě?
  2. Opravdu má na starších php verzích ActiveRow takový problém s toArray, případně získáváním prvků z objektu přes []?
  3. Nenapadá někoho, jak problém vyřešit s minimem práce?

Děkuji.

CZechBoY
Člen | 3608
+
0
-

Spis nam rekni proc potrebujes tolik zaznamu ukladat do promenny.

Jinak php5 vs php7 je v rychlosti znatelny.

ares952
Člen | 16
+
0
-

Mám v tabulce cca od 2 do 10 tisíc záznamů, které používám v templatu pro vykreslení do svg nebo jako přímý export do json pro zobrazení externí aplikací.
Asi bych mohl vybírat každý druhý nebo třetí záznam, na kvalitu zobrazení by to nemělo vliv, ale tím bych očividně zamaskoval jiný problém.

Koukal jsem a v jedné tabulce mám cca 25k záznamů.

Jedná se o gpx záznamy cca 50–200km úseků. Kvůli jemnosti zobrazení nemůžu vzít třeba jen 10%.

Editoval ares952 (28. 1. 2017 20:22)

CZechBoY
Člen | 3608
+
0
-

Nejde do gpx zapisovat postupne?

ares952
Člen | 16
+
0
-
  • GPX export samotný se používá v API pro zobrazení na mapách, tam by to šlo rozdělit, ale výsledek bude stejný jako když to zpracuju na vícekrát v jednom průchodu pomocí odkazů do pole místo ->toArray()
  • u GPX dat při zobrazení v SVG není problém použít každý desátý řádek nebo méně, SVG má 800px na šířku, takže více záznamů je zbytečné zobrazovat, „jen“ bych musel vyřešit průměrování
  • problém vidím v tom, že:
  1. z koordinátů se vypočítává ušlá vzdálenost, která je na x ose SVG, pokud vezmu každý n-tý vzorek, zatížím výpočet vzdálenosti chybou
  2. součástí gpx dat jsou data z fitbitu – kroky, kalorie, tepová frekvence, které se zpracovávají. Navíc kroky a kalorie fitbit zpřístupňuje ve vzorcích po 1 minutě, takže ne každý gpx řádek má nenulovou hodnotu kroků a kalorií. Pokud bych vzal jen řádky, které mají validní hodnotu kroků nebo kalorií, potom to bude každý cca 7mý vzorek u dlouhých záznamů, u krátkých v podstatě každý, což můj problém nevyřeší

Pokud bych chtěl zredukovat počet gpx záznamů, tak jedině už při prvotním uploadu gpx souboru s ušlou trasou, abych správně přepočítal tepovku, kroky, kalorie apod. Jenže tady je zase problém v tom, že gpx souborů je více a mohou (a také budou) nahrávány postupně den za dnem. K redukci by tak muselo docházet postupně nějakým inteligentním algoritmem a to mi situaci příliš nezjednoduší.

blaztar
Člen | 93
+
0
-

Nahoď ten příklad na github z test databází. :)

ares952
Člen | 16
+
0
-

App adresář jsem hodil sem: https://github.com/…te-benchtest

V podstatě jen home presenter je upravovaný.

Testovací tabulku si to vytváří samo.

Editoval ares952 (29. 1. 2017 9:22)

CZechBoY
Člen | 3608
+
0
-

A nema kazda udalost jen jeden gpx? Nebo proc davas dohromady vsechny gpx?

ares952
Člen | 16
+
0
-

Každá událost má dva typy gpx dat:
gpx plán – ten je jen jeden
gpx realita – těch může být více, podle počtu přestávek a podle počtu dní po které se jde. Tj. jedna událost může mít více gpx podkladů, ale při prohlížení se musí tvářit jako jeden.

A dohromady realitu sloučit nemůžu, protože:

  • k uploadu gpx dat dochází v průběhu „výletu“ přes mobilní aplikaci každý den (ty soubory budou určitě 3 denně)
  • k gpx datům se musí přes oauth2 stáhnout další data odjinud, to je časově náročné. Stáhnout 1 den jednoho typu dat trvá cca 1s-2s. Ty data jsou 4: hrm, calorie, kroky, stoupání a to po 24h úsecích, protože delší nelze stáhnout. 1 den jsou tak 4s v lepším případě. Při uploadu celého gpx, který má i 8 dní by určitě došlo k timeoutu php skriptu.
CZechBoY
Člen | 3608
+
0
-

Takže jakej je aktuální stav? Tobě přijde z mobilní appky GPX a ty se hned nějaký externí služby ptáš na ty dodatečný informace a zobrazuješ je hned jako odpověď v tý mobilní aplikaci? Nestačilo by nahrát GPX a ty informace zobrazit třeba za 10 minut (až ta externí služba vše pořeší, synchronizujete se atd.)?

ares952
Člen | 16
+
0
-

Myšlenka i zpracování je následující:

  • jdu na několikadenní výlet s ruční gpskou (garmin) a hodinkami (fitbit), které měří kroky, tepovku atd.
  • když jdu spát, nebo když se na delší dobu zastavím, tak gps trakování vypnu
  • sesynchronizuju hodinky se serverem fitbitu
  • pomocí OTG stáhnu zaznamenaný gpx soubor do mobilu a pomocí appky ho uploaduju na svůj server
  • na serveru se při uploadu gpx proparsuje a pomocí oauth2 se k těmto datům stáhnou z fitbitu dodatečné informace (kroky, hrm, cal, apod.)
  • ke každému gpx záznamu bude odpovídat právě jedna hodnota tepovky (ta se zaznamenává po cca 10ti až 15ti s), ke každému záznamu kroků, kalorií a stoupání (záznam po 1 minutě), bude odpovídat právě jeden záznam v gpx. Tohle všechno se uloží do databáze pro pozdější použití:
  • kdokoliv s odkazem si bude moci prohlédnout záznam trasy, statistiku a podobně

V podstatě všechno mám pořešené až na ten problém s toArray na serveru u posledního bodu – tj. prohlížení ve webovém prohlížeči.

V mobilní aplikaci se data nezobrazují, ta slouží jen pro upload gpx souboru (nějaká generická aplikace z google play), abych to nemusel řešit přes webový prohlížeč.

Editoval ares952 (29. 1. 2017 12:01)

CZechBoY
Člen | 3608
+
0
-

Jakej je teda výstup z webu? Ty si v javascriptu zažádáš o data v gpx formátu? Nebo json? Json by šel třeba takhle http://stackoverflow.com/…-using-yield

ares952
Člen | 16
+
0
-

Výstup má 3 sekce:

1. Statistika podle dní, to vypadá takto: https://dl.dropboxusercontent.com/…08/img/1.png
Statistika se počítá jen jednou, data se kešují v databázi, takže se počítají jen jednou → není problém

2. Zobrazení v svg s možností browse: https://dl.dropboxusercontent.com/…08/img/2.png
Data se vyčítají z databáze pokaždé → tady je problém s tím toArray(). Je možné snížit množství bodů na šířku svg objektu, větší rozlišení asi nemá smysl.

3. Zobrazení na mapách: https://dl.dropboxusercontent.com/…08/img/3.png
Data se exportují přes gpx/xml (moje nepřesná informace v příspěvku, že se jedná o json) → tady problém asi nejspíš také je, ale maskuje se to tím, že renderování trvá delší dobu, než stažení a vygenerování dat. Navíc pokud se vyřeší bod 2, tak se tím vyřeší i tento bod, protože používá stejný model pro získávání dat z databáze. Zde se používá více bodů kvůli jemnosti zobrazení při zoomu.

To, čeho se bojím je to, že když se sejde více požadavků naráz, tak tu máme timeout při zobrazení (pokud použiju toArray).

CZechBoY
Člen | 3608
+
0
-

Tak svg můžeš generovat postupně – stejně jako html. V tom problém nevidím. Ten json můžeš pořešit jak jsem posílal odkaz výše.

ares952
Člen | 16
+
0
-

Asi jsem to ne úplně dobře pochopil. Jak lze svg generovat postupně?

CZechBoY
Člen | 3608
+
0
-

No svg je jako xml, ne?
Takže třeba takhle (použití latte)

<svg>
	{var $prev = null}
	{foreach $items as $item}
		{if $prev !== null}
			<line x1="{$prev->x}" x2="{$item->x}" y1="{$prev->y}" y2="{$item->y}">
		{/if}

		{var $prev = $item}
	{/foreach}
</svg>

Editoval CZechBoY (29. 1. 2017 13:40)

ares952
Člen | 16
+
0
-

Aha díky. Tohle mně vůbec nenapadlo (nevěděl jsem o téhle možnosti). To by mohlo můj problém dost dobře vyřešit, protože bych tenhle svg také mohl kešovat a aktualizovat jen při změně. Tím by došlo k rapidnímu nárůstu rychlosti.

Děkuji za nakopnutí :-)

CZechBoY
Člen | 3608
+
0
-

np. Tak dej pak vědět jak to jde.

Jan Mikeš
Člen | 771
+
+1
-

Vrátím se k tomu benchmarku. Zkus upravit takto, měl by být rychlejší a mít menší nároky na paměť:

	$items = [];

	for($part=0; $part<$pcount; $part++) {
		// Bez ->fetchAll()
		foreach ($this->database->table("benchtest")->select("*")->limit($psize, $part*$psize) as $row) {
			$items[] = [
				"id" => $row->id,
				"time" => $row->time,
			];
}
	}

Taktéž bych porovnal rychlost DB pokud vyselectuješ všechny záznamy najednou bez limit a offset, jestli to nebude rychlejší.

Editoval Jan Mikeš (29. 1. 2017 15:01)

ares952
Člen | 16
+
0
-

Bez limit a offset jsem to dělal, rozdíl tam samozřejmě je, ale já to rozdělil právě kvůli tomu ->toArray(), protože bez rozdělení docházelo k timeoutu.

Tvé řešení je zhruba stejně rychlé jako moje bez toArray();

Na keši pracuji a dám určitě vědět, ale výsledek je už teď jasný :-).

Co se týká memory náročnosti, potom mi vyšlo:
php 7:
Moje řešení bez toArray:
11.34MB
Přímé query:
8.84MB
Tvé řešení:
10.21MB

php 5:
Moje řešení bez toArray:
16.98MB
Přímé query:
11.93MB
Tvé řešení:
18.00MB

Vše při selectu všech prvků jedním dotazem.

Jan Mikeš napsal(a):

Vrátím se k tomu benchmarku. Zkus upravit takto, měl by být rychlejší a mít menší nároky na paměť:

	$items = [];

	for($part=0; $part<$pcount; $part++) {
		// Bez ->fetchAll()
		foreach ($this->database->table("benchtest")->select("*")->limit($psize, $part*$psize) as $row) {
			$items[] = [
				"id" => $row->id,
				"time" => $row->time,
			];
}
	}

Taktéž bych porovnal rychlost DB pokud vyselectuješ všechny záznamy najednou bez limit a offset, jestli to nebude rychlejší.

ares952
Člen | 16
+
0
-

Děkuji za nakopnutí. Cache jsem naimplementoval a funguje to tak jak má. Teď je to zcela srovnatelné s localhostem, někdy i rychlejší. Ještě zkusím poladit nějaké další optimalizace, ale tohle výrazně pomohlo.

Díky