refactor(task): consolidate 6 executor pools into 3 by resource profile
TaskService/ExecutorConfig had one ThreadPoolTaskExecutor per TaskType (DATABASE, THUMBNAIL, METADATA, IMAGE_ANALYSIS, BACKGROUND_SYNC, HASH), each with its own hand-tuned size, but the actual work only falls into three resource profiles: a single-writer DB pool (unchanged), a CPU-bound pool for native-codec-backed decode/hash work (THUMBNAIL+IMAGE_ANALYSIS, still bounded platform threads to avoid oversubscribing the CPU and native-call pinning), and an I/O-bound pool (METADATA+BACKGROUND_SYNC+HASH) now backed by Executors.newVirtualThreadPerTaskExecutor() instead of a guessed thread count, since none of that work touches native code. TaskType keeps its 6 values as semantic labels; TaskService now maps several of them onto the same underlying executor instead of injecting six. PholioProperties.Pools drops thumbnail/metadata/image-analysis/ background-sync/hash in favour of a single cpu size. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
@@ -13,26 +15,33 @@ import java.util.concurrent.ThreadPoolExecutor;
|
||||
* <p>Every pool is a Spring bean so that closing the context shuts them all down — which is what the
|
||||
* window's close handler relies on to release H2 connections, caches and in-flight decodes.
|
||||
*
|
||||
* <p>Two deliberate choices worth keeping:
|
||||
* <p>Three deliberate choices worth keeping:
|
||||
* <ul>
|
||||
* <li>{@code databaseExecutor} is <strong>single-threaded</strong>. H2 in file mode serialises
|
||||
* writes anyway; funnelling them through one thread removes lock contention and makes batch
|
||||
* import ordering deterministic.
|
||||
* <li>Rejection policy is {@link ThreadPoolExecutor.AbortPolicy}, never {@code CallerRunsPolicy}.
|
||||
* A caller-runs policy would execute rejected work on whichever thread submitted it — and the
|
||||
* submitter is frequently the JavaFX application thread, which would freeze the UI. Callers
|
||||
* handle {@link java.util.concurrent.RejectedExecutionException} instead.
|
||||
* <li>{@code cpuExecutor} stays a bounded, platform-thread pool ({@code cores - 1}) rather than
|
||||
* virtual threads: everything it runs (thumbnail decode/resize/encode, the perceptual hash) is
|
||||
* genuinely CPU-bound and goes through the JDK's native image codec — an unbounded
|
||||
* virtual-thread-per-task executor would oversubscribe the CPU instead of queueing, and a
|
||||
* blocked native call pins whatever thread runs it.
|
||||
* <li>{@code ioExecutor} is the opposite: EXIF header reads, filesystem scans and file hashing
|
||||
* (often over a NAS share) block on I/O, not CPU, and touch nothing native — exactly where
|
||||
* {@link Executors#newVirtualThreadPerTaskExecutor()} pays off, since concurrency then scales
|
||||
* with whatever the I/O can actually sustain instead of a hand-picked thread count.
|
||||
* <li>Rejection policy for the two platform-thread pools is {@link ThreadPoolExecutor.AbortPolicy},
|
||||
* never {@code CallerRunsPolicy}. A caller-runs policy would execute rejected work on whichever
|
||||
* thread submitted it — and the submitter is frequently the JavaFX application thread, which
|
||||
* would freeze the UI. Callers handle {@link java.util.concurrent.RejectedExecutionException}
|
||||
* instead. {@code ioExecutor} has no bounded queue to reject from in the first place.
|
||||
* </ul>
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ExecutorConfig {
|
||||
|
||||
public static final String DATABASE_EXECUTOR = "databaseExecutor";
|
||||
public static final String THUMBNAIL_EXECUTOR = "thumbnailExecutor";
|
||||
public static final String METADATA_EXECUTOR = "metadataExecutor";
|
||||
public static final String IMAGE_ANALYSIS_EXECUTOR = "imageAnalysisExecutor";
|
||||
public static final String BACKGROUND_SYNC_EXECUTOR = "backgroundSyncExecutor";
|
||||
public static final String HASH_EXECUTOR = "hashExecutor";
|
||||
public static final String CPU_EXECUTOR = "cpuExecutor";
|
||||
public static final String IO_EXECUTOR = "ioExecutor";
|
||||
public static final String APPLICATION_SCHEDULER = "applicationScheduler";
|
||||
|
||||
@Bean(DATABASE_EXECUTOR)
|
||||
@@ -40,29 +49,14 @@ public class ExecutorConfig {
|
||||
return pool("pholio-db-", 1, properties);
|
||||
}
|
||||
|
||||
@Bean(THUMBNAIL_EXECUTOR)
|
||||
ThreadPoolTaskExecutor thumbnailExecutor(PholioProperties properties) {
|
||||
return pool("pholio-thumb-", properties.pools().effectiveThumbnail(), properties);
|
||||
@Bean(CPU_EXECUTOR)
|
||||
ThreadPoolTaskExecutor cpuExecutor(PholioProperties properties) {
|
||||
return pool("pholio-cpu-", properties.pools().effectiveCpu(), properties);
|
||||
}
|
||||
|
||||
@Bean(METADATA_EXECUTOR)
|
||||
ThreadPoolTaskExecutor metadataExecutor(PholioProperties properties) {
|
||||
return pool("pholio-meta-", properties.pools().effectiveMetadata(), properties);
|
||||
}
|
||||
|
||||
@Bean(IMAGE_ANALYSIS_EXECUTOR)
|
||||
ThreadPoolTaskExecutor imageAnalysisExecutor(PholioProperties properties) {
|
||||
return pool("pholio-ai-", properties.pools().imageAnalysis(), properties);
|
||||
}
|
||||
|
||||
@Bean(BACKGROUND_SYNC_EXECUTOR)
|
||||
ThreadPoolTaskExecutor backgroundSyncExecutor(PholioProperties properties) {
|
||||
return pool("pholio-sync-", properties.pools().backgroundSync(), properties);
|
||||
}
|
||||
|
||||
@Bean(HASH_EXECUTOR)
|
||||
ThreadPoolTaskExecutor hashExecutor(PholioProperties properties) {
|
||||
return pool("pholio-hash-", properties.pools().hash(), properties);
|
||||
@Bean(IO_EXECUTOR)
|
||||
ExecutorService ioExecutor() {
|
||||
return Executors.newVirtualThreadPerTaskExecutor();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,29 +16,20 @@ public record PholioProperties(@DefaultValue Pools pools,
|
||||
@DefaultValue Window window) {
|
||||
|
||||
/**
|
||||
* Thread pool sizing. Each functional area gets its own pool so a slow NAS scan can never starve
|
||||
* thumbnail decoding, and neither can ever run on the JavaFX application thread.
|
||||
* Thread pool sizing. Only {@code cpu} is left to size: the database pool is fixed at one thread
|
||||
* (see {@code ExecutorConfig}'s javadoc) and the I/O pool is a virtual-thread-per-task executor,
|
||||
* which has no thread count to pick — its concurrency scales with whatever the underlying I/O
|
||||
* (disk, NAS) can actually sustain.
|
||||
*/
|
||||
public record Pools(// Thumbnail decoding. 0 means "one thread per core, minus one left for the UI".
|
||||
@DefaultValue("0") int thumbnail,
|
||||
// Metadata extraction. 0 means "one thread per core, minus one left for the UI".
|
||||
@DefaultValue("0") int metadata,
|
||||
@DefaultValue("1") int imageAnalysis,
|
||||
@DefaultValue("1") int backgroundSync,
|
||||
// Content-hashing a scanned file (see TaskType.HASH): I/O-bound, dominated by
|
||||
// reading the file rather than CPU, so it profits from more concurrency than a
|
||||
// core count would suggest — especially over a network share.
|
||||
@DefaultValue("8") int hash,
|
||||
public record Pools(// CPU-bound decode work (thumbnail generation, perceptual hash). 0 means "one
|
||||
// thread per core, minus one left for the UI".
|
||||
@DefaultValue("0") int cpu,
|
||||
@DefaultValue("2") int scheduler,
|
||||
@DefaultValue("1000000") int queueCapacity,
|
||||
@DefaultValue("10s") Duration awaitTermination) {
|
||||
|
||||
public int effectiveThumbnail() {
|
||||
return thumbnail > 0 ? thumbnail : Math.max(1, Runtime.getRuntime().availableProcessors() - 1);
|
||||
}
|
||||
|
||||
public int effectiveMetadata() {
|
||||
return metadata > 0 ? metadata : Math.max(1, Runtime.getRuntime().availableProcessors() - 1);
|
||||
public int effectiveCpu() {
|
||||
return cpu > 0 ? cpu : Math.max(1, Runtime.getRuntime().availableProcessors() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,18 +52,18 @@ public class TaskService {
|
||||
private final ApplicationEventPublisher publisher;
|
||||
|
||||
public TaskService(@Qualifier(ExecutorConfig.DATABASE_EXECUTOR) Executor database,
|
||||
@Qualifier(ExecutorConfig.THUMBNAIL_EXECUTOR) Executor thumbnail,
|
||||
@Qualifier(ExecutorConfig.METADATA_EXECUTOR) Executor metadata,
|
||||
@Qualifier(ExecutorConfig.IMAGE_ANALYSIS_EXECUTOR) Executor imageAnalysis,
|
||||
@Qualifier(ExecutorConfig.BACKGROUND_SYNC_EXECUTOR) Executor backgroundSync,
|
||||
@Qualifier(ExecutorConfig.HASH_EXECUTOR) Executor hash,
|
||||
@Qualifier(ExecutorConfig.CPU_EXECUTOR) Executor cpu,
|
||||
@Qualifier(ExecutorConfig.IO_EXECUTOR) Executor io,
|
||||
ApplicationEventPublisher publisher) {
|
||||
executors.put(TaskType.DATABASE, database);
|
||||
executors.put(TaskType.THUMBNAIL, thumbnail);
|
||||
executors.put(TaskType.METADATA, metadata);
|
||||
executors.put(TaskType.IMAGE_ANALYSIS, imageAnalysis);
|
||||
executors.put(TaskType.BACKGROUND_SYNC, backgroundSync);
|
||||
executors.put(TaskType.HASH, hash);
|
||||
// CPU-bound, native-codec-backed decode work: kept off the virtual-thread pool below, see
|
||||
// ExecutorConfig's javadoc on why.
|
||||
executors.put(TaskType.THUMBNAIL, cpu);
|
||||
executors.put(TaskType.IMAGE_ANALYSIS, cpu);
|
||||
// I/O-bound, no native calls: free to share the virtual-thread pool.
|
||||
executors.put(TaskType.METADATA, io);
|
||||
executors.put(TaskType.BACKGROUND_SYNC, io);
|
||||
executors.put(TaskType.HASH, io);
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.icroco.pholio.infra.task;
|
||||
|
||||
/**
|
||||
* The pools {@link TaskService} can run work on, one per functional area — the same split
|
||||
* {@code ExecutorConfig} already makes, named so a caller picks a pool without injecting one.
|
||||
* The functional areas {@link TaskService} can run work on — named so a caller picks a lane without
|
||||
* injecting an {@code Executor} itself, and without needing to know or care which of
|
||||
* {@code ExecutorConfig}'s three actual pools its work lands on: several of these share a pool (see
|
||||
* {@link TaskService}'s constructor), grouped by resource profile rather than one pool each.
|
||||
*
|
||||
* <p>Deliberately excludes the application scheduler: that pool runs recurring or debounced work
|
||||
* with no single result and nothing to cancel, a different shape from the one-shot, trackable tasks
|
||||
@@ -10,16 +12,19 @@ package org.icroco.pholio.infra.task;
|
||||
*/
|
||||
public enum TaskType {
|
||||
|
||||
/** Single-threaded — see {@code ExecutorConfig}'s javadoc on why. */
|
||||
/** {@code ExecutorConfig#DATABASE_EXECUTOR} — single-threaded, see that class's javadoc on why. */
|
||||
DATABASE,
|
||||
/** {@code ExecutorConfig#CPU_EXECUTOR} — CPU-bound thumbnail decode/resize/encode. */
|
||||
THUMBNAIL,
|
||||
/** {@code ExecutorConfig#IO_EXECUTOR} — EXIF header reads, I/O-bound. */
|
||||
METADATA,
|
||||
/** {@code ExecutorConfig#CPU_EXECUTOR} — the perceptual hash, derived from an already-decoded bitmap. */
|
||||
IMAGE_ANALYSIS,
|
||||
/** {@code ExecutorConfig#IO_EXECUTOR} — filesystem tree scans, I/O-bound. */
|
||||
BACKGROUND_SYNC,
|
||||
/**
|
||||
* Content-hashing a scanned file — a full-file read whose cost is dominated by I/O (a NAS share,
|
||||
* typically), not CPU. Its own pool so a slow share can be read from concurrently without starving
|
||||
* {@link #THUMBNAIL} decoding, the same isolation reason every other pool exists.
|
||||
* {@code ExecutorConfig#IO_EXECUTOR} — content-hashing a scanned file, a full-file read whose cost
|
||||
* is dominated by I/O (a NAS share, typically), not CPU.
|
||||
*/
|
||||
HASH
|
||||
}
|
||||
|
||||
@@ -56,14 +56,10 @@ pholio:
|
||||
# instead — which is what the test context does.
|
||||
enabled: true
|
||||
pools:
|
||||
# 0 => availableProcessors() - 1
|
||||
thumbnail: 0
|
||||
metadata: 0
|
||||
image-analysis: 1
|
||||
background-sync: 1
|
||||
# Content-hashing a scanned file: I/O-bound (often a NAS share), so more concurrency than a core
|
||||
# count would suggest is worth it — the file read is what's slow, not the digest itself.
|
||||
hash: 8
|
||||
# CPU-bound decode work (thumbnail generation, perceptual hash). 0 => availableProcessors() - 1.
|
||||
# The database pool is fixed at 1 thread and the I/O pool is virtual-thread-per-task -- neither
|
||||
# has a size to configure here, see ExecutorConfig's javadoc.
|
||||
cpu: 0
|
||||
scheduler: 2
|
||||
queue-capacity: 1000000
|
||||
await-termination: 10s
|
||||
|
||||
@@ -57,11 +57,8 @@ class CliContextTest {
|
||||
@Test
|
||||
void allThreadPoolsArePresent() {
|
||||
assertThat(context.containsBean(ExecutorConfig.DATABASE_EXECUTOR)).isTrue();
|
||||
assertThat(context.containsBean(ExecutorConfig.THUMBNAIL_EXECUTOR)).isTrue();
|
||||
assertThat(context.containsBean(ExecutorConfig.METADATA_EXECUTOR)).isTrue();
|
||||
assertThat(context.containsBean(ExecutorConfig.IMAGE_ANALYSIS_EXECUTOR)).isTrue();
|
||||
assertThat(context.containsBean(ExecutorConfig.BACKGROUND_SYNC_EXECUTOR)).isTrue();
|
||||
assertThat(context.containsBean(ExecutorConfig.HASH_EXECUTOR)).isTrue();
|
||||
assertThat(context.containsBean(ExecutorConfig.CPU_EXECUTOR)).isTrue();
|
||||
assertThat(context.containsBean(ExecutorConfig.IO_EXECUTOR)).isTrue();
|
||||
assertThat(context.containsBean(ExecutorConfig.APPLICATION_SCHEDULER)).isTrue();
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ class LibraryFolderServiceTest {
|
||||
events = new CopyOnWriteArrayList<>();
|
||||
|
||||
Executor inert = Runnable::run;
|
||||
TaskService taskService = new TaskService(inert, inert, inert, inert, inert, inert, events::add);
|
||||
TaskService taskService = new TaskService(inert, inert, inert, events::add);
|
||||
IReverseGeocoder reverseGeocoder = locations -> locations.stream()
|
||||
.collect(java.util.stream.Collectors.toMap(location -> location, location -> STUB_PLACE_NAME));
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ class MediaAnalysisServiceTest {
|
||||
events = new ArrayList<>();
|
||||
|
||||
Executor inert = Runnable::run;
|
||||
taskService = new TaskService(inert, inert, inert, inert, inert, inert, events::add);
|
||||
taskService = new TaskService(inert, inert, inert, events::add);
|
||||
service = new MediaAnalysisService(formats, thumbnailGenerator, perceptualHasher, preferences,
|
||||
mediaFileRepository, taskService, events::add);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ class MediaMetadataEditServiceTest {
|
||||
when(mediaFileRepository.findById(MEDIA_FILE_ID)).thenReturn(Optional.of(mediaFileEntity));
|
||||
|
||||
Executor inert = Runnable::run;
|
||||
TaskService taskService = new TaskService(inert, inert, inert, inert, inert, inert, publisher);
|
||||
TaskService taskService = new TaskService(inert, inert, inert, publisher);
|
||||
|
||||
service = new MediaMetadataEditService(metadataRepository, metadataMapper, mediaFileRepository,
|
||||
libraryFolderService, formatRegistry, hasher, taskService, publisher,
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ class NotificationRetentionSchedulerTest {
|
||||
|
||||
private NotificationRetentionScheduler schedulerFor(AppPreferences preferences) {
|
||||
Executor inert = Runnable::run;
|
||||
TaskService taskService = new TaskService(inert, inert, inert, inert, inert, inert, event -> {});
|
||||
TaskService taskService = new TaskService(inert, inert, inert, event -> {});
|
||||
return new NotificationRetentionScheduler(notificationService, preferences, taskService);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class StartupTaskRunnerTest {
|
||||
IStartupTask taskOne = ran::countDown;
|
||||
IStartupTask taskTwo = ran::countDown;
|
||||
Executor inert = Runnable::run;
|
||||
TaskService taskService = new TaskService(inert, inert, inert, inert, inert, inert, event -> {});
|
||||
TaskService taskService = new TaskService(inert, inert, inert, event -> {});
|
||||
|
||||
StartupTaskRunner runner = spy(new StartupTaskRunner(List.of(taskOne, taskTwo), taskService));
|
||||
doNothing().when(runner).waitRandomlyToStart();
|
||||
@@ -38,7 +38,7 @@ class StartupTaskRunnerTest {
|
||||
IStartupTask first = orderedTask(runOrder, "first", ran);
|
||||
IStartupTask middle = orderedTask(runOrder, "middle", ran);
|
||||
Executor inert = Runnable::run;
|
||||
TaskService taskService = new TaskService(inert, inert, inert, inert, inert, inert, event -> {});
|
||||
TaskService taskService = new TaskService(inert, inert, inert, event -> {});
|
||||
|
||||
StartupTaskRunner runner = spy(new StartupTaskRunner(List.of(last, first, middle), taskService));
|
||||
doNothing().when(runner).waitRandomlyToStart();
|
||||
|
||||
@@ -46,7 +46,7 @@ class LibraryImportCoordinatorTest {
|
||||
libraryFolderService = mock(LibraryFolderService.class);
|
||||
|
||||
Executor inert = Runnable::run;
|
||||
TaskService taskService = new TaskService(inert, inert, inert, inert, inert, inert, event -> {});
|
||||
TaskService taskService = new TaskService(inert, inert, inert, event -> {});
|
||||
ModalService modalService = new ModalService(null);
|
||||
|
||||
coordinator = new LibraryImportCoordinator(i18n, preferences, scanner, libraryFolderService,
|
||||
|
||||
@@ -48,7 +48,7 @@ class NotificationsManagerTest {
|
||||
void setUp() {
|
||||
FxTestToolkit.requireToolkit();
|
||||
pool = Executors.newCachedThreadPool();
|
||||
taskService = new TaskService(pool, pool, pool, pool, pool, pool, event -> {});
|
||||
taskService = new TaskService(pool, pool, pool, event -> {});
|
||||
notificationService = mock(NotificationService.class);
|
||||
I18nService i18n = FxTestToolkit.i18n(PreferencesFixture.fromBundledSchema());
|
||||
manager = new NotificationsManager(notificationService, taskService, i18n);
|
||||
|
||||
@@ -77,7 +77,7 @@ class LibraryFolderTreeTest {
|
||||
command.run();
|
||||
};
|
||||
Executor inert = Runnable::run;
|
||||
taskService = new TaskService(inert, inert, inert, inert, counting, inert, event -> {});
|
||||
taskService = new TaskService(inert, inert, counting, event -> {});
|
||||
searchState = new GallerySearchState();
|
||||
publisher = mock(ApplicationEventPublisher.class);
|
||||
section = onFxThread(() -> new LibraryFolderTree(i18n, libraryFolderService, taskService,
|
||||
|
||||
@@ -48,7 +48,7 @@ class TaskManagerTest {
|
||||
void setUp() {
|
||||
FxTestToolkit.requireToolkit();
|
||||
pool = Executors.newCachedThreadPool();
|
||||
taskService = new TaskService(pool, pool, pool, pool, pool, pool, this::dispatch);
|
||||
taskService = new TaskService(pool, pool, pool, this::dispatch);
|
||||
manager = new TaskManager(taskService, this::dispatch);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class FullImageCacheTest {
|
||||
FxTestToolkit.requireToolkit();
|
||||
preferences = PreferencesFixture.fromBundledSchema();
|
||||
Executor inert = Runnable::run;
|
||||
taskService = new TaskService(inert, inert, inert, inert, inert, inert, event -> {});
|
||||
taskService = new TaskService(inert, inert, inert, event -> {});
|
||||
cache = onFxThread(() -> new FullImageCache(preferences, taskService));
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ class GalleryViewTest {
|
||||
|
||||
MediaLibraryState state = new MediaLibraryState();
|
||||
Executor inert = Runnable::run;
|
||||
TaskService taskService = new TaskService(inert, inert, inert, inert, inert, inert, event -> {});
|
||||
TaskService taskService = new TaskService(inert, inert, inert, event -> {});
|
||||
MediaFileService mediaFileService = mock(MediaFileService.class);
|
||||
when(mediaFileService.list()).thenReturn(files);
|
||||
AppPreferences localPreferences = PreferencesFixture.fromBundledSchema();
|
||||
|
||||
@@ -56,7 +56,7 @@ class OrientedFullImageLoaderTest {
|
||||
FxTestToolkit.requireToolkit();
|
||||
AppPreferences preferences = PreferencesFixture.fromBundledSchema();
|
||||
Executor inert = Runnable::run;
|
||||
TaskService taskService = new TaskService(inert, inert, inert, inert, inert, inert, event -> {});
|
||||
TaskService taskService = new TaskService(inert, inert, inert, event -> {});
|
||||
FullImageCache fullImageCache = onFxThread(() -> new FullImageCache(preferences, taskService));
|
||||
libraryFolderService = mock(LibraryFolderService.class);
|
||||
ThumbnailGenerator thumbnailGenerator = new ThumbnailGenerator(mock(MediaFormatRegistry.class));
|
||||
|
||||
@@ -86,7 +86,7 @@ class ThumbnailGalleryPaneTest {
|
||||
state = new MediaLibraryState();
|
||||
|
||||
Executor inert = Runnable::run;
|
||||
TaskService taskService = new TaskService(inert, inert, inert, inert, inert, inert, event -> {});
|
||||
TaskService taskService = new TaskService(inert, inert, inert, event -> {});
|
||||
preferences = PreferencesFixture.fromBundledSchema();
|
||||
preferences.setValue("gallery", "thumbnail-size", 100);
|
||||
I18nService i18n = FxTestToolkit.i18n(preferences);
|
||||
|
||||
@@ -45,7 +45,7 @@ class ThumbnailImageCacheTest {
|
||||
FxTestToolkit.requireToolkit();
|
||||
preferences = PreferencesFixture.fromBundledSchema();
|
||||
Executor inert = Runnable::run;
|
||||
taskService = new TaskService(inert, inert, inert, inert, inert, inert, event -> {});
|
||||
taskService = new TaskService(inert, inert, inert, event -> {});
|
||||
cache = onFxThread(() -> new ThumbnailImageCache(preferences, taskService));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user