40 min ucitavanja: tri oborene hipoteze i pravi uzrok — imena kolona po celiji, po redu
Simptomi koji su godinama izgledali kao „program je spor":
Ta asimetrija je bila ključ. Da je uzrok GUI ili baza, isključivanje filtera ne bi bilo skuplje od uključivanja — prozor se precrtava jednako, a baza se ne dira.
Formulacija koja najbolje opisuje uzrok, korisnikova:
„Neki parametri za koje mislimo da ih dobijamo direktno, zapravo trčimo po njih."
Pre nego što je nađen uzrok, tri razumne hipoteze su proverene i oborene. To je deo posla koji se lako preskoči, a bez njega se optimizuje pogrešna stvar.
| hipoteza | ishod | zašto |
|---|---|---|
| Tabela ima 600+ kolona → kopiranje reda je skupo | ❌ na ovom obrascu | obrazac ima 30 + 21 + 9 = 60 kolona; onih 600+ je drugi obrazac |
| „Ažuriraj ili ubaci" je O(N²) preko linearne pretrage ključa | ❌ nije taj put | loaderi idu na običan insert(), ne na setReplace() |
| Sortiranje se poziva posle svakog inserta | ❌ | reorder() ima dva poziva u celom engine-u: jednom na kraju učitavanja, i kad korisnik klikne zaglavlje kolone |
Petlja učitavanja CSV-a, za svaku liniju fajla:
dbtObrazacH.clear();
dataH.getBeforeFirst();
while(dataH.getNext().equals(GError.NOERROR)) // ~30 pomeraja kursora PO REDU PODATAKA
{
column = dataH.getInt("Pozicija") - 1; // lookup po IMENU
if(column < token.length)
dbtObrazacH.set(dataH.getString("FieldName").trim(), token[column]); // 2 lookupa + trim
}
dbtObrazacH.insert();A traženje kolone po imenu (gjava/data/GRecordHeader.java:298) je:
public int getColumnIndex(String pName)
{
for(int i = 0; i < getColumnCount(); i++) // linearno
if(getColumn(i).getName().equalsIgnoreCase(pName)) // equalsIgnoreCase
return i;
return -1;
}Linearna pretraga, equalsIgnoreCase (znatno skuplje od equals), getColumnCount() u svakoj iteraciji, getColumn(i) kroz indirekciju i sinhronizovanu kolekciju.
Cena po jednoj liniji fajla: ~30 pomeraja kursora + ~90 pretraga po imenu + ~30 String alokacija od trim() + jedan insert(). Grubo ~750 poređenja stringova po redu.
Za nekoliko stotina hiljada redova — stotine miliona poređenja. To su ta 40 minuta.
Zato je ekran sa dve kolone trenutan, a obrazac sa 60 kolona traje 40 minuta. Nije stvar količine podataka nego oblika petlje: mapiranje ime → indeks se ne menja, a računa se iznova za svaku ćeliju svakog reda.
I zato filtriranje radi za 10 sekundi: filter ide kroz loadData, koji ne razrešava imena po ćeliji — kopira po indeksu. Učitavanje i filtriranje idu različitim putevima, i samo učitavanje plaća imena.
Razrešiti indekse jednom, pre čitanja fajla, u int[] (i keširati redove definicije kolona u običan niz), pa u petlji koristiti set(int, value).
Utvrđeno je koji troškovi postoje. Koji od njih nosi koliko — ne sme se tvrditi bez merenja.
Vector, ne ArrayListRedovi se čuvaju u Vector<GRowDataSource>. Svaki get(i), add(), size() uzima monitor — ne jednom po operaciji, nego po svakom pristupu redu: u sortu (svaka poredba), u filtru (svaki red), u onim getColumn petljama. Konstantan faktor preko celog sistema.
Uz to, getKey(Object) je linearna pretraga sa size() u svakoj iteraciji. Koristi ga „ažuriraj ili ubaci", što je O(N²). Trenutni loaderi taj put ne gaze — ali ostaje mina za svaku buduću masovnu izmenu.
Kandidat: Vector → ArrayList. Klasa je package-private, pa su korisnici prebrojivi. ⚠️ Pre izmene dokazati da se podaci diraju samo sa GUI niti — štampa i XML izvoz mogu da rade sa radne niti, a tada bi skidanje sinhronizacije bilo trka, ne ubrzanje. To je merenje koje tracer ume: beleži nit svakog poziva.
Aktivni algoritam je ShellSort sa Ciurinim nizom gapova — složenost je dobra. Ali:
temp.rawCopy(reorderData.get(i)); // KOPIJA CELOG REDA
while(j >= gap && compareRows(reorderData.get(j-gap), temp, 0) > 0) {
reorderData.get(j).rawCopy(reorderData.get(j-gap)); // KOPIJA CELOG REDA
j -= gap;
}
if(j != i) reorderData.get(j).rawCopy(temp); // KOPIJA CELOG REDASvaki pomeraj je kopija sadržaja reda, ne zamena reference. Za tabelu sa 600+ kolona to je ~600 kopiranja polja po pomeraju.
Iznad koda stoji // TODO Algoritam sortiranja moze da se popravi i dva zakomentarisana O(n²) algoritma. Popravljena je složenost, ali ne cena jedne zamene — popravljen eksponent, ne konstanta.
Pravac: sortirati niz indeksa, pa jednom primeniti permutaciju. Pomeraj postaje jedna dodela reference. ⚠️ Rizik: kod koji računa da se redovi vraćaju u fizičkom redosledu mora da ide kroz permutaciju. Nije jednolinijska izmena.
System.gc() u vrućoj putanjiNa kraju sortiranja stoji doslovno System.gc() — pun stop-the-world GC posle svakog sortiranja. Ranija sumnja da „GC čisti nešto što ne sme" gledala je u pravu pojavu; samo je bila prikačena na pogrešan simptom. Skidanje ovog poziva je besplatan dobitak.
Promena filtera ide na loadData → ponovna puna kopija svih preživelih redova + reorder() + System.gc(). Filter isključen = svi redovi prežive = najveći posao. Odatle asimetrija.
Tracer nije alat za ovo. Operacija ima milione poziva, a JDWP plaća svaki; 40-minutna radnja pod tracerom se ne bi završila. Tracer je za semantiku — šta se poziva i u kom redu — ne za profilisanje.
Ali merenje već postoji u kodu: i sortiranje i uvoz podataka nose boolean testSpeed = false; i, kad je true, ispisuju vreme kroz GLog.println i System.out.println.
Kod GUI aplikacije System.out se ne vidi — zato mora log u fajl:
LoggingDevice=F u gengine.cfg (fiksne tri širine: F + dva razmaka)testSpeed = true na ta dva mesta, pa rebuildLogFile.txtRedosled koji ima smisla: merenje → popravka razrešavanja imena (najveći dobitak, najmanji rizik) → ponovo merenje → pa tek onda razmatrati Vector i sortiranje. I meriti na maloj bazi koja se učitava par sekundi, ne na produkcionoj od 40 minuta.