From f6d5377d7a3098226bd866f8a93420c8dc20fc3e Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Tue, 7 Jul 2026 09:34:11 +0200 Subject: [PATCH] Avoid redundant content reads when opening a compare editor Opening a compare editor read each side's contents up to 15 times: every viewer descriptor lookup re-ran content-type sniffing and the text heuristic against a fresh stream. The sniffing results are now memoized per element for the duration of one open, keyed by element identity and reset whenever a different input is examined. Each side is now read at most three times: once for content-type detection, once for the text heuristic, and once for the document shown in the viewer. Viewer selection is unchanged. Contributes to https://github.com/eclipse-platform/eclipse.platform/issues/2795 --- .../compare/internal/CompareUIPlugin.java | 75 +++++++++++++++++++ .../tests/CompareOpenEfficiencyTest.java | 8 +- 2 files changed, 78 insertions(+), 5 deletions(-) diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/CompareUIPlugin.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/CompareUIPlugin.java index f380ca3d85f..ebf1bf90b59 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/CompareUIPlugin.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/CompareUIPlugin.java @@ -19,6 +19,8 @@ import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -261,6 +263,25 @@ Collection getAll() { // content type private static final IContentTypeManager fgContentTypeManager= Platform.getContentTypeManager(); + /** + * Sniffing results for the input currently being examined, one entry per + * element. Used on the UI thread only; input and elements are held weakly. + */ + private static Reference fSniffInput= new WeakReference<>(null); + private static final List fSniffedElements= new ArrayList<>(3); + + private static final class SniffedElement { + final Reference element; + IContentType contentType; + boolean contentTypeSniffed; + String guessedType; + boolean guessedTypeSniffed; + + SniffedElement(ITypedElement element) { + this.element= new WeakReference<>(element); + } + } + public static final int NO_DIFFERENCE = 10000; /** @@ -1060,6 +1081,7 @@ public IStreamMerger createStreamMerger(IContentType type) { public ViewerDescriptor[] findStructureViewerDescriptor(Viewer oldViewer, ICompareInput input, CompareConfiguration configuration) { + beginContentTypeSniffing(input); // we don't show the structure of additions or deletions if ((input == null) || input == null || input.getLeft() == null || input.getRight() == null) { return null; @@ -1245,6 +1267,7 @@ private Collection getContentTypes(Object in) { } public ViewerDescriptor[] findContentViewerDescriptor(Viewer oldViewer, Object in, CompareConfiguration cc) { + beginContentTypeSniffing(in); LinkedHashSet result = new LinkedHashSet<>(); if (in instanceof IStreamContentAccessor) { String type= ITypedElement.TEXT_TYPE; @@ -1469,10 +1492,47 @@ private static String[] getTypes(ICompareInput input) { return tmp.toArray(new String[tmp.size()]); } + /** Drops the memoized results when a different input is examined. */ + private static void beginContentTypeSniffing(Object input) { + if (Display.getCurrent() == null || input == fSniffInput.get()) { + return; + } + fSniffedElements.clear(); + fSniffInput= new WeakReference<>(input); + } + + /** Results are reused on the UI thread only. */ + private static boolean isSniffing() { + return Display.getCurrent() != null && fSniffInput.get() != null; + } + + private static SniffedElement sniffedElement(ITypedElement element) { + for (SniffedElement sniffed : fSniffedElements) { + if (sniffed.element.get() == element) { + return sniffed; + } + } + SniffedElement sniffed= new SniffedElement(element); + fSniffedElements.add(sniffed); + return sniffed; + } + private static IContentType getContentType(ITypedElement element) { if (element == null) { return null; } + if (!isSniffing()) { + return computeContentType(element); + } + SniffedElement sniffed= sniffedElement(element); + if (!sniffed.contentTypeSniffed) { + sniffed.contentType= computeContentType(element); + sniffed.contentTypeSniffed= true; + } + return sniffed.contentType; + } + + private static IContentType computeContentType(ITypedElement element) { String name= element.getName(); IContentType ct= null; if (element instanceof IResourceProvider) { @@ -1603,6 +1663,21 @@ private static IContentType[] toFullPath(IContentType ct) { * Returns null if the input isn't an IStreamContentAccessor. */ private static String guessType(ITypedElement input) { + if (input == null) { + return null; + } + if (!isSniffing()) { + return computeGuessType(input); + } + SniffedElement sniffed= sniffedElement(input); + if (!sniffed.guessedTypeSniffed) { + sniffed.guessedType= computeGuessType(input); + sniffed.guessedTypeSniffed= true; + } + return sniffed.guessedType; + } + + private static String computeGuessType(ITypedElement input) { if (input instanceof IStreamContentAccessor sca) { InputStream is= null; try { diff --git a/team/tests/org.eclipse.compare.tests/src/org/eclipse/compare/tests/CompareOpenEfficiencyTest.java b/team/tests/org.eclipse.compare.tests/src/org/eclipse/compare/tests/CompareOpenEfficiencyTest.java index 4e004283670..38e15b927ea 100644 --- a/team/tests/org.eclipse.compare.tests/src/org/eclipse/compare/tests/CompareOpenEfficiencyTest.java +++ b/team/tests/org.eclipse.compare.tests/src/org/eclipse/compare/tests/CompareOpenEfficiencyTest.java @@ -54,12 +54,10 @@ public class CompareOpenEfficiencyTest { /** - * Upper bound for the {@code getContents()} calls per side during a single - * compare editor open, caused by repeated content-type sniffing and viewer - * descriptor lookups. The exact count is platform dependent (observed: 15 on - * Linux and Windows, 9 on macOS), so only the worst case is asserted. + * Upper bound for the {@code getContents()} calls per side on one compare editor + * open: content-type detection, text heuristic, and the document itself. */ - private static final int MAX_GET_CONTENTS_PER_SIDE = 15; + private static final int MAX_GET_CONTENTS_PER_SIDE = 3; private static final long TIMEOUT_MILLIS = 30_000;