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.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.DistanceTransformNoiseSamplerTemplate;
import com.dfsek.terra.addons.image.config.image.ImageTemplate;
@ -52,6 +53,10 @@ public class ImageLibraryAddon implements AddonInitializer {
.getHandler(FunctionalEventHandler.class)
.register(addon, ConfigPackPreLoadEvent.class)
.priority(10)
.then(event -> {
ImageLibraryPackConfigTemplate config = event.loadTemplate(new ImageLibraryPackConfigTemplate());
event.getPack().getContext().put(config);
})
.then(event -> {
ConfigPack pack = event.getPack();
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 java.io.FileNotFoundException;
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.Image;
import com.dfsek.terra.addons.image.image.SuppliedImage;
import com.dfsek.terra.api.config.ConfigPack;
import com.dfsek.terra.api.config.Loader;
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
*/
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 {
ImageCache cache;
ImageLibraryPackConfigTemplate config = pack.getContext().get(ImageLibraryPackConfigTemplate.class);
ImageCache images;
if(!pack.getContext().has(ImageCache.class)) {
cache = new ImageCache(new ConcurrentHashMap<>());
pack.getContext().put(cache);
} else {
cache = pack.getContext().get(ImageCache.class);
System.out.println("Initializing new image cache");
var cacheBuilder = Caffeine.newBuilder();
if (config.unloadOnTimeout()) cacheBuilder.expireAfterAccess(config.getCacheTimeout(), TimeUnit.SECONDS);
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 cache.map.get(path);
} else {
try {
BufferedImageWrapper image = new BufferedImageWrapper(ImageIO.read(files.get(path)));
cache.map.put(path, image);
return image;
} catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Unable to load image (image might be too large?)", e);
} catch(IOException e) {
if(e instanceof FileNotFoundException) {
// 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", e);
return images.cache.get(path);
}
private static Image loadImage(String path, Loader files) throws IOException {
System.out.println("Loading image: " + path);
try {
return new BufferedImageWrapper(ImageIO.read(files.get(path)));
} catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Unable to load image (image might be too large?)", e);
} catch(IOException e) {
if(e instanceof FileNotFoundException) {
// 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", 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();
}
}