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"