From b2fb5ea13ba5087411410519213fc953ecc44618 Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Wed, 1 May 2024 11:53:28 +0000 Subject: [PATCH] 8331142: Add test for number of loader threads in BasicDirectoryModel Reviewed-by: serb, tr --- .../LoaderThreadCount.java | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 test/jdk/javax/swing/plaf/basic/BasicDirectoryModel/LoaderThreadCount.java diff --git a/test/jdk/javax/swing/plaf/basic/BasicDirectoryModel/LoaderThreadCount.java b/test/jdk/javax/swing/plaf/basic/BasicDirectoryModel/LoaderThreadCount.java new file mode 100644 index 00000000000..b471c51a260 --- /dev/null +++ b/test/jdk/javax/swing/plaf/basic/BasicDirectoryModel/LoaderThreadCount.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.LongStream; +import java.util.stream.Stream; + +import javax.swing.JFileChooser; + +/* + * @test + * @bug 8325179 + * @summary Verifies there's only one BasicDirectoryModel.FilesLoader thread + * at any given moment + * @run main/othervm -Djava.awt.headless=true LoaderThreadCount + */ +public final class LoaderThreadCount extends ThreadGroup { + /** Initial number of files. */ + private static final long NUMBER_OF_FILES = 500; + + /** + * Number of threads running {@code fileChooser.rescanCurrentDirectory()}. + */ + private static final int NUMBER_OF_THREADS = 5; + + /** Number of snapshots with live threads. */ + private static final int SNAPSHOTS = 20; + + /** The barrier to synchronise scanner threads and capturing live threads. */ + private static final CyclicBarrier start = new CyclicBarrier(NUMBER_OF_THREADS + 1); + + /** List of scanner threads. */ + private static final List threads = new ArrayList<>(NUMBER_OF_THREADS); + + /** + * Stores an exception caught by any of the threads. + * If more exceptions are caught, they're added as suppressed exceptions. + */ + private static final AtomicReference exception = + new AtomicReference<>(); + + /** + * Stores an {@code IOException} thrown while removing the files. + */ + private static final AtomicReference ioException = + new AtomicReference<>(); + + + public static void main(String[] args) throws Throwable { + try { + // Start the test in its own thread group to catch and handle + // all thrown exceptions, in particular in + // BasicDirectoryModel.FilesLoader which is created by Swing. + ThreadGroup threadGroup = new LoaderThreadCount(); + Thread runner = new Thread(threadGroup, + LoaderThreadCount::wrapper, + "Test Runner"); + runner.start(); + runner.join(); + } catch (Throwable throwable) { + handleException(throwable); + } + + if (ioException.get() != null) { + System.err.println("An error occurred while removing files:"); + ioException.get().printStackTrace(); + } + + if (exception.get() != null) { + throw exception.get(); + } + } + + private static void wrapper() { + final long timeStart = System.currentTimeMillis(); + try { + runTest(timeStart); + } catch (Throwable throwable) { + handleException(throwable); + } finally { + System.out.printf("Duration: %,d\n", + (System.currentTimeMillis() - timeStart)); + } + } + + private static void runTest(final long timeStart) throws Throwable { + final Path temp = Files.createDirectory(Paths.get("fileChooser-concurrency-" + timeStart)); + + try { + createFiles(temp); + + final JFileChooser fc = new JFileChooser(temp.toFile()); + + threads.addAll(Stream.generate(() -> new Thread(new Scanner(fc))) + .limit(NUMBER_OF_THREADS) + .toList()); + threads.forEach(Thread::start); + + // Create snapshots of live threads + List threadsCapture = + Stream.generate(LoaderThreadCount::getThreadSnapshot) + .limit(SNAPSHOTS) + .toList(); + + threads.forEach(Thread::interrupt); + + List loaderCount = + threadsCapture.stream() + .map(ta -> Arrays.stream(ta) + .filter(Objects::nonNull) + .map(Thread::getName) + .filter(tn -> tn.startsWith("Basic L&F File Loading Thread")) + .count()) + .filter(c -> c > 0) + .toList(); + + if (loaderCount.isEmpty()) { + throw new RuntimeException("Invalid results: no loader threads detected"); + } + + System.out.println("Number of snapshots: " + loaderCount.size()); + + long ones = loaderCount.stream() + .filter(n -> n == 1) + .count(); + long twos = loaderCount.stream() + .filter(n -> n == 2) + .count(); + long count = loaderCount.stream() + .filter(n -> n > 2) + .count(); + System.out.println("Number of snapshots where number of loader threads:"); + System.out.println(" = 1: " + ones); + System.out.println(" = 2: " + twos); + System.out.println(" > 2: " + count); + if (count > 0) { + throw new RuntimeException("Detected " + count + " snapshots " + + "with several loading threads"); + } + } catch (Throwable e) { + threads.forEach(Thread::interrupt); + throw e; + } finally { + deleteFiles(temp); + deleteFile(temp); + } + } + + private static Thread[] getThreadSnapshot() { + try { + start.await(); + // Allow for the scanner threads to initiate re-scanning + Thread.sleep(10); + + Thread[] array = new Thread[Thread.activeCount()]; + Thread.currentThread() + .getThreadGroup() + .enumerate(array, false); + + // Additional delay between captures + Thread.sleep(500); + + return array; + } catch (InterruptedException | BrokenBarrierException e) { + handleException(e); + throw new RuntimeException("getThreadSnapshot is interrupted"); + } + } + + + private LoaderThreadCount() { + super("bdmConcurrency"); + } + + @Override + public void uncaughtException(Thread t, Throwable e) { + handleException(t, e); + } + + private static void handleException(Throwable throwable) { + handleException(Thread.currentThread(), throwable); + } + + private static void handleException(final Thread thread, + final Throwable throwable) { + System.err.println("Exception in " + thread.getName() + ": " + + throwable.getClass() + + (throwable.getMessage() != null + ? ": " + throwable.getMessage() + : "")); + if (!exception.compareAndSet(null, throwable)) { + exception.get().addSuppressed(throwable); + } + threads.stream() + .filter(t -> t != thread) + .forEach(Thread::interrupt); + } + + + private record Scanner(JFileChooser fileChooser) + implements Runnable { + + @Override + public void run() { + try { + do { + start.await(); + fileChooser.rescanCurrentDirectory(); + } while (!Thread.interrupted()); + } catch (InterruptedException | BrokenBarrierException e) { + // Just exit the loop + } + } + } + + private static void createFiles(final Path parent) { + LongStream.range(0, LoaderThreadCount.NUMBER_OF_FILES) + .mapToObj(n -> parent.resolve(n + ".file")) + .forEach(LoaderThreadCount::createFile); + } + + private static void createFile(final Path file) { + try { + Files.createFile(file); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void deleteFiles(final Path parent) throws IOException { + try (var stream = Files.walk(parent)) { + stream.filter(p -> p != parent) + .forEach(LoaderThreadCount::deleteFile); + } + } + + private static void deleteFile(final Path file) { + try { + Files.delete(file); + } catch (IOException e) { + if (!ioException.compareAndSet(null, e)) { + ioException.get().addSuppressed(e); + } + } + } +}