Fixed: Lag caused due to a large number of diagnostic items in RecyclerView

This commit is contained in:
Akash Yadav
2022-06-26 09:27:21 +05:30
parent c086349222
commit 8260a0bff4
8 changed files with 123 additions and 32 deletions
+2
View File
@@ -26,6 +26,8 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="UnusedAttribute,MediaCapabilities">
<profileable android:shell="true" />
<activity
android:name=".CrashHandlerActivity"
android:exported="true"
@@ -241,6 +241,7 @@ public class EditorActivity extends StudioActivity
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
LOG.debug("Prepare options menu");
ensureToolbarMenu(menu);
return true;
}
@@ -33,16 +33,17 @@ import com.itsaky.lsp.models.DiagnosticSeverity;
import java.io.File;
import java.util.List;
import java.util.Objects;
public class DiagnosticItemAdapter extends RecyclerView.Adapter<DiagnosticItemAdapter.VH> {
private final List<DiagnosticItem> diags;
private final List<DiagnosticItem> diagnostics;
private final File file;
private final DiagnosticClickListener listener;
public DiagnosticItemAdapter(
List<DiagnosticItem> diags, File file, DiagnosticClickListener listener) {
this.diags = diags;
List<DiagnosticItem> 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<DiagnosticItemAd
@Override
public void onBindViewHolder(DiagnosticItemAdapter.VH p1, int p2) {
final DiagnosticItem diagnostic = diags.get(p2);
final DiagnosticItem diagnostic = diagnostics.get(p2);
final LayoutDiagnosticItemBinding binding = p1.binding;
binding.icon.setImageResource(getDiagnosticIconId(diagnostic));
@@ -81,7 +82,7 @@ public class DiagnosticItemAdapter extends RecyclerView.Adapter<DiagnosticItemAd
@Override
public int getItemCount() {
return diags.size();
return diagnostics.size();
}
private int getDiagnosticIconId(DiagnosticItem diagnostic) {
@@ -22,6 +22,7 @@ import android.graphics.PorterDuff;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -32,17 +33,19 @@ import com.itsaky.androidide.interfaces.DiagnosticClickListener;
import com.itsaky.androidide.models.DiagnosticGroup;
import java.util.List;
import java.util.Objects;
public class DiagnosticsAdapter extends RecyclerView.Adapter<DiagnosticsAdapter.VH> {
private final List<DiagnosticGroup> diags;
private final List<DiagnosticGroup> diagnostics;
private final DiagnosticClickListener listener;
public DiagnosticsAdapter(List<DiagnosticGroup> diags, DiagnosticClickListener listener) {
this.diags = diags;
public DiagnosticsAdapter(List<DiagnosticGroup> 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<DiagnosticsAdapter.
@Override
public void onBindViewHolder(DiagnosticsAdapter.VH p1, int p2) {
final DiagnosticGroup group = diags.get(p2);
final DiagnosticGroup group = diagnostics.get(p2);
final LayoutDiagnosticGroupBinding binding = p1.binding;
final int color =
@@ -77,7 +80,7 @@ public class DiagnosticsAdapter extends RecyclerView.Adapter<DiagnosticsAdapter.
@Override
public int getItemCount() {
return diags.size();
return diagnostics.size();
}
public class VH extends RecyclerView.ViewHolder {
@@ -19,9 +19,10 @@
*/
package com.itsaky.androidide.lsp;
import android.os.Looper;
import android.os.Trace;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.FileIOUtils;
@@ -57,10 +58,13 @@ import com.itsaky.toaster.Toaster;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import io.github.rosemoe.sora.lang.diagnostic.DiagnosticsContainer;
@@ -75,6 +79,9 @@ public class IDELanguageClientImpl implements ILanguageClient {
private final Map<File, List<DiagnosticItem>> 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<DiagnosticGroup> mapAsGroup(Map<File, List<DiagnosticItem>> diags) {
List<DiagnosticGroup> 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<DiagnosticGroup> mapAsGroup(Map<File, List<DiagnosticItem>> map) {
final var groups = new ArrayList<DiagnosticGroup>();
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<File, List<DiagnosticItem>> filterRelevantDiagnostics(
@NonNull final Map<File, List<DiagnosticItem>> map) {
final var result = new HashMap<File, List<DiagnosticItem>>();
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<File> findOpenFiles(final Set<File> files, final int max) {
final var openedFiles = activity().getViewModel().getOpenedFiles();
final var result = new TreeSet<File>();
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<CodeActionItem> actions) {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(activity());
builder.setTitle(R.string.msg_code_actions);
@@ -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<SignatureHelp> future =
@@ -17,6 +17,8 @@
package com.itsaky.lsp.java;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
@@ -90,7 +92,12 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
applySettings(getSettings());
}
private void logIfMainThread(String methodName) {
LOG.debug(methodName, ": Is main thread?", Looper.myLooper() == Looper.getMainLooper());
}
private void analyzeSelected() {
logIfMainThread("analyzeSelected");
if (this.selectedFile == null) {
return;
}
@@ -129,6 +136,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
@NonNull
private JavaCompilerService createCompiler() {
logIfMainThread("createCompiler");
return new JavaCompilerService(configuration.getClassPaths(), Collections.emptySet());
}
@@ -197,6 +205,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
@Override
public void configurationChanged(Object newConfiguration) {
logIfMainThread("configurationChanged");
if (!(newConfiguration instanceof JavaServerConfiguration)) {
LOG.error("Invalid configuration passed to server.", newConfiguration);
LOG.error("Configuration change event will be ignored.");
@@ -219,6 +228,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
@NonNull
@Override
public ICompletionProvider getCompletionProvider() {
logIfMainThread("completionProvider");
if (!settings.completionsEnabled()) {
return new NoCompletionsProvider();
}
@@ -260,6 +270,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
@NonNull
@Override
public SignatureHelp signatureHelp(@NonNull SignatureHelpParams params) {
logIfMainThread("signatureHelp");
if (!settings.signatureHelpEnabled()) {
return new SignatureHelp(Collections.emptyList(), -1, -1);
}
@@ -270,6 +281,7 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
@NonNull
@Override
public List<DiagnosticItem> 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 {