From 8104889bf670474dd138dbb46bee5c41b1c4e202 Mon Sep 17 00:00:00 2001
From: opyale <opyale@noreply.codeberg.org>
Date: Sat, 10 Apr 2021 19:54:05 +0200
Subject: [PATCH] Improve markdown rendering performance (#890)

Use object pooling with up to 45 threads for improved parallelization in markdown rendering.

Reviewed-on: https://codeberg.org/gitnex/GitNex/pulls/890
Reviewed-by: M M Arif <mmarif@noreply.codeberg.org>
Co-Authored-By: opyale <opyale@noreply.codeberg.org>
Co-Committed-By: opyale <opyale@noreply.codeberg.org>
---
 README.md                                     |   1 +
 app/build.gradle                              |   1 +
 .../gitnex/activities/FileViewActivity.java   |   2 +-
 .../activities/IssueDetailActivity.java       |   2 +-
 .../mian/gitnex/adapters/DraftsAdapter.java   |   2 +-
 .../gitnex/adapters/IssueCommentsAdapter.java |   4 +-
 .../gitnex/adapters/MilestonesAdapter.java    |   4 +-
 .../mian/gitnex/adapters/ReleasesAdapter.java |   2 +-
 .../gitnex/fragments/RepoInfoFragment.java    |   2 +-
 .../org/mian/gitnex/helpers/Markdown.java     | 122 +++++++++++++++---
 .../{ => views}/SyntaxHighlightedArea.java    |   4 +-
 .../main/res/layout/activity_file_view.xml    |   2 +-
 12 files changed, 120 insertions(+), 28 deletions(-)
 rename app/src/main/java/org/mian/gitnex/helpers/{ => views}/SyntaxHighlightedArea.java (98%)

diff --git a/README.md b/README.md
index f3e3c340..22aae543 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,7 @@ Thanks to all the open source libraries, contributors and donators.
 - [ge0rg/MemorizingTrustManager](https://github.com/ge0rg/MemorizingTrustManager)
 - [mikaelhg/urlbuilder](https://github.com/mikaelhg/urlbuilder)
 - [ACRA/acra](https://github.com/ACRA/acra)
+- [chrisvest/stormpot](https://github.com/chrisvest/stormpot)
 
 #### Icon sets
 - [feathericons/feather](https://github.com/feathericons/feather)
diff --git a/app/build.gradle b/app/build.gradle
index ac7dbf5d..103c84b0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -112,5 +112,6 @@ dependencies {
     implementation "org.codeberg.gitnex:tea4j:1.0.5"
     coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
     implementation 'androidx.biometric:biometric:1.1.0'
+    implementation 'com.github.chrisvest:stormpot:2.4.1'
 
 }
diff --git a/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java b/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java
index 229ab3c8..80fa8cb0 100644
--- a/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java
+++ b/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java
@@ -305,7 +305,7 @@ public class FileViewActivity extends BaseActivity implements BottomSheetFileVie
 
 			if(!tinyDB.getBoolean("enableMarkdownInFileView")) {
 
-				new Markdown(ctx, EmojiParser.parseToUnicode(binding.contents.getContent()), binding.markdown);
+				Markdown.render(ctx, EmojiParser.parseToUnicode(binding.contents.getContent()), binding.markdown);
 
 				binding.contents.setVisibility(View.GONE);
 				binding.markdownFrame.setVisibility(View.VISIBLE);
diff --git a/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java b/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java
index 6a18e9a0..3760ab8a 100644
--- a/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java
+++ b/app/src/main/java/org/mian/gitnex/activities/IssueDetailActivity.java
@@ -582,7 +582,7 @@ public class IssueDetailActivity extends BaseActivity implements LabelsListAdapt
 					viewBinding.issueTitle.setText(HtmlCompat.fromHtml(issueNumber_ + " " + EmojiParser.parseToUnicode(singleIssue.getTitle()), HtmlCompat.FROM_HTML_MODE_LEGACY));
 					String cleanIssueDescription = singleIssue.getBody().trim();
 
-					new Markdown(ctx, EmojiParser.parseToUnicode(cleanIssueDescription), viewBinding.issueDescription);
+					Markdown.render(ctx, EmojiParser.parseToUnicode(cleanIssueDescription), viewBinding.issueDescription);
 
 					RelativeLayout.LayoutParams paramsDesc = (RelativeLayout.LayoutParams) viewBinding.issueDescription.getLayoutParams();
 
diff --git a/app/src/main/java/org/mian/gitnex/adapters/DraftsAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/DraftsAdapter.java
index 5f2d04b5..a6db98d7 100644
--- a/app/src/main/java/org/mian/gitnex/adapters/DraftsAdapter.java
+++ b/app/src/main/java/org/mian/gitnex/adapters/DraftsAdapter.java
@@ -127,7 +127,7 @@ public class DraftsAdapter extends RecyclerView.Adapter<DraftsAdapter.DraftsView
 	    holder.repoInfo.setText(headTitle);
 	    holder.draftWithRepository = currentItem;
 
-	    new Markdown(mCtx, currentItem.getDraftText(), holder.draftText);
+	    Markdown.render(mCtx, currentItem.getDraftText(), holder.draftText);
 
 	    if(!currentItem.getCommentId().equalsIgnoreCase("new")) {
 		    holder.editCommentStatus.setVisibility(View.VISIBLE);
diff --git a/app/src/main/java/org/mian/gitnex/adapters/IssueCommentsAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/IssueCommentsAdapter.java
index 51c8aabe..c52111c5 100644
--- a/app/src/main/java/org/mian/gitnex/adapters/IssueCommentsAdapter.java
+++ b/app/src/main/java/org/mian/gitnex/adapters/IssueCommentsAdapter.java
@@ -332,7 +332,7 @@ public class IssueCommentsAdapter extends RecyclerView.Adapter<IssueCommentsAdap
 			.centerCrop()
 			.into(holder.avatar);
 
-		new Markdown(ctx, EmojiParser.parseToUnicode(issueComment.getBody()), holder.comment);
+		Markdown.render(ctx, EmojiParser.parseToUnicode(issueComment.getBody()), holder.comment);
 
 		StringBuilder informationBuilder = null;
 		if(issueComment.getCreated_at() != null) {
@@ -349,9 +349,7 @@ public class IssueCommentsAdapter extends RecyclerView.Adapter<IssueCommentsAdap
 			}
 
 			if(!issueComment.getCreated_at().equals(issueComment.getUpdated_at())) {
-
 				if(informationBuilder != null) {
-
 					informationBuilder.append(ctx.getString(R.string.colorfulBulletSpan)).append(ctx.getString(R.string.modifiedText));
 				}
 			}
diff --git a/app/src/main/java/org/mian/gitnex/adapters/MilestonesAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/MilestonesAdapter.java
index 48a28c44..1439c862 100644
--- a/app/src/main/java/org/mian/gitnex/adapters/MilestonesAdapter.java
+++ b/app/src/main/java/org/mian/gitnex/adapters/MilestonesAdapter.java
@@ -165,11 +165,11 @@ public class MilestonesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
 			milestoneId.setText(String.valueOf(dataModel.getId()));
 			milestoneStatus.setText(dataModel.getState());
 
-			new Markdown(context, dataModel.getTitle(), msTitle);
+			Markdown.render(context, dataModel.getTitle(), msTitle);
 
 			if(!dataModel.getDescription().equals("")) {
 
-				new Markdown(context, EmojiParser.parseToUnicode(dataModel.getDescription()), msDescription);
+				Markdown.render(context, EmojiParser.parseToUnicode(dataModel.getDescription()), msDescription);
 			}
 			else {
 
diff --git a/app/src/main/java/org/mian/gitnex/adapters/ReleasesAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/ReleasesAdapter.java
index 3dfae2ad..58287007 100644
--- a/app/src/main/java/org/mian/gitnex/adapters/ReleasesAdapter.java
+++ b/app/src/main/java/org/mian/gitnex/adapters/ReleasesAdapter.java
@@ -129,7 +129,7 @@ public class ReleasesAdapter extends RecyclerView.Adapter<ReleasesAdapter.Releas
 	    }
 
         if(!currentItem.getBody().equals("")) {
-	        new Markdown(mCtx, currentItem.getBody(), holder.releaseBodyContent);
+	        Markdown.render(mCtx, currentItem.getBody(), holder.releaseBodyContent);
         }
         else {
 	        holder.releaseBodyContent.setText(R.string.noReleaseBodyContent);
diff --git a/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java b/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java
index f7c67749..ec0274a1 100644
--- a/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java
+++ b/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java
@@ -324,7 +324,7 @@ public class RepoInfoFragment extends Fragment {
 					switch(response.code()) {
 
 						case 200:
-							new Markdown(ctx, response.body(), binding.repoFileContents);
+							Markdown.render(ctx, response.body(), binding.repoFileContents);
 							break;
 
 						case 401:
diff --git a/app/src/main/java/org/mian/gitnex/helpers/Markdown.java b/app/src/main/java/org/mian/gitnex/helpers/Markdown.java
index 974b5cc2..067e0b96 100644
--- a/app/src/main/java/org/mian/gitnex/helpers/Markdown.java
+++ b/app/src/main/java/org/mian/gitnex/helpers/Markdown.java
@@ -9,8 +9,11 @@ import androidx.core.content.res.ResourcesCompat;
 import org.mian.gitnex.R;
 import org.mian.gitnex.clients.PicassoService;
 import org.mian.gitnex.core.MainGrammarLocator;
+import java.util.Objects;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import io.noties.markwon.AbstractMarkwonPlugin;
 import io.noties.markwon.Markwon;
 import io.noties.markwon.core.CorePlugin;
@@ -26,6 +29,13 @@ import io.noties.markwon.syntax.Prism4jThemeDarkula;
 import io.noties.markwon.syntax.Prism4jThemeDefault;
 import io.noties.markwon.syntax.SyntaxHighlightPlugin;
 import io.noties.prism4j.Prism4j;
+import stormpot.Allocator;
+import stormpot.BlazePool;
+import stormpot.Config;
+import stormpot.Pool;
+import stormpot.Poolable;
+import stormpot.Slot;
+import stormpot.Timeout;
 
 /**
  * @author opyale
@@ -33,26 +43,66 @@ import io.noties.prism4j.Prism4j;
 
 public class Markdown {
 
-	private static final ExecutorService executorService = Executors.newCachedThreadPool();
+	private static final int MAX_POOL_SIZE = 45;
+	private static final int MAX_THREAD_KEEP_ALIVE_SECONDS = 120;
+	private static final int MAX_CLAIM_TIMEOUT_SECONDS = 5;
 
-	private final Context context;
-	private final String markdown;
-	private final TextView textView;
+	private static final Timeout timeout = new Timeout(MAX_CLAIM_TIMEOUT_SECONDS, TimeUnit.SECONDS);
 
-	public Markdown(@NonNull Context context, @NonNull String markdown, @NonNull TextView textView) {
+	private static final ExecutorService executorService =
+		new ThreadPoolExecutor(MAX_POOL_SIZE / 2, MAX_POOL_SIZE, MAX_THREAD_KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, new SynchronousQueue<>());
 
-		this.context = context;
-		this.markdown = markdown;
-		this.textView = textView;
+	private static final Pool<Renderer> rendererPool;
 
-		executorService.execute(new Renderer());
+	static {
+
+		Config<Renderer> config = new Config<>();
+
+		config.setBackgroundExpirationEnabled(true);
+		config.setPreciseLeakDetectionEnabled(true);
+		config.setSize(MAX_POOL_SIZE);
+		config.setAllocator(new Allocator<Renderer>() {
+
+			@Override
+			public Renderer allocate(Slot slot) throws Exception {
+				return new Renderer(slot);
+			}
+
+			@Override public void deallocate(Renderer poolable) throws Exception {}
+
+		});
+
+		rendererPool = new BlazePool<>(config);
 
 	}
 
-	private class Renderer implements Runnable {
+	public static void render(Context context, String markdown, TextView textView) {
 
-		@Override
-		public void run() {
+		try {
+			Renderer renderer = rendererPool.claim(timeout);
+
+			if(renderer != null) {
+				renderer.setParameters(context, markdown, textView);
+				executorService.execute(renderer);
+			}
+		} catch(InterruptedException ignored) {}
+	}
+
+	private static class Renderer implements Runnable, Poolable {
+
+		private final Slot slot;
+
+		private Markwon markwon;
+
+		private Context context;
+		private String markdown;
+		private TextView textView;
+
+		public Renderer(Slot slot) {
+			this.slot = slot;
+		}
+
+		private void setup() {
 
 			Prism4jTheme prism4jTheme = TinyDB.getInstance(context).getString("currentTheme").equals("dark") ?
 				Prism4jThemeDarkula.create() :
@@ -72,16 +122,56 @@ public class Markdown {
 					@Override
 					public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
 						builder.codeBlockTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf"));
+						builder.codeBlockMargin((int) (context.getResources().getDisplayMetrics().density * 10));
+						builder.blockMargin((int) (context.getResources().getDisplayMetrics().density * 10));
+						builder.codeTextSize((int) (context.getResources().getDisplayMetrics().scaledDensity * 13));
 						builder.codeTypeface(Typeface.createFromAsset(context.getAssets(), "fonts/sourcecodeproregular.ttf"));
 						builder.linkColor(ResourcesCompat.getColor(context.getResources(), R.color.lightBlue, null));
 					}
 				});
 
-			Markwon markwon = builder.build();
-			Spanned spanned = markwon.toMarkdown(markdown);
+			markwon = builder.build();
 
-			textView.post(() -> markwon.setParsedMarkdown(textView, spanned));
+		}
 
+		public void setParameters(Context context, String markdown, TextView textView) {
+
+			this.context = context;
+			this.markdown = markdown;
+			this.textView = textView;
+		}
+
+		@Override
+		public void run() {
+
+			Objects.requireNonNull(context);
+			Objects.requireNonNull(markdown);
+			Objects.requireNonNull(textView);
+
+			if(markwon == null) setup();
+
+			Spanned processedMarkdown = markwon.toMarkdown(markdown);
+
+			TextView localReference = textView;
+			localReference.post(() -> localReference.setText(processedMarkdown));
+
+			release();
+
+		}
+
+		@Override
+		public void release() {
+
+			context = null;
+			markdown = null;
+			textView = null;
+
+			slot.release(this);
+
+		}
+
+		public void expire() {
+			slot.expire(this);
 		}
 	}
 }
diff --git a/app/src/main/java/org/mian/gitnex/helpers/SyntaxHighlightedArea.java b/app/src/main/java/org/mian/gitnex/helpers/views/SyntaxHighlightedArea.java
similarity index 98%
rename from app/src/main/java/org/mian/gitnex/helpers/SyntaxHighlightedArea.java
rename to app/src/main/java/org/mian/gitnex/helpers/views/SyntaxHighlightedArea.java
index 52686cfa..f7a65575 100644
--- a/app/src/main/java/org/mian/gitnex/helpers/SyntaxHighlightedArea.java
+++ b/app/src/main/java/org/mian/gitnex/helpers/views/SyntaxHighlightedArea.java
@@ -1,4 +1,4 @@
-package org.mian.gitnex.helpers;
+package org.mian.gitnex.helpers.views;
 
 import android.content.Context;
 import android.graphics.Canvas;
@@ -16,6 +16,8 @@ import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import org.mian.gitnex.core.MainGrammarLocator;
+import org.mian.gitnex.helpers.AppUtil;
+import org.mian.gitnex.helpers.TinyDB;
 import io.noties.markwon.syntax.Prism4jSyntaxHighlight;
 import io.noties.markwon.syntax.Prism4jTheme;
 import io.noties.markwon.syntax.Prism4jThemeDarkula;
diff --git a/app/src/main/res/layout/activity_file_view.xml b/app/src/main/res/layout/activity_file_view.xml
index 4b857eb3..bc0870e8 100644
--- a/app/src/main/res/layout/activity_file_view.xml
+++ b/app/src/main/res/layout/activity_file_view.xml
@@ -99,7 +99,7 @@
 
             </LinearLayout>
 
-            <org.mian.gitnex.helpers.SyntaxHighlightedArea
+            <org.mian.gitnex.helpers.views.SyntaxHighlightedArea
                 android:id="@+id/contents"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"