2013-10-09 13 views
5

Nella mia applicazione Web basata su framework di gioco, gli utenti possono scaricare tutte le righe di diverse tabelle di database in formato csv o json. Le tabelle sono relativamente grandi (100k + righe) e sto cercando di riprodurre il risultato utilizzando il chunking in Play 2.2.Risposta a blocchi lenti in Play 2.2

Tuttavia il problema è che le istruzioni println mostrano che le righe vengono scritte nell'oggetto Chunks.Out, non vengono visualizzate nel lato client! Se limito il ritorno delle righe, funzionerà, ma ha anche un grosso ritardo all'inizio che diventa più grande se provo a rimandare tutte le righe e provoca un timeout o il server esaurisce la memoria.

Io uso Ebean ORM e le tabelle sono indicizzate e l'esecuzione di query da psql non richiede molto tempo. Qualcuno ha qualche idea di quale potrebbe essere il problema?

Apprezzo molto il tuo aiuto!

Ecco il codice per uno dei controller:

@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"); 
} 

E lo 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 = ";"; 
} 

E il modello:

@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; 
    } 

risposta

3

1. La maggior parte dei browser attendere 1-5 kb di dati prima di mostrare i risultati. È possibile verificare se Play Framework invia effettivamente i dati con il comando curl http://localhost:9000.

2. Si crea streamer due volte, prima rimuovere final ResultStreamer<EpexEntry> streamer = new ResultStreamer<EpexEntry>();

3. - Si utilizza Page classe per il recupero di grandi set di dati - questo non è corretto. In realtà fai una grande richiesta iniziale e poi una richiesta per iterazione. Questo è LENTO. Usa semplice findIterate().

aggiungere questo al EpexEntry (sentitevi liberi di modificarlo come avete bisogno)

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

la nuova implementazione del metodo flusso:

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 

e il metodo di onReady:

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

4. Un altro problema è - si chiama Out<String>.write() troppo spesso. La chiamata di write() indica che il server deve inviare una nuova porzione di dati al client immediatamente. Ogni chiamata di Out<String>.write() ha un overhead significativo.

L'overhead viene visualizzato perché il server deve avvolgere la risposta in un risultato chunked: 6-7 byte per ogni messaggio Chunked response Format. Poiché si inviano piccoli messaggi, l'overhead è significativo. Inoltre, il server deve racchiudere la risposta nel pacchetto TCP, la cui dimensione sarà molto meno ottimale. E, il server deve eseguire alcune azioni interne per inviare un blocco, anche questo richiede alcune risorse. Di conseguenza, la larghezza di banda del download sarà tutt'altro che ottimale.

Ecco un semplice test: inviare 10000 righe di testo TEST0 a TEST9999 in blocchi. Ciò richiede in media 3 secondi sul mio computer. Ma con il buffering ci vogliono 65 ms. Inoltre, le dimensioni di download sono 136 kb e 87,5 kb.

Esempio con buffer:

controller

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"); 
    } 
} 

nuova classe BUFFOUT. E 'stupido, so

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(); 
    } 
} 

Questa implementazione ha 3 secondi il tempo di download e 136 dimensioni kb

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

Questa implementazione ha 65 ms scaricare tempo e 87,5 dimensioni kb

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

Grazie per la vostra rispondi a Viktor. Il buffering migliorerà la velocità, tuttavia il ritardo tra quando scrivo e quando compare nel browser è ancora enorme. L'aggiunta di semplici istruzioni println mostra che tutte le righe verranno scritte all'esterno e quando non ci sono più e le uscite sono chiuse, iniziano a caricarsi nel browser !! E se il numero di righe è troppo grande c'è un errore di timeout come questo: – p00ya00

+0

[ERRORE] [22/10/2013 13: 57: 16.285] [application-akka.actor.default-dispatcher-5] [ActorSystem (applicazione)] Impossibile eseguire il callback di terminazione, a causa di [Timeout scaduto dopo [5000 millisecondi]] java.util.concurrent.TimeoutException: i tempi scaduti dopo [5000 millisecondi] su scala.concurrent.impl.Promise $ DefaultPromise.ready (Promise.scala: 96) a scala.concurrent.impl.Promise $ DefaultPromise.result (Promise.scala: 100) a scala.concurrent.Await $$ anonfun $ result $ 1.apply (package.scala: 107) at akka.dispatch.MonitorableThreadFactory $ AkkaForkJoinWorkerThread $$ anon $ – p00ya00

+0

Potrebbe inserire diversi 'System.out.println (System.currentTimeMillis())' nel codice e mostrare l'output qui? Posizionali dopo 'risultato statico ShowEpex()', dopo la riga '// altrimenti risultato chunk e invia back', appena prima dell'ultima riga di' stream pubblico vuoto (Out out, Pagina page, UserRequest req) 'e poco prima 'return ok (chunks) .as (" text/plain ");'? Per qualche ragione, l'esecuzione del blocco non è terminata o richiede così tanto tempo, quindi l'esecuzione è stata interrotta dal framework di gioco. Inoltre, hai provato a eseguire il mio codice? Potresti confermare se hai gli stessi problemi? –