Anatomija sporosti — gde vreme zapravo ode

40 min ucitavanja: tri oborene hipoteze i pravi uzrok — imena kolona po celiji, po redu

↑ Nazad

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."

Metod: prvo oboriti hipoteze

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.

hipotezaishodzašto
Tabela ima 600+ kolona → kopiranje reda je skupo❌ na ovom obrascuobrazac 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 putloaderi 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

Uzrok: imena kolona se razrešavaju po ćeliji, po redu

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.

Skaliranje je redovi × kolone²

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.


Popravka sa najboljim odnosom dobitak/rizik

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).


Ostali troškovi — postoje, ali nisu rangirani

Utvrđeno je koji troškovi postoje. Koji od njih nosi koliko — ne sme se tvrditi bez merenja.

Skladište je Vector, ne ArrayList

Redovi 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.

Sortiranje pomera cele redove

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 REDA

Svaki 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 putanji

Na 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.

Filter uključi/isključi

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.


Kako meriti — engine već ima ugrađeno merenje

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:

  1. LoggingDevice=F u gengine.cfg (fiksne tri širine: F + dva razmaka)
  2. testSpeed = true na ta dva mesta, pa rebuild
  3. Uključi/isključi filter i čitaj vreme iz LogFile.txt

Redosled 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.

Izvor: 12-anatomija-sporosti.md