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>
This commit is contained in:
opyale 2021-04-10 19:54:05 +02:00
parent bd3e6ff20a
commit 8104889bf6
12 changed files with 120 additions and 28 deletions

View File

@ -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)

View File

@ -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'
}

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -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));
}
}

View File

@ -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 {

View File

@ -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);

View File

@ -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:

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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"