Skip to content

Commit be726af

Browse files
committed
Add efficiency regression test for the compare editor open path
CompareOpenEfficiencyTest opens a compare editor end to end and pins three observable contracts with deterministic assertions: how often the element contents are read during a single open (currently 15 times per side), that prepareInput runs exactly once per open, and that it does not run on the UI thread for inputs that can run as a job. With the unified diff preference enabled the test documents that prepareInput currently does run on the UI thread. This records the baseline for the open path performance work. Contributes to #2795
1 parent 01d02ab commit be726af

2 files changed

Lines changed: 305 additions & 0 deletions

File tree

team/tests/org.eclipse.compare.tests/src/org/eclipse/compare/tests/AllCompareTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
PatchUITest.class,
3838
RangeDifferencerThreeWayDiffTest.class,
3939
CompareUIPluginTest.class,
40+
CompareOpenEfficiencyTest.class,
4041
StructureCreatorTest.class,
4142
CompareFileRevisionEditorInputTest.class})
4243
public class AllCompareTests {
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Lars Vogel and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Eclipse contributors - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.compare.tests;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import static org.junit.jupiter.api.Assertions.assertNotNull;
18+
import static org.junit.jupiter.api.Assertions.assertTrue;
19+
import static org.junit.jupiter.api.Assertions.fail;
20+
21+
import java.io.ByteArrayInputStream;
22+
import java.io.InputStream;
23+
import java.nio.charset.StandardCharsets;
24+
import java.util.concurrent.atomic.AtomicInteger;
25+
import java.util.function.BooleanSupplier;
26+
27+
import org.eclipse.compare.CompareConfiguration;
28+
import org.eclipse.compare.CompareEditorInput;
29+
import org.eclipse.compare.CompareUI;
30+
import org.eclipse.compare.CompareViewerSwitchingPane;
31+
import org.eclipse.compare.IEncodedStreamContentAccessor;
32+
import org.eclipse.compare.ITypedElement;
33+
import org.eclipse.compare.internal.ComparePreferencePage;
34+
import org.eclipse.compare.internal.CompareUIPlugin;
35+
import org.eclipse.compare.internal.IMergeViewerTestAdapter;
36+
import org.eclipse.compare.structuremergeviewer.DiffNode;
37+
import org.eclipse.core.runtime.Adapters;
38+
import org.eclipse.core.runtime.IProgressMonitor;
39+
import org.eclipse.jface.preference.IPreferenceStore;
40+
import org.eclipse.jface.viewers.Viewer;
41+
import org.eclipse.swt.graphics.Image;
42+
import org.eclipse.swt.widgets.Display;
43+
import org.eclipse.ui.IWorkbenchPage;
44+
import org.eclipse.ui.PlatformUI;
45+
import org.junit.jupiter.api.AfterEach;
46+
import org.junit.jupiter.api.BeforeEach;
47+
import org.junit.jupiter.api.Test;
48+
49+
/**
50+
* Tests how often the compare editor open path re-reads element content and
51+
* runs {@code prepareInput}, and on which thread. The asserted bounds are the
52+
* current baseline and tighten as the open path is optimized.
53+
*/
54+
public class CompareOpenEfficiencyTest {
55+
56+
/**
57+
* Upper bound for the {@code getContents()} calls per side during a single
58+
* compare editor open, caused by repeated content-type sniffing and viewer
59+
* descriptor lookups. The exact count is platform dependent (observed: 15 on
60+
* Linux and Windows, 9 on macOS), so only the worst case is asserted.
61+
*/
62+
private static final int MAX_GET_CONTENTS_PER_SIDE = 15;
63+
64+
private static final long TIMEOUT_MILLIS = 30_000;
65+
66+
private boolean originalUnifiedDiff;
67+
68+
/** A text element that counts every {@link #getContents()} call. */
69+
private static final class CountingElement
70+
implements ITypedElement, IEncodedStreamContentAccessor {
71+
72+
private final String name;
73+
private final byte[] bytes;
74+
private final AtomicInteger contentReads = new AtomicInteger();
75+
76+
CountingElement(String name, String content) {
77+
this.name = name;
78+
this.bytes = content.getBytes(StandardCharsets.UTF_8);
79+
}
80+
81+
@Override
82+
public InputStream getContents() {
83+
contentReads.incrementAndGet();
84+
return new ByteArrayInputStream(bytes);
85+
}
86+
87+
@Override
88+
public String getCharset() {
89+
return "UTF-8"; //$NON-NLS-1$
90+
}
91+
92+
@Override
93+
public String getName() {
94+
return name;
95+
}
96+
97+
@Override
98+
public String getType() {
99+
return TEXT_TYPE;
100+
}
101+
102+
@Override
103+
public Image getImage() {
104+
return null;
105+
}
106+
107+
int reads() {
108+
return contentReads.get();
109+
}
110+
}
111+
112+
/**
113+
* A compare input that counts {@code prepareInput} invocations and records
114+
* whether they ran on the UI thread.
115+
*/
116+
private static final class CountingCompareEditorInput extends CompareEditorInput {
117+
118+
private final ITypedElement left;
119+
private final ITypedElement right;
120+
private final boolean runAsJob;
121+
private final AtomicInteger prepareInputCount = new AtomicInteger();
122+
private volatile boolean anyRunOnUiThread;
123+
124+
CountingCompareEditorInput(boolean runAsJob, ITypedElement left, ITypedElement right) {
125+
super(new CompareConfiguration());
126+
this.runAsJob = runAsJob;
127+
this.left = left;
128+
this.right = right;
129+
setTitle("Counting compare"); //$NON-NLS-1$
130+
}
131+
132+
@Override
133+
protected Object prepareInput(IProgressMonitor monitor) {
134+
prepareInputCount.incrementAndGet();
135+
if (Display.getCurrent() != null) {
136+
anyRunOnUiThread = true;
137+
}
138+
return new DiffNode(left, right);
139+
}
140+
141+
@Override
142+
public boolean canRunAsJob() {
143+
return runAsJob;
144+
}
145+
146+
int prepareInputCalls() {
147+
return prepareInputCount.get();
148+
}
149+
150+
boolean ranOnUiThread() {
151+
return anyRunOnUiThread;
152+
}
153+
}
154+
155+
@BeforeEach
156+
public void setUp() {
157+
assertNotNull(Display.getCurrent(), "tests require a UI thread / Display"); //$NON-NLS-1$
158+
originalUnifiedDiff = store().getBoolean(ComparePreferencePage.UNIFIED_DIFF);
159+
}
160+
161+
@AfterEach
162+
public void tearDown() {
163+
store().setValue(ComparePreferencePage.UNIFIED_DIFF, originalUnifiedDiff);
164+
IWorkbenchPage page = activePage();
165+
if (page != null) {
166+
page.closeAllEditors(false);
167+
}
168+
processQueuedEvents();
169+
}
170+
171+
@Test
172+
public void testGetContentsCallCountPerSide() throws Exception {
173+
store().setValue(ComparePreferencePage.UNIFIED_DIFF, false);
174+
CountingElement leftElement = new CountingElement("left.txt", //$NON-NLS-1$
175+
"alpha\nbravo\ncharlie\ndelta\n"); //$NON-NLS-1$
176+
CountingElement rightElement = new CountingElement("right.txt", //$NON-NLS-1$
177+
"alpha\nBRAVO\ncharlie\nDELTA\n"); //$NON-NLS-1$
178+
CountingCompareEditorInput input = new CountingCompareEditorInput(true, leftElement, rightElement);
179+
180+
CompareUI.openCompareEditor(input);
181+
pumpUntil(() -> {
182+
IMergeViewerTestAdapter adapter = mergeViewerAdapter(input);
183+
return adapter != null && adapter.getChangesCount() > 0;
184+
}, "merge viewer did not report a diff"); //$NON-NLS-1$
185+
186+
System.out.println("PERF-EFFICIENCY getContents left=" + leftElement.reads() //$NON-NLS-1$
187+
+ " right=" + rightElement.reads()); //$NON-NLS-1$
188+
189+
assertReadsWithinBound("left", leftElement.reads()); //$NON-NLS-1$
190+
assertReadsWithinBound("right", rightElement.reads()); //$NON-NLS-1$
191+
}
192+
193+
private static void assertReadsWithinBound(String side, int reads) {
194+
assertTrue(reads >= 1, "expected at least one getContents() call on the " + side + " side"); //$NON-NLS-1$ //$NON-NLS-2$
195+
assertTrue(reads <= MAX_GET_CONTENTS_PER_SIDE,
196+
"getContents() called " + reads + " times on the " + side //$NON-NLS-1$ //$NON-NLS-2$
197+
+ " side, exceeding the known worst case of " + MAX_GET_CONTENTS_PER_SIDE); //$NON-NLS-1$
198+
}
199+
200+
@Test
201+
public void testPrepareInputRunsOnceUnifiedOff() throws Exception {
202+
store().setValue(ComparePreferencePage.UNIFIED_DIFF, false);
203+
CountingCompareEditorInput input = openAndWait(true);
204+
assertEquals(1, input.prepareInputCalls(),
205+
"prepareInput must run exactly once per open (unified diff off)"); //$NON-NLS-1$
206+
}
207+
208+
@Test
209+
public void testPrepareInputRunsOnceUnifiedOn() throws Exception {
210+
store().setValue(ComparePreferencePage.UNIFIED_DIFF, true);
211+
CountingCompareEditorInput input = openAndWait(true);
212+
// The unified diff path runs the input once in canShowInUnifiedDiff and the
213+
// classic fallback reuses the cached result, so the total stays at one.
214+
assertEquals(1, input.prepareInputCalls(),
215+
"prepareInput invocation count with unified diff on (documents current behavior)"); //$NON-NLS-1$
216+
}
217+
218+
@Test
219+
public void testPrepareInputOffUiThreadUnifiedOff() throws Exception {
220+
store().setValue(ComparePreferencePage.UNIFIED_DIFF, false);
221+
CountingCompareEditorInput input = openAndWait(true);
222+
assertTrue(input.prepareInputCalls() >= 1, "prepareInput did not run"); //$NON-NLS-1$
223+
assertEquals(false, input.ranOnUiThread(),
224+
"prepareInput must run off the UI thread when the input can run as a job"); //$NON-NLS-1$
225+
}
226+
227+
// Documents current behavior: the unified diff path prepares the input on the
228+
// UI thread. This assertion flips once the preparation moves to a background job.
229+
@Test
230+
public void testPrepareInputOnUiThreadUnifiedOnDocumentsCurrentBehavior() throws Exception {
231+
store().setValue(ComparePreferencePage.UNIFIED_DIFF, true);
232+
CountingCompareEditorInput input = openAndWait(true);
233+
assertTrue(input.prepareInputCalls() >= 1, "prepareInput did not run"); //$NON-NLS-1$
234+
assertEquals(true, input.ranOnUiThread(),
235+
"documents current behavior: unified diff runs prepareInput on the UI thread (Phase 1 item 3)"); //$NON-NLS-1$
236+
}
237+
238+
private CountingCompareEditorInput openAndWait(boolean runAsJob) throws Exception {
239+
CountingCompareEditorInput input = new CountingCompareEditorInput(runAsJob,
240+
new CountingElement("left.txt", "alpha\nbravo\ncharlie\n"), //$NON-NLS-1$ //$NON-NLS-2$
241+
new CountingElement("right.txt", "alpha\nBRAVO\ncharlie\n")); //$NON-NLS-1$ //$NON-NLS-2$
242+
CompareUI.openCompareEditor(input);
243+
pumpUntil(() -> input.getCompareResult() != null && contentPane(input) != null,
244+
"compare editor did not finish opening"); //$NON-NLS-1$
245+
return input;
246+
}
247+
248+
private static IMergeViewerTestAdapter mergeViewerAdapter(CompareEditorInput input) {
249+
CompareViewerSwitchingPane pane = contentPane(input);
250+
if (pane == null) {
251+
return null;
252+
}
253+
Viewer viewer = pane.getViewer();
254+
if (viewer == null) {
255+
return null;
256+
}
257+
return Adapters.adapt(viewer, IMergeViewerTestAdapter.class);
258+
}
259+
260+
private static CompareViewerSwitchingPane contentPane(CompareEditorInput input) {
261+
try {
262+
return (CompareViewerSwitchingPane) ReflectionUtils.getField(input, "fContentInputPane", true); //$NON-NLS-1$
263+
} catch (ReflectiveOperationException e) {
264+
throw new IllegalStateException(e);
265+
}
266+
}
267+
268+
private static void pumpUntil(BooleanSupplier condition, String failMessage) {
269+
Display display = Display.getCurrent();
270+
long deadline = System.currentTimeMillis() + TIMEOUT_MILLIS;
271+
// A self-rescheduling timer keeps the loop waking so the deadline is
272+
// enforced even while blocked in Display.sleep().
273+
Runnable[] wake = new Runnable[1];
274+
wake[0] = () -> display.timerExec(50, wake[0]);
275+
display.timerExec(50, wake[0]);
276+
try {
277+
while (!condition.getAsBoolean()) {
278+
if (System.currentTimeMillis() > deadline) {
279+
fail(failMessage + " within " + TIMEOUT_MILLIS + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
280+
}
281+
if (!display.readAndDispatch()) {
282+
display.sleep();
283+
}
284+
}
285+
} finally {
286+
display.timerExec(-1, wake[0]);
287+
}
288+
}
289+
290+
private void processQueuedEvents() {
291+
Display display = Display.getCurrent();
292+
while (display.readAndDispatch()) {
293+
// drain the event queue
294+
}
295+
}
296+
297+
private static IWorkbenchPage activePage() {
298+
return PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
299+
}
300+
301+
private static IPreferenceStore store() {
302+
return CompareUIPlugin.getDefault().getPreferenceStore();
303+
}
304+
}

0 commit comments

Comments
 (0)