Skip to content

Performance: Repeated ServiceLoader scanning without caching #1611

Description

@jouzi5

Problem

ServiceLoader instances are being loaded and scanned repeatedly during WorkflowApplication.build() and other initialization paths without caching the results. ServiceLoader scanning is expensive because it:

  1. Iterates through all entries on the classpath
  2. Instantiates service provider classes
  3. Maintains internal state that's recreated each scan

This becomes a bottleneck when:

  • Multiple WorkflowApplication instances are created
  • Services are looked up frequently during workflow execution
  • Large numbers of plugins/implementations exist on the classpath

Current Issues

WorkflowApplication.java (lines 472, 482)

ServiceLoader.load(ExpressionFactory.class).forEach(exprFactories::add);
// ... later ...
ServiceLoader.load(EventPublisher.class).forEach(e -> eventPublishers.add(e));

Called during every build() invocation.

RunTaskExecutor.java (line 36)

private static final ServiceLoader<RunnableTaskBuilder> runnables =
    ServiceLoader.load(RunnableTaskBuilder.class);

Then rescanned via runnables.stream() with .sorted() on every task execution.

EmitExecutor.java (line 48)

private static final Collection<EmittedEventDecorator> emittedDecorators =
    ServiceLoader.load(EmittedEventDecorator.class).stream()
        .map(ServiceLoader.Provider::get)
        .sorted()
        .toList();

RunScriptExecutorBuilder.java (line 57)

ServiceLoader lookup happens per task without caching results by language.

DefaultTaskExecutorFactory.java (line 50)

private Collection<CallableTaskBuilder> callTasks =
    ServiceLoader.load(CallableTaskBuilder.class)
        .stream()
        .map(Provider::get)
        .sorted()
        .toList();

Good example of caching (at initialization), but other locations don't follow this pattern.

Impact

  • Startup time: Multiple milliseconds added per WorkflowApplication creation
  • Throughput: Each task execution incurs sorting/filtering overhead
  • Resource usage: Unnecessary object allocations and classpath scanning

Suggested Fix

  1. Cache ServiceLoader results at WorkflowApplication level
  2. Make cached providers available to task executors
  3. Index results by type/language for O(1) lookups instead of O(n) filtering
  4. Document caching pattern for future service implementations

Example Solution (Conceptual)

// In WorkflowApplication
private final Map<Class<?>, Collection<?>> serviceLoaderCache = new ConcurrentHashMap<>();

<T> Collection<T> loadServices(Class<T> serviceClass) {
    return (Collection<T>) serviceLoaderCache.computeIfAbsent(
        serviceClass,
        k -> ServiceLoader.load(serviceClass).stream()
            .map(ServiceLoader.Provider::get)
            .sorted()
            .toList()
    );
}

Then pass this through to executors that need service lookups.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions