2013-10-09 4 views
5

В моем веб-приложении на основе play-framework пользователи могут загружать все строки разных таблиц базы данных в формате csv или json. Таблицы относительно большие (100k + rows), и я пытаюсь передать результат, используя chunking в Play 2.2.Медленная реакция на фрагмент в Play 2.2

Однако проблема заключается в том, что инструкции println показывают, что строки записываются в объект Chunks.Out, они не отображаются на стороне клиента! Если я ограничу строки, отправляемые обратно, это будет работать, но также имеет большую задержку в начале, которая становится больше, если я пытаюсь отправить все строки и вызывать тайм-аут, или на сервере заканчивается память.

Я использую Ebean ORM, и таблицы индексируются, а запросы из psql не занимают много времени. Кто-нибудь знает, что может быть проблемой?

Я ценю вашу помощь!

Вот код для одного из контроллеров:

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

И растяжка:

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

И модель:

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

ответ

3

1. Большинство браузеров подождите 1-5 килобайт данных, прежде чем показывать какие-либо результаты. Вы можете проверить, действительно ли Play Framework отправляет данные командой curl http://localhost:9000.

2. Вы создаете косу дважды, удалить первый final ResultStreamer<EpexEntry> streamer = new ResultStreamer<EpexEntry>();

3. - Вы используете Page класс для получения большого набора данных - это неверно. Фактически вы делаете один большой начальный запрос, а затем один запрос на итерацию. Это МЕДЛЕННО. Используйте простой метод findIterate().

добавить это EpexEntry (вы можете изменить его, как вам нужно)

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

ваш новый реализация метода потока:

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 

И ваш onReady метод:

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

4. Другая проблема - вы слишком часто звоните Out<String>.write(). Вызов write() означает, что серверу необходимо отправить новый фрагмент данных клиенту сразу. Каждый вызов Out<String>.write() имеет значительные накладные расходы.

Накладные расходы возникают из-за того, что серверу необходимо обернуть ответ в chunked result - 6-7 байт для каждого сообщения Chunked response Format. Поскольку вы отправляете небольшие сообщения, накладные расходы значительны. Кроме того, сервер должен обернуть ваш ответ в TCP-пакете, размер которого будет значительно меньше оптимального. И сервер должен выполнить некоторые внутренние действия для отправки фрагмента, для этого также требуются некоторые ресурсы. В результате пропускная способность загрузки будет далека от оптимальной.

Простой тест: отправьте 10000 строк текста TEST0 на TEST9999 в кусках. Это занимает в среднем 3 секунды на моем компьютере. Но с буферизацией это занимает 65 мс. Кроме того, размер загрузки составляет 136 кб и 87,5 кб.

Пример с буферизацией:

Контроллер

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

новый класс BuffOut. Это глупо, я знаю

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

Эта реализация имеет 3 секунды времени загрузки и 136 размер т.п.н.

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

Эта реализация есть 65 мс время загрузки и 87,5 размер т.п.н.

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

Спасибо за ваш ответь Виктор. Буферизация улучшит скорость, однако задержка между тем, когда я пишу, и когда она появляется в браузере, по-прежнему огромна. Добавление простых операторов println показывает, что все строки будут записаны в выход, а когда их больше нет, они закрываются, они начинают загружаться в браузере! И если количество строк слишком велико, есть ошибка таймаута: – p00ya00

+0

[ERROR] [10/22/2013 13: 57: 16.285] [application-akka.actor.default-dispatcher-5] [ActorSystem (приложение)] Не удалось выполнить обратный вызов завершения, из-за [Futures timed out after [5000 миллисекунд]] java.util.concurrent.TimeoutException: Фьючерсы, истекающие после [5000 миллисекунд] в файле scala.concurrent.impl.Promise $ DefaultPromise.ready (Promise.scala: 96) at scala.concurrent.impl.Promise $ DefaultPromise.result (Promise.scala: 100) at scala.concurrent.Await $$ anonfun $ result $ 1.apply (package.scala: 107) на akka.dispatch.MonitorableThreadFactory $ AkkaForkJoinWorkerThread $$ anon $ – p00ya00

+0

Не могли бы вы вставить несколько 'System.out.println (System.currentTimeMillis())' в свой код и показать вывод здесь? Поместите их после 'static Result showEpex()' после строки '// else chunk result and send back', перед предыдущей строкой' public void stream (Out out, Страница , UserRequest req) 'и непосредственно перед 'return ok (chunks) .as (" text/plain ");'? По какой-то причине выполнение вашего куска не закончилось или не заняло так много времени, поэтому исполнение было прекращено с помощью игровой среды. Кроме того, вы пытались запустить мой код? Не могли бы вы подтвердить, есть ли у вас с этим проблемы? –

Смежные вопросы