Sunday 9 March 2014

Another go at a smoothly loading list of icons

Due to the lack of streaming support in MediaPlayer I've given up on the idea of a javafx internet radio player for the moment but I thought perhaps I could make a remote interface for the android one I have. This is something I could actually use since I don't have my speakers plugged into my workstation.

First thing as ever is that scrolly list of stations with pictures. I think I finally have a tidy solution using JavaFX implemented using basic Java classes. It only requires a little bit more code than the custom ListCell required anyway.

First, the data item. I load this using an executor asynchronously.

  class Station {
    String title;
    String thumb;
  }
Then a cache based on a LinkedHashMap - which simply requires implementing removeEldestEntry. I think an important detail I missed last time I tried this was cancelling any still-loading image here directly. Without that it can end up with a queue of images to load that will never be seen and this adds an unnecessary delay in loading those that are currently being shown.
  class ImageCache extends LinkedHashMap<String, Image> {
    final int limit;

    ImageCache(int limit) {
      this.limit = limit;
    }

    protected boolean removeEldestEntry(Map.Entry<String, Image> eldest) {
      if (size() > limit) {
        eldest.getValue().cancel();
        return true;
      }
      return false;
    }
  }
And finally the custom cell itself. It fades the image in once it's loaded although to be honest i'm not sure if that feels right on a desktop machine. After playing with it a bit it feels like it could cause eye fatigue by drawing the eye to multiple parts of the screen for much longer than necessary. But it does appear 'smoother' this way.
 // Copyright 2014 Michael Zucchi
 // This code is covered by the GNU General Public License
 // version 3, or later.
 class StationListCell extends ListCell<Station> {
    ImageCache cache = new ImageCache(32);
    ImageView iv;
    FadeTransition fadein;

    StationListCell() {
      iv = new ImageView();
      iv.setFitWidth(128);
      iv.setFitHeight(64);
      iv.setPreserveRatio(true);

      setGraphic(iv);
    }

    protected void updateItem(Station t, boolean empty) {
      super.updateItem(t, empty);

      if (t != null) {
        setText(t.title);

        if (t.thumb != null) {
            Image icon = cache.get(t.thumb);

            if (icon == null) {
              if (fadein != null) {
                fadein.stop();
                fadein = null;
              }
              icon = new Image(t.thumb, 128, 64, true, true, true);
              icon.progressProperty().addListener((ObservableValue<? extends Number> ov,
                                                  Number t1, Number t2) -> {
                if (t2.doubleValue() == 1.0) {
                  fadein = new FadeTransition(Duration.millis(250), iv);
                  fadein.setFromValue(0);
                  fadein.setToValue(1);
                  fadein.play();
                }
              });
            }
            cache.put(t.thumb, icon); // update lru
            iv.setImage(icon);
          }
        } else {
          iv.setImage(null);
        }
      }
    }
  }

I think last time I tried it I forgot to write to the cache every time an entry is accessed - i.e. to update the LRU order. It's still a bit more code than i'd really like but given that it's in one language in one place it's probably about as concise as can be expected.

Actually there is still some weird JavaFX bug in that the label text jumps to the left once the image is loaded - which makes no sense given the properties on the ImageView and the constructor arguments to the background loaded Image. But this can be fixed by placing the ImageView inside an appropriately configured Region like an AnchorView.

I also played with a busy animation while it was loading but that just looked naff.

This appears very nice and smooth as you scroll through the list. The only obvious time it drops some frames is when the list is initially populated with setItems(ObservableList) (which is still a bit unfortunate).

The Java Heap vs native heap

I was curious about whether the size-limited cache was worth putting in at all rather than simply using the mechanism to lazily load every image (or ... just use the Image background loading feature directly). So I tried some profiling The images are fixed at 128x64 pixels so they're not particularly big, and I dunno there's a few dozen stations.

Using lazy loading the JVM maxes out at 10MB of active heap. A 32-element cache required about 7MB. So that seems a sizable benefit.

However ...

The process itself requires about 150MB total (top - RES) so it's pretty insignificant in the grand scheme of things. Using just the interpreter drops this down to 100MB but that's not much use. Classes, compilers, compiled code, X, GL and other native resources really chew it up.

A typical C++ application probably needs comparable total memory to exist, sans the compiler, but a lot of that is just in shared libraries so it can ameliorated by sharing across applications. Although in reality with so many libraries needed to do anything in C or C++ and such an ill-defined "platform" as GNU/Linux there is much much less sharing going on than there could be (it's certainly no AmigaOS). One of the trade-offs of using a jit compiler and dynamic runtime is that compiled code can't really be shared unless multiple applications run in the same jvm (i've seen mention of such a feature but it isn't here yet - it's certainly technologically possible).

But yeah, memory is cheap and the main memory factor in most applications is the data itself which is much the same regardless of language (unless it doesn't support primitive-type arrays).

2 comments:

Solerman Kaplon said...

There is a mechanism in desktop oracle client vm that you can take a memory snapshot of the jvm classes and share it across apps using a memory mapped file. I think one can even drop stuff into the lib/ext and it will share it also. Doesn't help android or sharing across non-jvm stuff.

NotZed said...

Yeah, on the embedded jvm you can sort of 'pre-compile' most of the jdk.

Still, it's all stuff that is in conflict with the way hotspot works since the whole point is not precompiling but compiling to suit the immediate problem at hand. It does save memory/compilation time but then you're back to pre-compiled stuff which is one thing a dynamic jvm does better.

With memory being so bountiful now it's becoming a bit academic anyway although still interesting if you're into that kinda shit like I am.