diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java index 06d16a0519a..e9ecc883494 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java @@ -893,7 +893,11 @@ private IDocument createDocument() { try { String encoding = internalGetEncoding(); - s = Utilities.readString(sca, encoding); + // The background job may have read the contents already; decoding is + // all that is left then. + byte[] prefetched = CompareUIPlugin.takePrefetchedContents(fElement); + s = prefetched != null ? Utilities.readString(prefetched, encoding) + : Utilities.readString(sca, encoding); } catch (CoreException ex) { this.fViewer.setError(fLeg, ex.getMessage()); } 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 52ef72f1ba1..ec471fe561b 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 @@ -17,6 +17,7 @@ package org.eclipse.compare.internal; import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.Reference; @@ -32,6 +33,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; +import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -77,6 +79,7 @@ import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.content.IContentDescription; import org.eclipse.core.runtime.content.IContentType; import org.eclipse.core.runtime.content.IContentTypeManager; @@ -285,6 +288,15 @@ private static final class SniffedElement { } } + /** + * Contents read ahead while preparing an input, keyed by element identity. They + * are written in the background job and read on the UI thread, and an entry is + * dropped as soon as it became a document, so at most one input's contents are + * held at a time. + */ + private static final Map fPrefetchedContents= Collections + .synchronizedMap(new IdentityHashMap<>()); + public static final int NO_DIFFERENCE = 10000; /** @@ -900,6 +912,9 @@ public IStatus prepareInput(CompareEditorInput input, IProgressMonitor monitor) if (input.getCompareResult() == null) { return new Status(IStatus.ERROR, CompareUIPlugin.PLUGIN_ID, NO_DIFFERENCE, Utilities.getString("CompareUIPlugin.noDifferences"), null); //$NON-NLS-1$ } + // Still off the UI thread here, so this is the place to read the contents + // that the viewer would otherwise read while the user waits. + prefetchContents(input.getCompareResult(), monitor); return Status.OK_STATUS; } catch (InterruptedException e) { throw new OperationCanceledException(); @@ -1565,6 +1580,60 @@ private static IContentType getContentType(ITypedElement element) { return sniffed.contentType; } + /** + * Reads the contents of the given input's elements once, so that the sniffing + * and the merge viewer do not have to open the streams again on the UI thread. + * Elements that come with a shared document are left alone: those are served + * from the file buffer and never read a stream. + */ + public static void prefetchContents(Object compareResult, IProgressMonitor monitor) { + if (!(compareResult instanceof ICompareInput input)) { + return; + } + fPrefetchedContents.clear(); + SubMonitor progress= SubMonitor.convert(monitor, 3); + prefetchContents(input.getAncestor(), progress.split(1)); + prefetchContents(input.getLeft(), progress.split(1)); + prefetchContents(input.getRight(), progress.split(1)); + } + + private static void prefetchContents(ITypedElement element, IProgressMonitor monitor) { + if (!(element instanceof IStreamContentAccessor accessor) || hasSharedDocument(element)) { + return; + } + try (InputStream stream= accessor.getContents()) { + if (stream != null) { + fPrefetchedContents.put(element, stream.readAllBytes()); + } + } catch (CoreException | IOException | OutOfMemoryError e) { + // not fatal: whoever needs the content reads the stream itself + } finally { + monitor.done(); + } + } + + private static boolean hasSharedDocument(ITypedElement element) { + ISharedDocumentAdapter adapter= SharedDocumentAdapterWrapper.getAdapter(element); + return adapter != null && adapter.getDocumentKey(element) != null; + } + + /** + * Returns the prefetched contents of the given element and forgets them, or + * null if nothing was read ahead for it. + */ + public static byte[] takePrefetchedContents(Object element) { + return fPrefetchedContents.remove(element); + } + + /** The contents of the given element, read ahead if that already happened. */ + private static InputStream contentsOf(ITypedElement element) throws CoreException { + byte[] prefetched= fPrefetchedContents.get(element); + if (prefetched != null) { + return new ByteArrayInputStream(prefetched); + } + return element instanceof IStreamContentAccessor accessor ? accessor.getContents() : null; + } + private static IContentType computeContentType(ITypedElement element) { String name= element.getName(); IContentType ct= null; @@ -1579,9 +1648,9 @@ private static IContentType computeContentType(ITypedElement element) { } } } - if (element instanceof IStreamContentAccessor isa) { + if (element instanceof IStreamContentAccessor) { try { - InputStream is= isa.getContents(); + InputStream is= contentsOf(element); if (is != null) { try (InputStream bis = new BufferedInputStream(is)) { ct= fgContentTypeManager.findContentTypeFor(is, name); @@ -1711,10 +1780,10 @@ private static String guessType(ITypedElement input) { } private static String computeGuessType(ITypedElement input) { - if (input instanceof IStreamContentAccessor sca) { + if (input instanceof IStreamContentAccessor) { InputStream is= null; try { - is= sca.getContents(); + is= contentsOf(input); if (is == null) { return null; } diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/Utilities.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/Utilities.java index ba1dc5c5897..a8c82e64411 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/Utilities.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/internal/Utilities.java @@ -19,6 +19,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; @@ -582,6 +583,18 @@ private static IStatus addStatus(IStatus status, IStatus entry) { // encoding + /** + * Decodes already read contents, with the same encoding fallback as + * {@link #readString(IStreamContentAccessor, String)}. + */ + public static String readString(byte[] contents, String encoding) { + try { + return new String(contents, encoding); + } catch (UnsupportedEncodingException e) { + return new String(contents, Charset.defaultCharset()); + } + } + public static String readString(IStreamContentAccessor sca, String encoding) throws CoreException { String s = 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 c3fa70ae74a..4c7bccf752f 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 @@ -55,9 +55,10 @@ public class CompareOpenEfficiencyTest { /** * Upper bound for the {@code getContents()} calls per side on one compare editor - * open: content-type detection, text heuristic, and the document itself. + * open: the background job reads the contents once and everything else is served + * from it. */ - private static final int MAX_GET_CONTENTS_PER_SIDE = 3; + private static final int MAX_GET_CONTENTS_PER_SIDE = 1; private static final long TIMEOUT_MILLIS = 30_000;