Skip to content

Commit

Permalink
Simplifying ContextManagers and avoid new Lists when creating or reac…
Browse files Browse the repository at this point in the history
…tivating snapshots.

This should be a significant performance improvement.

Signed-off-by: Sjoerd Talsma <sjoerdtalsma@users.noreply.github.com>
  • Loading branch information
sjoerdtalsma committed Nov 8, 2024
1 parent 7932f71 commit 46680b7
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@
import nl.talsmasoftware.context.api.ContextManager;
import nl.talsmasoftware.context.api.ContextSnapshot;
import nl.talsmasoftware.context.api.ContextSnapshot.Reactivation;
import nl.talsmasoftware.context.api.ContextTimer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.ListIterator;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand All @@ -43,15 +39,6 @@
public final class ContextManagers {
private static final Logger LOGGER = Logger.getLogger(ContextManagers.class.getName());

/**
* Sometimes a single, fixed classloader may be necessary (e.g. #97)
*/
private static volatile ClassLoader classLoaderOverride = null;

private static volatile List<ContextManager<?>> contextManagers = null;

private static volatile List<ContextTimer> contextTimers = null;

/**
* Private constructor to avoid instantiation of this class.
*/
Expand All @@ -72,32 +59,26 @@ private ContextManagers() {
* @return A new snapshot that can be reactivated elsewhere (e.g. a background thread)
* within a try-with-resources construct.
*/
@SuppressWarnings("rawtypes")
public static nl.talsmasoftware.context.api.ContextSnapshot createContextSnapshot() {
final long start = System.nanoTime();
final List<ContextManager<?>> managers = new LinkedList<>();
final List<Object> values = new LinkedList<>();
Long managerStart = null;
for (ContextManager<?> manager : getContextManagers()) {
managerStart = System.nanoTime();
final List<ContextManager> managers = ServiceCache.cached(ContextManager.class);
final Object[] values = new Object[managers.size()];

for (ListIterator<ContextManager> it = managers.listIterator(); it.hasNext(); ) {
final ContextManager manager = it.next();
long managerStart = System.nanoTime();
try {
final Object activeContextValue = manager.getActiveContextValue();
if (activeContextValue != null) {
values.add(activeContextValue);
managers.add(manager);
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("Active context value of " + manager + " added to new snapshot: " + activeContextValue);
}
Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "getActiveContext");
} else if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, "There is no active context for " + manager + " in this snapshot.");
}
values[it.previousIndex()] = getActiveContextValue(manager);
Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "getActiveContext");
} catch (RuntimeException rte) {
LOGGER.log(Level.WARNING, "Exception obtaining active context from " + manager + " for snapshot.", rte);
LOGGER.log(Level.WARNING, "Error obtaining active context from " + manager + " (in thread " + Thread.currentThread().getName() + ").", rte);
Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "getActiveContext.exception");
}
}

final ContextSnapshotImpl result = new ContextSnapshotImpl(managers, values);
if (managerStart == null && LOGGER.isLoggable(Level.FINER)) {
if (managers.isEmpty() && LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer(result + " was created but no ContextManagers were found! "
+ " Thead=" + Thread.currentThread()
+ ", ContextClassLoader=" + Thread.currentThread().getContextClassLoader());
Expand Down Expand Up @@ -126,17 +107,14 @@ public static nl.talsmasoftware.context.api.ContextSnapshot createContextSnapsho
public static void clearActiveContexts() {
final long start = System.nanoTime();
Long managerStart = null;
for (ContextManager<?> manager : getContextManagers()) {
for (ContextManager<?> manager : ServiceCache.cached(ContextManager.class)) {
managerStart = System.nanoTime();
try {
manager.clear();
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("Active context of " + manager + " was cleared.");
}
clear(manager);
Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "clear");
} catch (RuntimeException rte) {
LOGGER.log(Level.WARNING, "Exception clearing active context from " + manager + ".", rte);
contextManagers = null;
LOGGER.log(Level.WARNING, "Error clearing active context from " + manager + ".", rte);
ServiceCache.clear();
Timers.timed(System.nanoTime() - managerStart, manager.getClass(), "clear.exception");
}
}
Expand Down Expand Up @@ -170,71 +148,44 @@ public static void clearActiveContexts() {
* @since 1.0.5
*/
public static synchronized void useClassLoader(ClassLoader classLoader) {
if (classLoaderOverride == classLoader) {
LOGGER.finest(() -> "Maintaining classloader override as " + classLoader + " (unchanged)");
return;
}
LOGGER.fine(() -> "Updating classloader override to " + classLoader + " (was: " + classLoaderOverride + ")");
classLoaderOverride = classLoader;
contextManagers = null;
contextTimers = null;
}

@SuppressWarnings({"unchecked", "rawtypes"})
private static List<ContextManager<?>> getContextManagers() {
if (contextManagers == null) {
synchronized (ContextManagers.class) {
if (contextManagers == null) {
contextManagers = (List) load(ContextManager.class);
}
}
}
return contextManagers;
ServiceCache.useClassLoader(classLoader);
}

static List<ContextTimer> getContextTimers() {
if (contextTimers == null) {
synchronized (ContextManagers.class) {
if (contextTimers == null) {
contextTimers = load(ContextTimer.class);
}
}
}
return contextTimers;
private static Object getActiveContextValue(ContextManager<?> manager) {
final Object activeContextValue = manager.getActiveContextValue();
LOGGER.finest(() -> activeContextValue == null
? "There is no active context value for " + manager + " (in thread " + Thread.currentThread().getName() + ")."
: "Active context value of " + manager + " in " + Thread.currentThread().getName() + ": " + activeContextValue);
return activeContextValue;
}

private static <T> List<T> load(Class<T> type) {
ArrayList<T> list = new ArrayList<>();
if (classLoaderOverride == null) {
ServiceLoader.load(type).forEach(list::add);
} else {
ServiceLoader.load(type, classLoaderOverride).forEach(list::add);
}
list.trimToSize();
return Collections.unmodifiableList(list);
private static void clear(ContextManager<?> manager) {
manager.clear();
LOGGER.finest(() -> "Active context of " + manager + " was cleared.");
}

/**
* Implementation of the <code>createContextSnapshot</code> functionality that can reactivate all values from the
* snapshot in each corresponding {@link ContextManager}.
*/
@SuppressWarnings("rawtypes")
private static final class ContextSnapshotImpl implements nl.talsmasoftware.context.api.ContextSnapshot {
private static final ContextManager[] MANAGER_ARRAY = new ContextManager[0];
private final ContextManager[] managers;
private static final class ContextSnapshotImpl implements ContextSnapshot {
private final List<ContextManager> managers;
private final Object[] values;

private ContextSnapshotImpl(List<ContextManager<?>> managers, List<Object> values) {
this.managers = managers.toArray(MANAGER_ARRAY);
this.values = values.toArray();
private ContextSnapshotImpl(List<ContextManager> managers, Object[] values) {
this.managers = managers;
this.values = values;
}

public Reactivation reactivate() {
final long start = System.nanoTime();
final List<Context<?>> reactivatedContexts = new ArrayList<Context<?>>(managers.length);
final Context[] reactivatedContexts = new Context[managers.size()];
try {
for (int i = 0; i < managers.length && i < values.length; i++) {
reactivatedContexts.add(reactivate(managers[i], values[i]));
for (ListIterator<ContextManager> it = managers.listIterator(); it.hasNext(); ) {
final ContextManager manager = it.next();
final Object value = values[it.previousIndex()];
reactivatedContexts[it.previousIndex()] = value != null ? reactivate(manager, value) : null;
}
ReactivationImpl reactivation = new ReactivationImpl(reactivatedContexts);
Timers.timed(System.nanoTime() - start, nl.talsmasoftware.context.api.ContextSnapshot.class, "reactivate");
Expand All @@ -251,7 +202,7 @@ public Reactivation reactivate() {
reactivationException.addSuppressed(rte);
}
}
contextManagers = null;
ServiceCache.clear();
throw reactivationException;
}
}
Expand All @@ -269,7 +220,7 @@ private Context reactivate(ContextManager contextManager, Object snapshotValue)

@Override
public String toString() {
return "ContextSnapshot{size=" + managers.length + '}';
return "ContextSnapshot{size=" + managers.size() + '}';
}
}

Expand All @@ -278,18 +229,19 @@ public String toString() {
* when it is closed itself.<br>
* This context contains no meaningful value in itself and purely exists to close the reactivated contexts.
*/
@SuppressWarnings("rawtypes")
private static final class ReactivationImpl implements Reactivation {
private final List<Context<?>> reactivated;
private final Context[] reactivated;

private ReactivationImpl(List<Context<?>> reactivated) {
private ReactivationImpl(Context[] reactivated) {
this.reactivated = reactivated;
}

public void close() {
RuntimeException closeException = null;
// close in reverse order of reactivation
for (int i = this.reactivated.size() - 1; i >= 0; i--) {
Context<?> reactivated = this.reactivated.get(i);
for (int i = this.reactivated.length - 1; i >= 0; i--) {
Context<?> reactivated = this.reactivated[i];
if (reactivated != null) try {
reactivated.close();
} catch (RuntimeException rte) {
Expand All @@ -302,7 +254,7 @@ public void close() {

@Override
public String toString() {
return "ContextSnapshot.Reactivation{size=" + reactivated.size() + '}';
return "ContextSnapshot.Reactivation{size=" + reactivated.length + '}';
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2016-2024 Talsma ICT
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nl.talsmasoftware.context.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Logger;

/**
* Cache for resolved services.
*
* <p>
* This is required because the ServiceLoader itself is not thread-safe due to its internal lazy iterator.
*/
class ServiceCache {
private static final Logger LOGGER = Logger.getLogger(ServiceCache.class.getName());

/**
* Internal concurrent map as cache.
*/
@SuppressWarnings("rawtypes")
private static final ConcurrentMap<Class, List> CACHE = new ConcurrentHashMap<>();

/**
* Sometimes a single, fixed classloader may be necessary (e.g. #97)
*/
private static volatile ClassLoader classLoaderOverride = null;

static synchronized void useClassLoader(ClassLoader classLoader) {
if (classLoaderOverride == classLoader) {
LOGGER.finest(() -> "Maintaining classloader override as " + classLoader + " (unchanged)");
return;
}
LOGGER.fine(() -> "Updating classloader override to " + classLoader + " (was: " + classLoaderOverride + ")");
classLoaderOverride = classLoader;
CACHE.clear();
}

@SuppressWarnings("unchecked")
static <T> List<T> cached(Class<T> serviceClass) {
return (List<T>) CACHE.computeIfAbsent(serviceClass, ServiceCache::load);
}

static void clear() {
CACHE.clear();
}

/**
* Loads the service implementations of the requested type.
*
* <p>
* This method is synchronized because ServiceLoader is not thread-safe.
* Fortunately this only gets called after a cache miss, so should not affect performance.
*
* <p>
* The returned {@code List} will be {@linkplain Collections#unmodifiableList(List) unmodifiable}.
*
* @param serviceType The service type to load.
* @param <T> The service type to load.
* @return Unmodifiable list of service implementations.
*/
private synchronized static <T> List<T> load(Class<T> serviceType) {
final ArrayList<T> services = new ArrayList<>();
final ServiceLoader<T> loader = classLoaderOverride == null
? ServiceLoader.load(serviceType)
: ServiceLoader.load(serviceType, classLoaderOverride);
for (T service : loader) {
services.add(service);
}
services.trimToSize();
// TODO debug logging
return Collections.unmodifiableList(services);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class Timers {
private static final Logger TIMING_LOGGER = Logger.getLogger(Timers.class.getName());

static void timed(long durationNanos, Class<?> type, String method) {
for (ContextTimer delegate : ContextManagers.getContextTimers()) {
for (ContextTimer delegate : ServiceCache.cached(ContextTimer.class)) {
delegate.update(type, method, durationNanos, TimeUnit.NANOSECONDS);
}
if (TIMING_LOGGER.isLoggable(Level.FINEST)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import nl.talsmasoftware.context.dummy.DummyContextManager;
import org.junit.jupiter.api.Test;

import java.io.Closeable;
import java.io.IOException;

import static org.hamcrest.MatcherAssert.assertThat;
Expand All @@ -36,9 +35,7 @@ public class ContextSnapshotTest {

@Test
public void testSnapshotToString() {
try (Context<String> ctx = MGR.initializeNewContext("Dummy value")) {
assertThat(ContextManagers.createContextSnapshot(), hasToString(startsWith("ContextSnapshot{size=")));
}
assertThat(ContextManagers.createContextSnapshot(), hasToString(startsWith("ContextSnapshot{size=")));
}

@Test
Expand All @@ -48,9 +45,9 @@ public void testSnapshotReactivate() throws IOException {
try (Context<String> ctx2 = MGR.initializeNewContext("New value")) {
assertThat(MGR.getActiveContextValue(), is("New value"));

try (Closeable reactivation = snapshot.reactivate()) {
try (ContextSnapshot.Reactivation reactivation = snapshot.reactivate()) {
assertThat(MGR.getActiveContextValue(), is("Old value"));
assertThat(reactivation, hasToString(startsWith("ReactivatedContext{size=")));
assertThat(reactivation, hasToString(startsWith("ContextSnapshot.Reactivation{size=")));
}

assertThat(MGR.getActiveContextValue(), is("New value"));
Expand Down

0 comments on commit 46680b7

Please sign in to comment.