From 8260a0bff4c4f578cc747e94593ecb868ef819db Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sun, 26 Jun 2022 09:27:21 +0530 Subject: [PATCH] Fixed: Lag caused due to a large number of diagnostic items in RecyclerView --- app/src/main/AndroidManifest.xml | 2 + .../com/itsaky/androidide/EditorActivity.java | 1 + .../adapters/DiagnosticItemAdapter.java | 11 ++- .../adapters/DiagnosticsAdapter.java | 13 ++- .../androidide/lsp/IDELanguageClientImpl.java | 90 ++++++++++++++++-- .../androidide/views/editor/IDEEditor.java | 22 ++--- .../test/test-home/.androidide/init/model.jar | Bin 55232 -> 55232 bytes .../itsaky/lsp/java/JavaLanguageServer.java | 16 ++++ 8 files changed, 123 insertions(+), 32 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a42b8a619..880d10f33 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,6 +26,8 @@ android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="UnusedAttribute,MediaCapabilities"> + + { - private final List diags; + private final List diagnostics; private final File file; private final DiagnosticClickListener listener; public DiagnosticItemAdapter( - List diags, File file, DiagnosticClickListener listener) { - this.diags = diags; + List diagnostics, File file, DiagnosticClickListener listener) { + this.diagnostics = Objects.requireNonNull(diagnostics); this.file = file; this.listener = listener; } @@ -56,7 +57,7 @@ public class DiagnosticItemAdapter extends RecyclerView.Adapter { - private final List diags; + private final List diagnostics; private final DiagnosticClickListener listener; - public DiagnosticsAdapter(List diags, DiagnosticClickListener listener) { - this.diags = diags; + public DiagnosticsAdapter(List diagnostics, DiagnosticClickListener listener) { + this.diagnostics = Objects.requireNonNull(diagnostics); this.listener = listener; } + @NonNull @Override public DiagnosticsAdapter.VH onCreateViewHolder(ViewGroup p1, int p2) { return new VH( @@ -51,7 +54,7 @@ public class DiagnosticsAdapter extends RecyclerView.Adapter> diagnostics = new HashMap<>(); protected EditorActivityProvider activityProvider; + public static final int MAX_DIAGNOSTIC_FILES = 10; + public static final int MAX_DIAGNOSTIC_ITEMS_PER_FILE = 20; + private IDELanguageClientImpl(EditorActivityProvider provider) { setActivityProvider(provider); } @@ -153,8 +160,7 @@ public class IDELanguageClientImpl implements ILanguageClient { File file = result.getFile().toFile(); if (!file.exists() || !file.isFile()) return; - LOG.debug("Updating diagnostics. Is main thread?", Looper.getMainLooper() == Looper.myLooper()); - + Trace.beginSection("publishDiagnostics"); final var editorView = activity().getEditorForFile(file); if (editorView != null) { final var editor = editorView.getEditor(); @@ -174,6 +180,7 @@ public class IDELanguageClientImpl implements ILanguageClient { diagnostics.put(file, result.getDiagnostics()); activity().setDiagnosticsAdapter(newDiagnosticsAdapter()); + Trace.endSection(); } @Nullable @@ -284,18 +291,81 @@ public class IDELanguageClientImpl implements ILanguageClient { }); } - private List mapAsGroup(Map> diags) { - List groups = new ArrayList<>(); - if (diags == null || diags.size() <= 0) return groups; - for (File file : diags.keySet()) { - var fileDiags = diags.get(file); - if (fileDiags == null || fileDiags.size() <= 0) continue; - DiagnosticGroup group = new DiagnosticGroup(R.drawable.ic_language_java, file, fileDiags); + private List mapAsGroup(Map> map) { + final var groups = new ArrayList(); + var diagnosticMap = map; + if (diagnosticMap == null || diagnosticMap.size() <= 0) return groups; + + if (diagnosticMap.size() > 10) { + LOG.warn("Limiting the diagnostics to 10 files"); + diagnosticMap = filterRelevantDiagnostics(map); + } + + for (File file : diagnosticMap.keySet()) { + var fileDiagnostics = diagnosticMap.get(file); + if (fileDiagnostics == null || fileDiagnostics.size() <= 0) continue; + + // Trim the diagnostics list if we have too many diagnostic items. + // Including a lot of diagnostic items will result in UI lag when they are shown + if (fileDiagnostics.size() > MAX_DIAGNOSTIC_ITEMS_PER_FILE) { + LOG.warn( + "Limiting diagnostics to", + MAX_DIAGNOSTIC_ITEMS_PER_FILE, + "items for file", + file.getName()); + + fileDiagnostics = fileDiagnostics.subList(0, MAX_DIAGNOSTIC_ITEMS_PER_FILE); + } + DiagnosticGroup group = + new DiagnosticGroup(R.drawable.ic_language_java, file, fileDiagnostics); groups.add(group); } return groups; } + @NonNull + private Map> filterRelevantDiagnostics( + @NonNull final Map> map) { + final var result = new HashMap>(); + final var files = map.keySet(); + + // Diagnostics of files that are open must always be included + final var relevantFiles = findOpenFiles(files, MAX_DIAGNOSTIC_FILES); + + // If we can show a few more file diagnostics... + if (relevantFiles.size() < MAX_DIAGNOSTIC_FILES) { + final var alphabetical = new TreeSet<>(Comparator.comparing(File::getName)); + alphabetical.addAll(files); + for (var file : alphabetical) { + relevantFiles.add(file); + if (relevantFiles.size() == MAX_DIAGNOSTIC_FILES) { + break; + } + } + } + + for (var file : relevantFiles) { + result.put(file, map.get(file)); + } + return result; + } + + @NonNull + private Set findOpenFiles(final Set files, final int max) { + final var openedFiles = activity().getViewModel().getOpenedFiles(); + final var result = new TreeSet(); + for (int i = 0; i < openedFiles.size(); i++) { + final var opened = openedFiles.get(i); + if (files.contains(opened)) { + result.add(opened); + } + if (result.size() == max) { + break; + } + } + return result; + } + private void showAvailableQuickfixes(IDEEditor editor, List actions) { final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(activity()); builder.setTitle(R.string.msg_code_actions); diff --git a/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java b/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java index b1b3a3dd6..cb28b7e49 100644 --- a/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java +++ b/app/src/main/java/com/itsaky/androidide/views/editor/IDEEditor.java @@ -16,7 +16,6 @@ */ package com.itsaky.androidide.views.editor; -import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; @@ -128,21 +127,21 @@ public class IDEEditor extends CodeEditor { * @param event The content change event. */ private void handleContentChange(ContentChangeEvent event, Unsubscribe unsubscribe) { - if (getContext() instanceof Activity) { - ((Activity) getContext()).invalidateOptionsMenu(); - } - if (getFile() == null || mLanguageServer == null) { return; } - final var documentHandler = mLanguageServer.getDocumentHandler(); - final var file = getFile().toPath(); - if (documentHandler.accepts(file)) { - documentHandler.onContentChange(new DocumentChangeEvent(file, getText(), mFileVersion + 1)); - } + CompletableFuture.runAsync( + () -> { + final var documentHandler = mLanguageServer.getDocumentHandler(); + final var file = getFile().toPath(); + if (documentHandler.accepts(file)) { + documentHandler.onContentChange( + new DocumentChangeEvent(file, getText(), mFileVersion + 1)); + } - checkForSignatureHelp(event); + checkForSignatureHelp(event); + }); } public static int createInputFlags() { @@ -251,7 +250,6 @@ public class IDEEditor extends CodeEditor { * If any language server is set, requests signature help at the cursor's position. On a valid * response, shows the signature help in a popup window. */ - @SuppressWarnings("unused") // accessed using reflection in CompletionItem class public void signatureHelp() { if (mLanguageServer != null && getFile() != null) { final CompletableFuture future = diff --git a/build-tools/tooling-api-impl/src/test/test-home/.androidide/init/model.jar b/build-tools/tooling-api-impl/src/test/test-home/.androidide/init/model.jar index c1155be0ddb400ed4c8d41b2c1d79fa01f21ad9a..84d4191953c757f26e1b34e63ec1868a05f05d26 100644 GIT binary patch delta 33 ocmX@Gp83FfX5IjAW)?061`ZB}G_PA5dDov}2GX1NpJL<%0I)a analyze(@NonNull Path file) { + logIfMainThread("analyze file:" + file); if (!settings.codeAnalysisEnabled()) { return Collections.emptyList(); } @@ -301,6 +313,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler { @Override public void onFileOpened(DocumentOpenEvent event) { + logIfMainThread("onFileOpened"); onFileSelected(event.getOpenedFile()); ensureAnalyzeTimerStarted(); FileStore.open(event); @@ -308,6 +321,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler { @Override public void onContentChange(@NonNull DocumentChangeEvent event) { + logIfMainThread("onContentChanged"); // If a file's content is changed, it is definitely visible to user. onFileSelected(event.getChangedFile()); ensureAnalyzeTimerStarted(); @@ -321,6 +335,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler { @Override public void onFileClosed(DocumentCloseEvent event) { + logIfMainThread("onFileClosed"); FileStore.close(event); } @@ -330,6 +345,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler { } private void ensureAnalyzeTimerStarted() { + logIfMainThread("ensureAnalyzeTimerStarted"); if (!this.analyzeTimer.isStarted()) { this.analyzeTimer.start(); } else {