Problema rendimiento / concurrencia

Ranthas

Tengo un problema de rendimiento que he intentado resolver mediante paralelización. Aunque he obtenido mejor rendimiento, sigue siendo un rendimiento pobre. A ver si a alguien se le ocurre algo:

Tengo una aplicación en JavaFX que muestra una tabla. Esa tabla tiene una columna donde se muestra una imagen:

El problema viene con el objeto que pinta la imagen. La instanciación de ese objeto es muy costosa, y se debe instanciar tantas veces como filas tenga la tabla. Cada tabla tiene de media unas 350 filas, con lo que usando una solución secuencial, la pantalla tarda en renderizarse hasta 14 segundos.

List<SetCardTableRecord> tableRecords = cardService.findAllBySet(set).stream().map(SetCardTableRecord::new).collect(Collectors.toList());

El primer cambio ha sido sustituir el stream por parallelStream, mejorando de 14 a 6 segundos:

List<SetCardTableRecord> tableRecords = cardService.findAllBySet(set).parallelStream().map(SetCardTableRecord::new).collect(Collectors.toList());

Pero sigue sin ser suficiente. Entonces he optado por paralelizar yo el proceso; parto la lista de registros en N sublistas de 10 elementos y lanzo N hilos de ejecución:

        List<SetCardTableRecord> tableRecords = new ArrayList<>();
        List<Card> cards = cardService.findAllBySet(set);
        int chunkSize = 10;
        int finalized = 0;
        AtomicInteger counter = new AtomicInteger();
        Collection<List<Card>> cardsChunkedList = cards.stream().collect(Collectors.groupingBy(it -> counter.getAndIncrement() / chunkSize)).values();
        ExecutorService executorService = Executors.newFixedThreadPool(cardsChunkedList.size());
        CompletionService<List<SetCardTableRecord>> completionService = new ExecutorCompletionService<>(executorService);

    for (List<Card> cardsChunked : cardsChunkedList) {
        completionService.submit(new SetTableRecordCallable(cardsChunked));
    }

    while (finalized < cardsChunkedList.size()) {
        try {
            Future<List<SetCardTableRecord>> future = completionService.take();
            tableRecords.addAll(future.get());
            finalized++;
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

Esto presenta una ligera mejora, ya que el tiempo baja a 4 segundos, pero sigue siendo demasiado. He ido jugando con el nº de elementos de las sublistas, pero los valores entre 10 y 15 son los óptimos. Si bajo de 10, el overhead de crear los hilos penaliza, y si subo a 20 o más, la instanciación del objeto de la imagen penaliza el tiempo.

¿A alguien se le ocurre una mejor manera de paralelizar o de resolver este problema? Lo ideal es que el tiempo de ejecución de esta rutina sea de 0,5 a 1,5 segundos.

Lecherito

Entonces el problema es en el cargar las imagenes?

Si es asi, hay que mirar donde se esta yendo el tiempo. Se va el tiempo en el IO o una vez que tienes la imagen en construir el objeto?

  1. Si es en IO, aparte de la paralelizacion que has hecho, poco mas se puede hacer. Algo que haria es cargar las imagenes conforme esten listas
  2. Si es en construir el objeto... algo hay ahi que se tiene que cambiar si o si.

Como usuario lo que mas detesto es que se cargue absolutamente todo a la vez (ya que eso supone un tiempo largo de espera), si puedes cargar el texto antes que la imagen, ya ves algo en pantalla y sabes que se esta trabajando en ello.

2 respuestas
pineda

en lugar de agrupar y abrir un hilo por grupo, yo haría un hilo por cada elemento, pero dentro de un pool para que solo se ejecutasen esas 10 que quieres de máximo de forma concurrente, con esto creo que te quitarías la posibilidad de que haya varias lentas en un mismo grupo

dicho esto, como dice #2 habría que centrarse más en por que ese proceso de cada imagen tarda tanto

1 4 respuestas
Ranthas

#2 Tengo localizado el cuello de botella justo en la instanciación del objeto ImageView. Le paso la URI de la imagen y este objeto internamente abre un socket, descarga la imagen en un buffer y la renderiza. Hacer el new de ese objeto es lo que tarda una barbaridad y ralentiza el renderizado de la pantalla.

Lamentablemente, la pantalla se renderiza toda la vez; lo que podría hacer es separar en 2 tareas asíncronas la renderización: por un lado todo el contenido de la pantalla y por otro las imágenes.

2 respuestas
isvidal

#4 Muy evidente entonces que las imagenes deberian cargar una vez renderizada la lista.

Pero no tengo ni puta idea de Java, en JS es un problema trivial.

1 respuesta
MTX_Anubis

#4 Vas a tener que descargar las imagenes en background, yo lo haría en un pool como dice #3

O mira a ver si esto te soluciona la papeleta: https://docs.oracle.com/javase/8/javafx/api/javafx/scene/image/Image.html#Image-java.lang.String-boolean-

1 respuesta
Lecherito

Si no furula lo del background y sigues queriendo usar lo tuyo con un ExecutorService (como dice #3) y las imagenes se podrian construir con https://docs.oracle.com/javase/8/javafx/api/javafx/scene/image/Image.html#Image-java.io.InputStream-

1 respuesta
B

A parte de lo ya comentado... ¿podrías hacer "clipping"? .. te fijas en la parte visible y cargas lo que se ve más un poco por arriba y un poco por abajo.

Supongo que también has mirado de bajar la calidad de las imágenes todo lo posible.

2 respuestas
Ranthas

Muchas gracias por las respuestas; vamos por partes:

#3 Probaré a instanciar el pool de hilos con el total de elementos en lugar de con el nº de sublistas, a ver como sale

#5 Ya sé que en JS es trivial, las maravillas de la asincronía

#6 Ya he probado lo de descargar en background, justo ví ese enlace mientras peleaba con la documentación, pero no termina de funcionar, ya que al renderizarse la tabla, parece que deja de "escuchar" y no carga las imágenes, como si la ejecución en segundo plano no se comunicase con la vista principal.

#7 Para eso tendré primero que descargar las imágenes a disco y luego meterlas en el InputStream; esperaba que esa fuera mi última opción, aunque esto es simplemente manía personal; son unas 55.000 imágenes de entre 9 y 15 kB cada una, unos 805 MB. Mirando el fuente de la clase Image, debería ser más rápido que mostrando la imagen desde una URL. También lo probaré.

#8 Podría probar también, pero no creo que sea debido al peso de las imágenes, ya que son relativamente pequeñas. Bajar la calidad de las imágenes no lo he hecho ya que las cojo directamente de una URL.

Muchas gracias por las respuestas, iré probando a ver si avanza

EDIT, ahora que pienso, puedo descargar en un inputstream directamente desde la URL, no me hace falta descargar a disco; mucho mejor

1 respuesta
willy_chaos

en android habia librerias de cache de imagenes.

mira si hay lo mismo.

te gusrdaba la imagen en local una vez descargada y cada x tiempo hace un refresh

Esta claro que el tiempo se va descargando la imagen.

Haz que cargue una 'no-imagen' o 'loading' y luego fuerzas un download cuando ya este toda la tabla ( las que tenga el usuario dentro de la ventana que visualiza)

mientras descarga, metele una imagen con un spinner en plan loading, asi el usuario entiende que se esta descargando o cargando la imagen

Ranthas

Con el cambio de #3 estoy ya en un rango de tiempo "aceptable". Hay algunas colecciones donde las imágenes tienen más calidad y ralentizan el proceso, pero es un punto de partida.

Supongo que si me quedo en un punto muerto y no consigo optimizar más los tiempos, podría levantar un pantalla de espera en lo que carga la tabla...aunque eso no es resolver el problema, es solo ponerle una pantalla de carga.

1
Kaledros

Yo haría lo que dice #8, algo parecido al occlusion culling. Si cargar 350 imágenes te cuesta 4 segundos, cargas 40 en medio segundo y el resto las vas cargando en background, que por muy rápido que haga scroll el usuario no creo que llegue a acabarse esas 40 imágenes en 3.5 segundos.

1
Lecherito

#9 Siempre puedes usar el URL("urlhere").openStream() y ya tienes un InputStream

1
KoRMuZ

#1 No se si será una idiotez para lo que estés desarrollando pero, ¿has probado con lazy-load?

Cargas la lista vacía, y las imágenes que están en pantalla/van a mostrarse próximamente son las que cargas.

Digamos que cuando estén a X filas de aparecer en pantalla, cargas la foto. De esa forma renderizarías la tabla vacía e irías cargando las imágenes conforme se vayan necesitando, ahorrando datos por ese lado también si no las tienes en disco.

1 1 respuesta
Phatality

Lo que dice #14 es lo primero q me viene a la cabeza.

r2d2rigo

Lo que necesitas se llama virtualizacion de la UI. No sabria decirte como se hace en JavaFX porque es prehistorico pero la mayoria de frameworks de UI modernos lo tienen por defecto en los ListView/GridView.

Aqui un hilo de SO que comenta el tema: https://stackoverflow.com/questions/20995444/what-are-virtualized-controls-mentioned-in-javafx-documentation

Simba26

No has pensado rehacer la app con go si quieres concurrencia?

1 respuesta
r2d2rigo

#17 a decir tonterias a fedadev.

Ranthas

Bueno, al final lo he resuelto.

He descargado unas cuantas imágenes a disco y he probado la instanciación del ImageView pasandole un InputStream que lea de disco en lugar de una URL. El rendimiento no puede ser mejor: la colección más grande (unas 1000 imágenes) no llega ni a 600 ms en cargar así.

He probado poniendo las imágenes en el HDD en lugar de en el SSD y sigue teniendo tiempos similares. Usando paralelización es prácticamente instantáneo.

Aún así, me ha picado en el orgullo y he intentado implementar una estrategia de lazy-loading en la carga de la tabla. El resultado ha sido bueno (aunque el código es un churro que flipas) pero al hacer el scroll vertical, la pantalla da unos pequeños tirones y además, tarda algo más que la solución anterior (algo más de 1s)

De momento voy a estudiar como me afecta tener que descargar las imágenes de los cartones a disco. Después veré que solución uso.

Muchas gracias a todos por las respuestas y por la ayuda. Me alegra ver que fuera de feda sabemos comportarnos.

1 1 respuesta
Simba26

#17 Que tiene de tonteria esto? usar Go que le da 1000 patadas a java? que puedes desarrollar una api sin ninguna dependencias de terceros si lo quieres

2 respuestas
r2d2rigo

#20 pero tu te has parado siquiera a leer el problema que tiene? Que frameworks de UI oficiales tiene Go como para replicar lo que ya tiene hecho en Java?

Mira que sois cansinos los gophers con el evangelismo.

3 1 respuesta
cabron

#20

Pues tiene de tontería que has asociado concurrencia a go, como si fuese el único lenguaje que la soporta o el que mejor lo hace

1 respuesta
Simba26

#21 Compara el consumo de recursos de un thread de java con una goroutine, segundo de acuerdo a los frameworks UI esto es relativo con la CDN de bootstrap es suficiente para lo que haces y Go tiene su motor de plantilla ( no es el mejor pero para lo que lo gastas es suficiente) incluso hay un libreria para graficos (Go echarts https://github.com/go-echarts/go-echarts. Pero tu mismo se supone que es un foro para compartir ideas pero si tu quieres seguir usando Java FX en el 2021 casi 10 años despues de su ultima actualizacion adelante.

1 respuesta
Simba26

#22 Muchos hay claro que si. Te dejo un link puedes mirar los mejores lenguajes para programacion concurrente y veras la posicion de Go https://www.slant.co/topics/6024/programming-languages-for-concurrent-programming.

1 respuesta
cabron

#24

y qué validez tiene una página que no da ni un solo motivo técnico y se basa en votaciones de sus usuarios registrados?

1 1 respuesta
Simba26

#25 La misma validez de tu opinion

1 respuesta
r2d2rigo

#23 mucho mejor una tecnologia muerta pero que soporta al 100% las funcionalidades que necesitas que una nueva y hipster que hace aguas por todos lados.

Que tal llevais la gestion de dependencias en 2021? Todavia seguis teniendo que hacer un git clone de master del repo?

1 1 respuesta
cabron

#26

qué opinión? que no es el único lenguaje que tiene concurrencia? si necesitas que te validen esa opinión...

Kaledros
#19Ranthas:

Me alegra ver que fuera de feda sabemos comportarnos.

¿Decías?

6 1 respuesta
Fyn4r

#29 tampoco lo pondría como ejemplo de mal comportamiento, es algo que puedes ver en stackoverflow perfectamente xd

1 1 respuesta