Skip to content

Commit b2fb5ea

Browse files
committed
8331142: Add test for number of loader threads in BasicDirectoryModel
Reviewed-by: serb, tr
1 parent 663acd2 commit b2fb5ea

File tree

1 file changed

+276
-0
lines changed

1 file changed

+276
-0
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
/*
2+
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
import java.io.IOException;
25+
import java.nio.file.Files;
26+
import java.nio.file.Path;
27+
import java.nio.file.Paths;
28+
import java.util.ArrayList;
29+
import java.util.Arrays;
30+
import java.util.List;
31+
import java.util.Objects;
32+
import java.util.concurrent.BrokenBarrierException;
33+
import java.util.concurrent.CyclicBarrier;
34+
import java.util.concurrent.atomic.AtomicReference;
35+
import java.util.stream.LongStream;
36+
import java.util.stream.Stream;
37+
38+
import javax.swing.JFileChooser;
39+
40+
/*
41+
* @test
42+
* @bug 8325179
43+
* @summary Verifies there's only one BasicDirectoryModel.FilesLoader thread
44+
* at any given moment
45+
* @run main/othervm -Djava.awt.headless=true LoaderThreadCount
46+
*/
47+
public final class LoaderThreadCount extends ThreadGroup {
48+
/** Initial number of files. */
49+
private static final long NUMBER_OF_FILES = 500;
50+
51+
/**
52+
* Number of threads running {@code fileChooser.rescanCurrentDirectory()}.
53+
*/
54+
private static final int NUMBER_OF_THREADS = 5;
55+
56+
/** Number of snapshots with live threads. */
57+
private static final int SNAPSHOTS = 20;
58+
59+
/** The barrier to synchronise scanner threads and capturing live threads. */
60+
private static final CyclicBarrier start = new CyclicBarrier(NUMBER_OF_THREADS + 1);
61+
62+
/** List of scanner threads. */
63+
private static final List<Thread> threads = new ArrayList<>(NUMBER_OF_THREADS);
64+
65+
/**
66+
* Stores an exception caught by any of the threads.
67+
* If more exceptions are caught, they're added as suppressed exceptions.
68+
*/
69+
private static final AtomicReference<Throwable> exception =
70+
new AtomicReference<>();
71+
72+
/**
73+
* Stores an {@code IOException} thrown while removing the files.
74+
*/
75+
private static final AtomicReference<IOException> ioException =
76+
new AtomicReference<>();
77+
78+
79+
public static void main(String[] args) throws Throwable {
80+
try {
81+
// Start the test in its own thread group to catch and handle
82+
// all thrown exceptions, in particular in
83+
// BasicDirectoryModel.FilesLoader which is created by Swing.
84+
ThreadGroup threadGroup = new LoaderThreadCount();
85+
Thread runner = new Thread(threadGroup,
86+
LoaderThreadCount::wrapper,
87+
"Test Runner");
88+
runner.start();
89+
runner.join();
90+
} catch (Throwable throwable) {
91+
handleException(throwable);
92+
}
93+
94+
if (ioException.get() != null) {
95+
System.err.println("An error occurred while removing files:");
96+
ioException.get().printStackTrace();
97+
}
98+
99+
if (exception.get() != null) {
100+
throw exception.get();
101+
}
102+
}
103+
104+
private static void wrapper() {
105+
final long timeStart = System.currentTimeMillis();
106+
try {
107+
runTest(timeStart);
108+
} catch (Throwable throwable) {
109+
handleException(throwable);
110+
} finally {
111+
System.out.printf("Duration: %,d\n",
112+
(System.currentTimeMillis() - timeStart));
113+
}
114+
}
115+
116+
private static void runTest(final long timeStart) throws Throwable {
117+
final Path temp = Files.createDirectory(Paths.get("fileChooser-concurrency-" + timeStart));
118+
119+
try {
120+
createFiles(temp);
121+
122+
final JFileChooser fc = new JFileChooser(temp.toFile());
123+
124+
threads.addAll(Stream.generate(() -> new Thread(new Scanner(fc)))
125+
.limit(NUMBER_OF_THREADS)
126+
.toList());
127+
threads.forEach(Thread::start);
128+
129+
// Create snapshots of live threads
130+
List<Thread[]> threadsCapture =
131+
Stream.generate(LoaderThreadCount::getThreadSnapshot)
132+
.limit(SNAPSHOTS)
133+
.toList();
134+
135+
threads.forEach(Thread::interrupt);
136+
137+
List<Long> loaderCount =
138+
threadsCapture.stream()
139+
.map(ta -> Arrays.stream(ta)
140+
.filter(Objects::nonNull)
141+
.map(Thread::getName)
142+
.filter(tn -> tn.startsWith("Basic L&F File Loading Thread"))
143+
.count())
144+
.filter(c -> c > 0)
145+
.toList();
146+
147+
if (loaderCount.isEmpty()) {
148+
throw new RuntimeException("Invalid results: no loader threads detected");
149+
}
150+
151+
System.out.println("Number of snapshots: " + loaderCount.size());
152+
153+
long ones = loaderCount.stream()
154+
.filter(n -> n == 1)
155+
.count();
156+
long twos = loaderCount.stream()
157+
.filter(n -> n == 2)
158+
.count();
159+
long count = loaderCount.stream()
160+
.filter(n -> n > 2)
161+
.count();
162+
System.out.println("Number of snapshots where number of loader threads:");
163+
System.out.println(" = 1: " + ones);
164+
System.out.println(" = 2: " + twos);
165+
System.out.println(" > 2: " + count);
166+
if (count > 0) {
167+
throw new RuntimeException("Detected " + count + " snapshots "
168+
+ "with several loading threads");
169+
}
170+
} catch (Throwable e) {
171+
threads.forEach(Thread::interrupt);
172+
throw e;
173+
} finally {
174+
deleteFiles(temp);
175+
deleteFile(temp);
176+
}
177+
}
178+
179+
private static Thread[] getThreadSnapshot() {
180+
try {
181+
start.await();
182+
// Allow for the scanner threads to initiate re-scanning
183+
Thread.sleep(10);
184+
185+
Thread[] array = new Thread[Thread.activeCount()];
186+
Thread.currentThread()
187+
.getThreadGroup()
188+
.enumerate(array, false);
189+
190+
// Additional delay between captures
191+
Thread.sleep(500);
192+
193+
return array;
194+
} catch (InterruptedException | BrokenBarrierException e) {
195+
handleException(e);
196+
throw new RuntimeException("getThreadSnapshot is interrupted");
197+
}
198+
}
199+
200+
201+
private LoaderThreadCount() {
202+
super("bdmConcurrency");
203+
}
204+
205+
@Override
206+
public void uncaughtException(Thread t, Throwable e) {
207+
handleException(t, e);
208+
}
209+
210+
private static void handleException(Throwable throwable) {
211+
handleException(Thread.currentThread(), throwable);
212+
}
213+
214+
private static void handleException(final Thread thread,
215+
final Throwable throwable) {
216+
System.err.println("Exception in " + thread.getName() + ": "
217+
+ throwable.getClass()
218+
+ (throwable.getMessage() != null
219+
? ": " + throwable.getMessage()
220+
: ""));
221+
if (!exception.compareAndSet(null, throwable)) {
222+
exception.get().addSuppressed(throwable);
223+
}
224+
threads.stream()
225+
.filter(t -> t != thread)
226+
.forEach(Thread::interrupt);
227+
}
228+
229+
230+
private record Scanner(JFileChooser fileChooser)
231+
implements Runnable {
232+
233+
@Override
234+
public void run() {
235+
try {
236+
do {
237+
start.await();
238+
fileChooser.rescanCurrentDirectory();
239+
} while (!Thread.interrupted());
240+
} catch (InterruptedException | BrokenBarrierException e) {
241+
// Just exit the loop
242+
}
243+
}
244+
}
245+
246+
private static void createFiles(final Path parent) {
247+
LongStream.range(0, LoaderThreadCount.NUMBER_OF_FILES)
248+
.mapToObj(n -> parent.resolve(n + ".file"))
249+
.forEach(LoaderThreadCount::createFile);
250+
}
251+
252+
private static void createFile(final Path file) {
253+
try {
254+
Files.createFile(file);
255+
} catch (IOException e) {
256+
throw new RuntimeException(e);
257+
}
258+
}
259+
260+
private static void deleteFiles(final Path parent) throws IOException {
261+
try (var stream = Files.walk(parent)) {
262+
stream.filter(p -> p != parent)
263+
.forEach(LoaderThreadCount::deleteFile);
264+
}
265+
}
266+
267+
private static void deleteFile(final Path file) {
268+
try {
269+
Files.delete(file);
270+
} catch (IOException e) {
271+
if (!ioException.compareAndSet(null, e)) {
272+
ioException.get().addSuppressed(e);
273+
}
274+
}
275+
}
276+
}

0 commit comments

Comments
 (0)