2013-10-09 16 views
5

W mojej aplikacji sieciowej opartej na grze, użytkownicy mogą pobierać wszystkie wiersze różnych tabel bazy danych w formacie CSV lub JSON. Tabele są stosunkowo duże (100k + wiersze) i staram się odtwarzać wynik za pomocą chunkingu w Play 2.2.Powolna odpowiedź na porcję w Play 2.2

Jednak problem polega na tym, że instrukcje println pokazują, że wiersze są zapisywane do obiektu Chunks.Out, nie pojawiają się po stronie klienta! Jeśli ograniczę liczbę odesłanych wierszy, zadziała, ale na początku będzie duże opóźnienie, które będzie większe, jeśli spróbuję odesłać wszystkie wiersze i upłynie limit czasu lub serwerowi skończy się pamięć.

Używam ORMS Ebean, a tabele są indeksowane, a zapytanie z psql nie zajmuje dużo czasu. Czy ktoś ma pojęcie, co może być problemem?

Bardzo dziękuję za pomoc!

Oto kod dla jednego z kontrolerów:

@SecureSocial.UserAwareAction 
public static Result showEpex() { 

    User user = getUser(); 
    if(user == null || user.getRole() == null) 
     return ok(views.html.profile.render(user, Application.NOT_CONFIRMED_MSG)); 

    DynamicForm form = DynamicForm.form().bindFromRequest(); 
    final UserRequest req = UserRequest.getRequest(form); 

    if(req.getFormat().equalsIgnoreCase("html")) { 
     Page<EpexEntry> page = EpexEntry.page(req.getStart(), req.getFinish(), req.getPage()); 
     return ok(views.html.epex.render(page, req)); 
    } 

    // otherwise chunk result and send back 
    final ResultStreamer<EpexEntry> streamer = new ResultStreamer<EpexEntry>(); 
    Chunks<String> chunks = new StringChunks() { 
      @Override 
      public void onReady(play.mvc.Results.Chunks.Out<String> out) { 

       Page<EpexEntry> page = EpexEntry.page(req.getStart(), req.getFinish(), 0); 
       ResultStreamer<EpexEntry> streamer = new ResultStreamer<EpexEntry>(); 
       streamer.stream(out, page, req); 
      } 
    }; 
    return ok(chunks).as("text/plain"); 
} 

A streamer:

public class ResultStreamer<T extends Entry> { 

private static ALogger logger = Logger.of(ResultStreamer.class); 

public void stream(Out<String> out, Page<T> page, UserRequest req) { 

    if(req.getFormat().equalsIgnoreCase("json")) { 
     JsonContext context = Ebean.createJsonContext(); 
     out.write("[\n"); 
     for(T e: page.getList()) 
      out.write(context.toJsonString(e) + ", "); 
     while(page.hasNext()) { 
      page = page.next(); 
      for(T e: page.getList()) 
       out.write(context.toJsonString(e) + ", "); 
     } 
     out.write("]\n"); 
     out.close(); 
    } else if(req.getFormat().equalsIgnoreCase("csv")) { 
     for(T e: page.getList()) 
      out.write(e.toCsv(CSV_SEPARATOR) + "\n"); 
     while(page.hasNext()) { 
      page = page.next(); 
      for(T e: page.getList()) 
       out.write(e.toCsv(CSV_SEPARATOR) + "\n"); 
     } 
     out.close(); 
    }else { 
     out.write("Invalid format! Only CSV, JSON and HTML can be generated!"); 
     out.close(); 
    } 
} 


public static final String CSV_SEPARATOR = ";"; 
} 

A model:

@Entity 
@Table(name="epex") 
public class EpexEntry extends Model implements Entry { 

    @Id 
    @Column(columnDefinition = "pg-uuid") 
    private UUID id; 
    private DateTime start; 
    private DateTime finish; 
    private String contract; 
    private String market; 
    private Double low; 
    private Double high; 
    private Double last; 
    @Column(name="weight_avg") 
    private Double weightAverage; 
    private Double index; 
    private Double buyVol; 
    private Double sellVol; 

    private static final String START_COL = "start"; 
    private static final String FINISH_COL = "finish"; 
    private static final String CONTRACT_COL = "contract"; 
    private static final String MARKET_COL = "market"; 
    private static final String ORDER_BY = MARKET_COL + "," + CONTRACT_COL + "," + START_COL; 

    public static final int PAGE_SIZE = 100; 

    public static final String HOURLY_CONTRACT = "hourly"; 
    public static final String MIN15_CONTRACT = "15min"; 

    public static final String FRANCE_MARKET = "france"; 
    public static final String GER_AUS_MARKET = "germany/austria"; 
    public static final String SWISS_MARKET = "switzerland"; 

    public static Finder<UUID, EpexEntry> find = 
      new Finder(UUID.class, EpexEntry.class); 

    public EpexEntry() { 
    } 

    public EpexEntry(UUID id, DateTime start, DateTime finish, String contract, 
      String market, Double low, Double high, Double last, 
      Double weightAverage, Double index, Double buyVol, Double sellVol) { 
     this.id = id; 
     this.start = start; 
     this.finish = finish; 
     this.contract = contract; 
     this.market = market; 
     this.low = low; 
     this.high = high; 
     this.last = last; 
     this.weightAverage = weightAverage; 
     this.index = index; 
     this.buyVol = buyVol; 
     this.sellVol = sellVol; 
    } 

    public static Page<EpexEntry> page(DateTime from, DateTime to, int page) { 

     if(from == null && to == null) 
      return find.order(ORDER_BY).findPagingList(PAGE_SIZE).getPage(page); 
     ExpressionList<EpexEntry> exp = find.where(); 
     if(from != null) 
      exp = exp.ge(START_COL, from); 
     if(to != null) 
      exp = exp.le(FINISH_COL, to.plusHours(24)); 
     return exp.order(ORDER_BY).findPagingList(PAGE_SIZE).getPage(page); 
    } 

    @Override 
    public String toCsv(String s) { 
     return id + s + start + s + finish + s + contract + 
       s + market + s + low + s + high + s + 
       last + s + weightAverage + s + 
       index + s + buyVol + s + sellVol; 
    } 

Odpowiedz

3

1. Większość przeglądarek zaczekaj na 1-5 kb danych przed wyświetleniem jakichkolwiek wyników. Możesz sprawdzić, czy Play Framework faktycznie wysyła dane za pomocą komendy curl http://localhost:9000.

2. Ty dwukrotnie tworzyć chorągiew, usuń najpierw final ResultStreamer<EpexEntry> streamer = new ResultStreamer<EpexEntry>();

3. - użyć Page klasę za zdobycie dużego zestawu danych - jest nieprawidłowe. Właściwie wykonujesz jedną dużą początkową prośbę, a następnie jedno żądanie na iterację. To jest WOLNE. Użyj prostej funkcji findIterate().

dodać do EpexEntry (krępuj się go zmienić, jak trzeba)

public static QueryIterator<EpexEntry> all() { 
    return find.order(ORDER_BY).findIterate(); 
} 

twoja nowa realizacja metoda stream:

public void stream(Out<String> out, QueryIterator<T> iterator, UserRequest req) { 

    if(req.getFormat().equalsIgnoreCase("json")) { 
     JsonContext context = Ebean.createJsonContext(); 
     out.write("[\n"); 
     while (iterator.hasNext()) { 
      out.write(context.toJsonString(iterator.next()) + ", "); 
     } 
     iterator.close(); // its important to close iterator 
     out.write("]\n"); 
     out.close(); 
    } else // csv implementation here 

A twój onReady metoda:

  QueryIterator<EpexEntry> iterator = EpexEntry.all(); 
      ResultStreamer<EpexEntry> streamer = new ResultStreamer<EpexEntry>(); 
      streamer.stream(new BuffOut(out, 10000), iterator, req); // notice buffering here 

4. Kolejny problem to - zbyt często dzwonisz pod numer Out<String>.write(). Call of write() oznacza, że ​​serwer musi wysłać nowy fragment danych do klienta natychmiast. Każde wywołanie Out<String>.write() ma znaczny narzut.

Pojawia się napowietrzenie, ponieważ serwer musi zawijać odpowiedź na wynik porcji - 6-7 bajtów dla każdej wiadomości Chunked response Format. Ponieważ wysyłasz małe wiadomości, obciążenie jest znaczące. Ponadto serwer musi zawijać odpowiedź w pakiecie TCP, którego rozmiar będzie znacznie mniejszy od optymalnego. Serwer musi wykonać pewne wewnętrzne działanie, aby wysłać porcję, to także wymaga pewnych zasobów. W rezultacie pobieranie przepustowości będzie dalekie od optymalnego.

Oto prosty test: wyślij 10000 linii tekstu TEST0 do TEST9999 w porcjach. Zajmuje to średnio 3 s na moim komputerze. Ale z buforowaniem trwa to 65 ms. Ponadto rozmiary pobierania wynoszą 136 kb i 87,5 kb.

Przykład z buforowania:

Kontroler

public class Application extends Controller { 
    public static Result showEpex() { 
     Chunks<String> chunks = new StringChunks() { 
      @Override 
      public void onReady(play.mvc.Results.Chunks.Out<String> out) { 
       new ResultStreamer().stream(out); 
      } 
     }; 
     return ok(chunks).as("text/plain"); 
    } 
} 

nowa klasa Wygrzewu. To głupie, wiem

public class BuffOut { 
    private StringBuilder sb; 
    private Out<String> dst; 

    public BuffOut(Out<String> dst, int bufSize) { 
     this.dst = dst; 
     this.sb = new StringBuilder(bufSize); 
    } 

    public void write(String data) { 
     if ((sb.length() + data.length()) > sb.capacity()) { 
      dst.write(sb.toString()); 
      sb.setLength(0); 
     } 
     sb.append(data); 
    } 

    public void close() { 
     if (sb.length() > 0) 
      dst.write(sb.toString()); 
     dst.close(); 
    } 
} 

Ta implementacja ma 3 drugi czas pobierania i 136 rozmiar kb

public class ResultStreamer { 
    public void stream(Out<String> out) { 
    for (int i = 0; i < 10000; i++) { 
      out.write("TEST" + i + "\n"); 
     } 
     out.close(); 
    } 
} 

Ta implementacja mieć 65 ms czas i 87,5 rozmiar kB Pobierz

public class ResultStreamer { 
    public void stream(Out<String> out) { 
     BuffOut out2 = new BuffOut(out, 1000); 
     for (int i = 0; i < 10000; i++) { 
      out2.write("TEST" + i + "\n"); 
     } 
     out2.close(); 
    } 
} 
+0

Dziękujemy za Twoje odpowiedz Viktor. Buforowanie poprawi prędkość, ale opóźnienie pomiędzy zapisywaniem i wyświetlaniem w przeglądarce jest wciąż ogromne. Dodanie prostych instrukcji println pokazuje, że wszystkie wiersze zostaną wypisane na zewnątrz, a gdy nie ma już żadnych i zostaną zamknięte, zaczynają ładowanie w przeglądarce !! A jeśli liczba wierszy jest zbyt duża, pojawia się błąd przekroczenia limitu czasu: – p00ya00

+0

[BŁĄD] [22.10.2013 13: 57: 16.285] [application-akka.actor.default-dispatcher-5] [ActorSystem (application)] Nie udało się uruchomić wywołania zwrotnego z terminem z powodu [Futures przekroczono limit czasu po [5000 milisekund]] java.util.concurrent.TimeoutException: Futures przekroczono limit czasu po [5000 milisekund] na scala.concurrent.impl.Promise $ DefaultPromise.ready (Promise.scala: 96) na scala.concurrent.impl.Promise $ DefaultPromise.result (Promise.scala: 100) na scala.concurrent.Await $$ anonfun $ wynik $ 1.apply (package.scala: 107) na akka.dispatch.MonitorableThreadFactory $ AkkaForkJoinWorkerThread $$ anon $ – p00ya00

+0

Czy mógłbyś wstawić kilka kodów "System.out.println (System.currentTimeMillis())" do swojego kodu i pokazać tutaj wyjście? Proszę umieścić je po 'static Result showEpex()', po '// w przeciwnym razie wynik porcji i odesłanie', tuż przed ostatnią linią 'public void stream (Out out, Page page, UserRequest req) i tuż przed 'return ok (chunks) .as (" text/plain ");'? Z jakiegoś powodu wykonanie fragmentów nie zakończyło się lub zajęło tak dużo czasu, więc wykonanie zostało zakończone przez strukturę gry. Czy próbowałeś uruchomić mój kod? Czy mógłbyś potwierdzić, czy masz takie same kłopoty? –

Powiązane problemy