Improve image caching options

This commit is contained in:
Astrash 2023-10-24 13:26:35 +11:00
parent 805f99f57a
commit e30bcbf1ba
4 changed files with 113 additions and 22 deletions

View File

@ -6,6 +6,7 @@ import java.util.function.Supplier;
import com.dfsek.terra.addons.image.config.ColorLoader; import com.dfsek.terra.addons.image.config.ColorLoader;
import com.dfsek.terra.addons.image.config.ColorLoader.ColorString; import com.dfsek.terra.addons.image.config.ColorLoader.ColorString;
import com.dfsek.terra.addons.image.config.ImageLibraryPackConfigTemplate;
import com.dfsek.terra.addons.image.config.noisesampler.ChannelNoiseSamplerTemplate; import com.dfsek.terra.addons.image.config.noisesampler.ChannelNoiseSamplerTemplate;
import com.dfsek.terra.addons.image.config.noisesampler.DistanceTransformNoiseSamplerTemplate; import com.dfsek.terra.addons.image.config.noisesampler.DistanceTransformNoiseSamplerTemplate;
import com.dfsek.terra.addons.image.config.image.ImageTemplate; import com.dfsek.terra.addons.image.config.image.ImageTemplate;
@ -52,6 +53,10 @@ public class ImageLibraryAddon implements AddonInitializer {
.getHandler(FunctionalEventHandler.class) .getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class) .register(addon, ConfigPackPreLoadEvent.class)
.priority(10) .priority(10)
.then(event -> {
ImageLibraryPackConfigTemplate config = event.loadTemplate(new ImageLibraryPackConfigTemplate());
event.getPack().getContext().put(config);
})
.then(event -> { .then(event -> {
ConfigPack pack = event.getPack(); ConfigPack pack = event.getPack();
CheckedRegistry<Supplier<ObjectTemplate<Image>>> imageRegistry = pack.getOrCreateRegistry(IMAGE_REGISTRY_KEY); CheckedRegistry<Supplier<ObjectTemplate<Image>>> imageRegistry = pack.getOrCreateRegistry(IMAGE_REGISTRY_KEY);

View File

@ -0,0 +1,41 @@
package com.dfsek.terra.addons.image.config;
import com.dfsek.tectonic.api.config.template.ConfigTemplate;
import com.dfsek.tectonic.api.config.template.annotations.Default;
import com.dfsek.tectonic.api.config.template.annotations.Description;
import com.dfsek.tectonic.api.config.template.annotations.Value;
import com.dfsek.terra.api.properties.Properties;
public class ImageLibraryPackConfigTemplate implements ConfigTemplate, Properties {
// TODO - These would be better as plugin wide config parameters in config.yml
@Value("images.cache.load-on-use")
@Description("If set to true, images will load into memory upon use rather than on pack load.")
@Default
private boolean loadOnUse = false;
@Value("images.cache.unload-on-timeout")
@Description("If set to true, images will be removed from memory if not used after a timeout, otherwise images will stay loaded in memory. " +
"Trades decreased memory consumption when not performing image reads for extra processing time required to perform cache lookups.")
@Default
private boolean unloadOnTimeout = false;
@Value("images.cache.timeout")
@Description("How many seconds to keep images loaded in the image cache for if images.cache.unload-on-timeout is enabled.")
@Default
private int cacheTimeout = 300;
public boolean loadOnUse() {
return loadOnUse;
}
public boolean unloadOnTimeout() {
return unloadOnTimeout;
}
public int getCacheTimeout() {
return cacheTimeout;
}
}

View File

@ -3,44 +3,62 @@ package com.dfsek.terra.addons.image.config.image;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit;
import com.dfsek.terra.addons.image.config.ImageLibraryPackConfigTemplate;
import com.dfsek.terra.addons.image.image.BufferedImageWrapper; import com.dfsek.terra.addons.image.image.BufferedImageWrapper;
import com.dfsek.terra.addons.image.image.Image; import com.dfsek.terra.addons.image.image.Image;
import com.dfsek.terra.addons.image.image.SuppliedImage;
import com.dfsek.terra.api.config.ConfigPack; import com.dfsek.terra.api.config.ConfigPack;
import com.dfsek.terra.api.config.Loader; import com.dfsek.terra.api.config.Loader;
import com.dfsek.terra.api.properties.Properties; import com.dfsek.terra.api.properties.Properties;
import com.dfsek.terra.api.util.generic.Lazy;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
/* /*
* Cache prevents configs from loading the same image multiple times into memory * Cache prevents configs from loading the same image multiple times into memory
*/ */
record ImageCache(ConcurrentHashMap<String, Image> map) implements Properties { record ImageCache(LoadingCache<String, Image> cache) implements Properties {
public static Image load(String path, ConfigPack pack, Loader files) throws IOException { public static Image load(String path, ConfigPack pack, Loader files) throws IOException {
ImageCache cache; ImageLibraryPackConfigTemplate config = pack.getContext().get(ImageLibraryPackConfigTemplate.class);
ImageCache images;
if(!pack.getContext().has(ImageCache.class)) { if(!pack.getContext().has(ImageCache.class)) {
cache = new ImageCache(new ConcurrentHashMap<>()); System.out.println("Initializing new image cache");
pack.getContext().put(cache); var cacheBuilder = Caffeine.newBuilder();
} else { if (config.unloadOnTimeout()) cacheBuilder.expireAfterAccess(config.getCacheTimeout(), TimeUnit.SECONDS);
cache = pack.getContext().get(ImageCache.class); images = new ImageCache(cacheBuilder.build(s -> loadImage(path, files)));
pack.getContext().put(images);
} else images = pack.getContext().get(ImageCache.class);
if (config.loadOnUse()) {
if(config.unloadOnTimeout()) { // Grab directly from cache if images are to unload on timeout
return new SuppliedImage(() -> images.cache.get(path));
} else {
// If images do not time out, image can be lazily loaded once instead of performing cache lookups for each image operation
Lazy<Image> lazyImage = Lazy.lazy(() -> images.cache.get(path));
return new SuppliedImage(lazyImage::value);
}
} }
if(cache.map.containsKey(path)) { return images.cache.get(path);
return cache.map.get(path); }
} else {
try { private static Image loadImage(String path, Loader files) throws IOException {
BufferedImageWrapper image = new BufferedImageWrapper(ImageIO.read(files.get(path))); System.out.println("Loading image: " + path);
cache.map.put(path, image); try {
return image; return new BufferedImageWrapper(ImageIO.read(files.get(path)));
} catch(IllegalArgumentException e) { } catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Unable to load image (image might be too large?)", e); throw new IllegalArgumentException("Unable to load image (image might be too large?)", e);
} catch(IOException e) { } catch(IOException e) {
if(e instanceof FileNotFoundException) { if(e instanceof FileNotFoundException) {
// Rethrow using nicer message // Rethrow using nicer message
throw new IOException("Unable to load image: No such file or directory: " + path, e); throw new IOException("Unable to load image: No such file or directory: " + path, e);
}
throw new IOException("Unable to load image", e);
} }
throw new IOException("Unable to load image", e);
} }
} }
} }

View File

@ -0,0 +1,27 @@
package com.dfsek.terra.addons.image.image;
import java.util.function.Supplier;
public class SuppliedImage implements Image {
private final Supplier<Image> imageSupplier;
public SuppliedImage(Supplier<Image> imageSupplier) {
this.imageSupplier = imageSupplier;
}
@Override
public int getRGB(int x, int y) {
return imageSupplier.get().getRGB(x, y);
}
@Override
public int getWidth() {
return imageSupplier.get().getWidth();
}
@Override
public int getHeight() {
return imageSupplier.get().getHeight();
}
}