diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathDrawableCache.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathDrawableCache.java new file mode 100644 index 00000000..c3cbd33e --- /dev/null +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathDrawableCache.java @@ -0,0 +1,143 @@ +package io.noties.markwon.ext.latex; + +import android.graphics.drawable.Drawable; +import android.util.LruCache; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import ru.noties.jlatexmath.JLatexMathDrawable; + +/** + * Cache for JLatexMathDrawable instances to improve performance + * by avoiding redundant rendering of the same LaTeX formulas. + * + */ +public class JLatexMathDrawableCache { + + private static final int DEFAULT_CACHE_SIZE = 32; + + // Singleton instance + private static volatile JLatexMathDrawableCache instance; + + // LRU cache to store rendered LaTeX drawables + private final LruCache cache; + + // Whether to enable the cache + private final boolean enabled; + + private JLatexMathDrawableCache(int maxSize, boolean enabled) { + this.cache = new LruCache<>(maxSize); + this.enabled = enabled; + } + + /** + * Get the singleton instance of the cache + */ + @NonNull + public static JLatexMathDrawableCache getInstance() { + if (instance == null) { + synchronized (JLatexMathDrawableCache.class) { + if (instance == null) { + instance = new JLatexMathDrawableCache(DEFAULT_CACHE_SIZE, true); + } + } + } + return instance; + } + + /** + * Create a new instance with custom configuration + * + * @param maxSize maximum number of entries in the cache + * @param enabled whether the cache is enabled + * @return a new cache instance + */ + @NonNull + public static JLatexMathDrawableCache create(int maxSize, boolean enabled) { + return new JLatexMathDrawableCache(maxSize, enabled); + } + + /** + * Get a drawable from the cache + * + * @param key the LaTeX formula string + * @return the cached drawable or null if not found + */ + /** + * Cache entry containing both the drawable and its configuration + */ + private static class CacheEntry { + final Drawable drawable; + final JLatexMathDrawable.Builder builder; + + CacheEntry(Drawable drawable, JLatexMathDrawable.Builder builder) { + this.drawable = drawable; + this.builder = builder; + } + } + + @Nullable + public Drawable get(@NonNull String key) { + if (!enabled) { + return null; + } + + synchronized (cache) { + final CacheEntry entry = (CacheEntry) cache.get(key); + if (entry != null) { + if (entry.drawable instanceof JLatexMathDrawable && entry.builder != null) { + // Recreate the drawable with the same configuration + return entry.builder.build(); + } + return entry.drawable; + } + return null; + } + } + + /** + * Store a drawable in the cache + * + * @param key the LaTeX formula string + * @param drawable the drawable to cache + */ + public void put(@NonNull String key, @NonNull Drawable drawable) { + put(key, drawable, null); + } + + /** + * Store a drawable in the cache with its builder for recreation + * + * @param key the LaTeX formula string + * @param drawable the drawable to cache + * @param builder the builder used to create the drawable + */ + public void put(@NonNull String key, @NonNull Drawable drawable, @Nullable JLatexMathDrawable.Builder builder) { + if (!enabled) { + return; + } + + synchronized (cache) { + cache.put(key, new CacheEntry(drawable, builder)); + } + } + + /** + * Clear the cache + */ + public void clear() { + synchronized (cache) { + cache.evictAll(); + } + } + + /** + * Get the current size of the cache + */ + public int size() { + synchronized (cache) { + return cache.size(); + } + } +} diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java index dd8a606e..dcd5d518 100644 --- a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -359,16 +359,51 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { private final Config config; private final Handler handler = new Handler(Looper.getMainLooper()); private final Map> cache = new HashMap<>(3); - + private final JLatexMathDrawableCache drawableCache; JLatextAsyncDrawableLoader(@NonNull Config config) { this.config = config; + this.drawableCache = config.cacheEnabled + ? JLatexMathDrawableCache.create(config.cacheSize, true) + : JLatexMathDrawableCache.create(0, false); } @Override public void load(@NonNull final AsyncDrawable drawable) { - // this method must be called from main-thread only (thus synchronization can be skipped) + final String latex = drawable.getDestination(); + + // Check the cache first + final Drawable cachedDrawable = drawableCache.get(latex); + if (cachedDrawable != null) { + // Use cached drawable immediately + drawable.setResult(cachedDrawable); + return; + } + + // If synchronous rendering is enabled, render immediately on the main thread + if (config.syncRendering) { + try { + final JLatextAsyncDrawable jLatextAsyncDrawable = (JLatextAsyncDrawable) drawable; + final JLatexMathDrawable result; + + if (jLatextAsyncDrawable.isBlock()) { + result = createBlockDrawable(jLatextAsyncDrawable); + } else { + result = createInlineDrawable(jLatextAsyncDrawable); + } + + // Cache the result + drawableCache.put(latex, result); + + // Set the result immediately + drawable.setResult(result); + } catch (Throwable t) { + handleRenderingError(drawable, t); + } + return; + } + // check for currently running tasks associated with provided drawable final Future future = cache.get(drawable); @@ -376,7 +411,6 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { // as asyncDrawable is immutable, it won't have destination changed (so there is no need // to cancel any started tasks) if (future == null) { - cache.put(drawable, config.executorService.submit(new Runnable() { @Override public void run() { @@ -384,32 +418,12 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { try { execute(); } catch (Throwable t) { - // @since 4.3.0 add error handling - final ErrorHandler errorHandler = config.errorHandler; - if (errorHandler == null) { - // as before - Log.e( - "JLatexMathPlugin", - "Error displaying latex: `" + drawable.getDestination() + "`", - t); - } else { - // just call `getDestination` without casts and checks - final Drawable errorDrawable = errorHandler.handleError( - drawable.getDestination(), - t - ); - if (errorDrawable != null) { - DrawableUtils.applyIntrinsicBoundsIfEmpty(errorDrawable); - setResult(drawable, errorDrawable); - } - } + handleRenderingError(drawable, t); } } private void execute() { - final JLatexMathDrawable jLatexMathDrawable; - final JLatextAsyncDrawable jLatextAsyncDrawable = (JLatextAsyncDrawable) drawable; if (jLatextAsyncDrawable.isBlock()) { @@ -418,12 +432,38 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { jLatexMathDrawable = createInlineDrawable(jLatextAsyncDrawable); } + // Cache the result + drawableCache.put(latex, jLatexMathDrawable); + setResult(drawable, jLatexMathDrawable); } })); } } + private void handleRenderingError(@NonNull AsyncDrawable drawable, @NonNull Throwable t) { + // @since 4.3.0 add error handling + final ErrorHandler errorHandler = config.errorHandler; + if (errorHandler == null) { + // as before + Log.e( + "JLatexMathPlugin", + "Error displaying latex: `" + drawable.getDestination() + "`", + t); + } else { + // just call `getDestination` without casts and checks + final Drawable errorDrawable = errorHandler.handleError( + drawable.getDestination(), + t + ); + if (errorDrawable != null) { + DrawableUtils.applyIntrinsicBoundsIfEmpty(errorDrawable); + setResult(drawable, errorDrawable); + } + } + } + + @Override public void cancel(@NonNull AsyncDrawable drawable) {