2013-10-09 15 views
5

In meiner Play-Framework-basierten Web-Anwendung können Benutzer alle Zeilen verschiedener Datenbanktabellen in CSV oder JSON-Format herunterladen. Tabellen sind relativ groß (100k + Zeilen) und ich versuche, das Ergebnis mithilfe von Chunking in Play 2.2 zurückzustreamen.Slow Chunk Antwort in Play 2.2

Das Problem ist jedoch, obwohl println-Anweisungen zeigt, dass die Zeilen in das Chunks.Out-Objekt geschrieben werden, sie werden nicht auf der Clientseite angezeigt! Wenn ich die Anzahl der zurückgeschickten Zeilen limitiere, wird es funktionieren, aber es hat auch eine große Verzögerung am Anfang, die größer wird, wenn ich versuche, alle Zeilen zurückzusenden und eine Zeitüberschreitung verursacht oder der Server nicht mehr genügend Speicherplatz hat.

Ich benutze Ebean ORM und die Tabellen sind indiziert und Abfragen von psql dauert nicht viel Zeit. Hat jemand eine Idee, was das Problem sein könnte?

Ich schätze Ihre Hilfe sehr! Hier

ist der Code für einen der 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"); 
} 

Und der 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 = ";"; 
} 

Und das Modell:

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

Antwort

3

1. meisten Browsern Warten Sie auf 1-5 kb Daten, bevor Sie Ergebnisse anzeigen. Sie können überprüfen, ob Play Framework tatsächlich Daten mit dem Befehl curl http://localhost:9000 sendet.

2. Sie Streamer zweimal zu erstellen, entfernen Sie zuerst final ResultStreamer<EpexEntry> streamer = new ResultStreamer<EpexEntry>();

3. - Sie verwenden Page Klasse für große Datenmengen abrufen - das ist falsch. Tatsächlich machen Sie eine große anfängliche Anfrage und dann eine Anfrage pro Iteration. Das ist LANGSAM. Verwenden Sie simple findIterate().

fügen Sie diese EpexEntry (fühlen Sie sich frei, es zu ändern, wie Sie benötigen)

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

Ihre neuen Stream-Methode Umsetzung:

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 

Und Ihre onReady Methode:

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

4. Ein anderes Problem ist - Sie rufen Out<String>.write() zu oft. Anruf von write() bedeutet, dass der Server einen neuen Datenblock an den Client sofort senden muss. Jeder Anruf von Out<String>.write() hat einen erheblichen Overhead.

Overhead wird angezeigt, da der Server die Antwort in das Chunked-Ergebnis umwandeln muss - 6-7 Byte für jede Nachricht Chunked response Format. Da Sie kleine Nachrichten senden, ist der Overhead erheblich. Außerdem muss der Server Ihre Antwort in ein TCP-Paket schreiben, dessen Größe weit weniger optimal ist. Und Server muss einige interne Aktion durchführen, um einen Chunk zu senden, dies erfordert auch einige Ressourcen. Als Ergebnis ist die Download-Bandbreite weit von optimal.

Hier ist ein einfacher Test: 10000 Zeilen Text TEST0 zu TEST9999 in Chunks senden. Dies dauert im Durchschnitt 3 Sekunden auf meinem Computer. Aber mit Pufferung dauert das 65 ms. Die Downloadgrößen sind 136 kb und 87,5 kb.

Beispiel mit Pufferung:

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

neue BuffOut Klasse. Es ist dumm, ich weiß

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

Diese Implementierung hat 3 Sekunden Download-Zeit und 136 kb Größe

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

Diese Implementierung hat 65 ms Zeit und 87,5 kb Größe

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

Dank Download für Ihre Antworte Viktor. Die Pufferung wird die Geschwindigkeit verbessern, aber die Verzögerung zwischen dem Schreiben und dem Erscheinen im Browser ist immer noch groß. Das Hinzufügen einfacher println-Anweisungen zeigt an, dass alle Zeilen in das out geschrieben werden und wenn es keine mehr gibt und out, wird das Laden in den Browser gestartet !! Und wenn die Anzahl der Zeilen zu groß ist, gibt es einen Timeout-Fehler wie folgt: – p00ya00

+0

[ERROR] [22.10.2013 13: 57: 16.285] [application-akka.actor.default-dispatcher-5] [ActorSystem (Anwendung)] Fehler beim Ausführen des Terminierungsrückrufs aufgrund von [Terminüberschreitungen nach [5000 Millisekunden]] java.util.concurrent.TimeoutException: Terminüberschreitungen nach [5000 Millisekunden] bei scala.concurrent.impl.Promise $ DefaultPromise.ready (Promise.scala: 96) bei scala.concurrent.impl.Promise $ DefaultPromise.result (Promise.scala: 100) bei scala.concurrent.Await $$ anonfun $ Ergebnis $ 1.apply (package.scala: 107) at akka.dispatch.MonitorableThreadFactory $ AkkaForkJoinWorkerThread $$ anon $ – p00ya00

+0

Könnten Sie bitte mehrere 'System.out.println (System.currentTimeMillis())' in Ihren Code einfügen und die Ausgabe hier anzeigen? Bitte plazieren Sie sie hinter 'static Result showEpex()', nach '// ansonsten chunk result und send back' Zeile, kurz vor der letzten Zeile von' public void stream (Out out, Seite page, UserRequest req) 'und kurz davor 'return ok (Chunks) .as (" text/plain ");'? Aus irgendeinem Grund wurde deine Chunk-Ausführung nicht beendet oder nahm so viel Zeit in Anspruch, sodass die Ausführung durch das Spiel-Framework beendet wurde. Haben Sie auch versucht, meinen Code auszuführen? Könnten Sie bitte bestätigen, wenn Sie die gleichen Probleme damit haben? Hier –