Problém s výkonem při větším selectu a použití toArray
- ares952
- Člen | 16
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í:
- Na lokálním PC je výstupem měření času cca 40ms
- 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í:
- Nedělám něco úplně blbě?
- 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 []?
- Nenapadá někoho, jak problém vyřešit s minimem práce?
Děkuji.
- ares952
- Člen | 16
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)
- ares952
- Člen | 16
- 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:
- 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
- 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ší.
- ares952
- Člen | 16
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)
- ares952
- Člen | 16
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
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
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
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
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
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)
- Jan Mikeš
- Člen | 771
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
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
aoffset
, jestli to nebude rychlejší.