question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

TransactionalTestExecutionListener Hotspot

See original GitHub issue

Affects: Spring 5.2.4


We have a relatively large, monolithic Spring Boot application and I have been doing some profiling of our integration tests. For context, our application has 1127 bean definitions and a majority of our performance issues are in our code base.

However, I did find a hotspot in the TransactionalTestExecutionListener. It has two methods to find annotated methods on the test class: runBeforeTransactionMethods() and runAfterTransactionMethods.

I am using JProfiler with full instrumentation, so the times are skewed :

TransactionalTestExecutionListener

After looking at the code for a bit, I extended the listener to cache the list of methods for a given class and replaced the the default listener with our custom listener:

public class CachingTransactionalTestExecutionListener extends TransactionalTestExecutionListener {
	

	/**
	 * A cache of class -> methods annotated with BeforeTransaction.
	 */
	private static LruClassMethodCache beforeTransactionMethodCache = new LruClassMethodCache(4);

	/**
	 * A cache of class -> methods annotated with AfterTransaction.
	 */
	private static LruClassMethodCache afterTransactionMethodCache = new LruClassMethodCache(4);

	@Override
	protected void runBeforeTransactionMethods(TestContext testContext) throws Exception {

		try {
			List<Method> methods = beforeTransactionMethodCache.get(testContext.getTestClass());
			if (methods == null) {
		
				methods = getAnnotatedMethods(testContext.getTestClass(), BeforeTransaction.class);
				Collections.reverse(methods);
				for (Method method : methods) {
					ReflectionUtils.makeAccessible(method);
				}
				beforeTransactionMethodCache.put(testContext.getTestClass(), methods);
			}
			for (Method method : methods) {
				if (logger.isDebugEnabled()) {
					logger.debug("Executing @BeforeTransaction method [" + method + "] for test context " + testContext);
				}
				method.invoke(testContext.getTestInstance());
			}			
		}
		catch (InvocationTargetException ex) {
			if (logger.isErrorEnabled()) {
				logger.error("Exception encountered while executing @BeforeTransaction methods for test context " +
						testContext + ".", ex.getTargetException());
			}
			ReflectionUtils.rethrowException(ex.getTargetException());
		}
	}

	protected void runAfterTransactionMethods(TestContext testContext) throws Exception {
		Throwable afterTransactionException = null;

		List<Method> methods = afterTransactionMethodCache.get(testContext.getTestClass());
		if (methods == null) {
			methods = getAnnotatedMethods(testContext.getTestClass(), AfterTransaction.class);
			for (Method method : methods) {			
					if (logger.isDebugEnabled()) {
						logger.debug("Executing @AfterTransaction method [" + method + "] for test context " + testContext);
					}
					ReflectionUtils.makeAccessible(method);
			}
			afterTransactionMethodCache.put(testContext.getTestClass(), methods);
		}
		
		for (Method method : methods) {			
			try {
				if (logger.isDebugEnabled()) {
					logger.debug("Executing @AfterTransaction method [" + method + "] for test context " + testContext);
				}
				method.invoke(testContext.getTestInstance());
			}
			catch (InvocationTargetException ex) {
				Throwable targetException = ex.getTargetException();
				if (afterTransactionException == null) {
					afterTransactionException = targetException;
				}
				logger.error("Exception encountered while executing @AfterTransaction method [" + method +
						"] for test context " + testContext, targetException);
			}
			catch (Exception ex) {
				if (afterTransactionException == null) {
					afterTransactionException = ex;
				}
				logger.error("Exception encountered while executing @AfterTransaction method [" + method +
						"] for test context " + testContext, ex);
			}
		}

		if (afterTransactionException != null) {
			ReflectionUtils.rethrowException(afterTransactionException);
		}
	}

// ...

	private static class LruClassMethodCache extends LinkedHashMap<Class<?>, List<Method>> {

		/**
		 * Create a new {@code LruCache} with the supplied initial capacity
		 * and load factor.
		 * @param initialCapacity the initial capacity
		 * @param loadFactor the load factor
		 */
		LruClassMethodCache(int initialCapacity) {
			super(initialCapacity);
		}

		@Override
		protected boolean removeEldestEntry(Map.Entry<Class<?>, List<Method>> eldest) {
			if (size() > 4) {
				return true;
			} else {
				return false;
			}
		}
	}

	static class PostProcessor implements DefaultTestExecutionListenersPostProcessor {

		@Override
		public Set<Class<? extends TestExecutionListener>> postProcessDefaultTestExecutionListeners(
				Set<Class<? extends TestExecutionListener>> listeners) {
			Set<Class<? extends TestExecutionListener>> updated = new LinkedHashSet<>(listeners.size());
			for (Class<? extends TestExecutionListener> listener : listeners) {				updated.add(listener.equals(TransactionalTestExecutionListener.class)
						? CachingTransactionalTestExecutionListener.class : listener);
			}
			return updated;
		}


To test this, I created 10 IT classes (each with 50 methods), and just do a simple assert. :

public class Example1IT  extends BaseDaoTest  {

	@Test
	public void test1() {
		assertThat("This String").isNotNull();
	}
	@Test
	public void test2() {
		assertThat("This String").isNotNull();
	}

	// ...

	@Test
	public void test50() {
		assertThat("This String").isNotNull();
	}

There is some “noise” because I am running this on my machine but the results look promising (but definitely not scientific 8)

After this change, the hotspot is no longer present in our profiling results and when running those 500 tests with no profiler in play:

Before:

normal-dao

After:

optimized-dao

I wish I could report back how this change impacts our 8000+ integration test but I am currently working from home and the database activity over the VPN makes it very difficult to get repeatable results.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:9 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
sbrannencommented, Mar 25, 2020

There are two additional TestExecutionListners in the Spring Boot project that also have hotspots, if you think this sort of profiling is useful, I can open an issue with them as well.

If you feel that it would make a noticeable difference, feel free to raise issues in Spring Boot’s issue tracker for those.

1reaction
tkvangordercommented, Mar 25, 2020

yeah, I wasn’t sure what size to make it, for the most part our tests run single-threaded. One thing I can try this morning is to add some timers around those methods and then report to results on shutdown, just so I can better quantify the actual time in our integration tests. I will let you know what I find. There are two additional TestExecutionListners in the Spring Boot project that also have hotspots, if you think this sort of profiling is useful, I can open an issue with them as well.

Read more comments on GitHub >

github_iconTop Results From Across the Web

TransactionalTestExecutionListe...
TestExecutionListener that provides support for executing tests within test-managed transactions by honoring Spring's @Transactional annotation.
Read more >
TestExecutionListeners annotation prevents spring beans ...
When I remove the @TestExecutionListeners annotation in MyTest the test finishes as expected, but leaving that annotation makes the unittest ...
Read more >
After upgrade to server version 5 , Java client can't connect to ...
It's seems like in the new version when create bucket no “Access Control” for password. Trying : bucket = couchBaseCluster.openBucket(“ ...
Read more >
Open Source Used In Crosswork Data Gateway 1.1.3 - Cisco
This document contains licenses and notices for open source software used in this product. With respect to the free/open source software listed in...
Read more >
spring boot 实战 / 可执行war启动参数详解 - 51CTO博客
TransactionalTestExecutionListener ] due to a missing dependency. ... path.separator=:, java.vm.name=Java HotSpot(TM) 64-Bit Server VM, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found