Skip to content

Commit 55f7762

Browse files
committed
Resource leakage WIP
1 parent 9da5b6b commit 55f7762

8 files changed

+429
-3
lines changed

spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/SmartDirtiesContextTestExecutionListener.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static com.github.seregamorph.testsmartcontext.SmartDirtiesTestsSupport.isInnerClass;
44

5+
import com.github.seregamorph.testsmartcontext.leakage.ResourceLeakageManager;
56
import org.slf4j.Logger;
67
import org.slf4j.LoggerFactory;
78
import org.springframework.test.context.TestContext;
@@ -28,28 +29,38 @@ public class SmartDirtiesContextTestExecutionListener extends AbstractTestExecut
2829

2930
@Override
3031
public int getOrder() {
31-
// DirtiesContextTestExecutionListener.getOrder() + 1
32+
// DirtiesContextTestExecutionListener.getOrder() + 10
3233
//noinspection MagicNumber
33-
return 3001;
34+
return 3010;
3435
}
3536

3637
@Override
3738
public void beforeTestClass(TestContext testContext) {
39+
Class<?> testClass = testContext.getTestClass();
3840
// stack Nested classes
3941
CurrentTestContext.pushCurrentTestClass(testContext.getTestClass());
40-
Class<?> testClass = testContext.getTestClass();
4142
if (isInnerClass(testClass)) {
4243
SmartDirtiesTestsSupport.verifyInnerClass(testClass);
4344
}
45+
46+
ResourceLeakageManager leakageManager = ResourceLeakageManager.getInstance();
47+
if (SmartDirtiesTestsSupport.isFirstClassPerConfig(testClass)) {
48+
logger.info("firstClassPerConfig {}", testClass.getName());
49+
leakageManager.handleBeforeClassGroup();
50+
}
51+
leakageManager.handleBeforeClass(testClass);
4452
}
4553

4654
@Override
4755
public void afterTestClass(TestContext testContext) {
4856
try {
4957
Class<?> testClass = testContext.getTestClass();
58+
ResourceLeakageManager leakageManager = ResourceLeakageManager.getInstance();
59+
leakageManager.handleAfterClass(testClass);
5060
if (SmartDirtiesTestsSupport.isLastClassPerConfig(testClass)) {
5161
logger.info("markDirty (closing context) after {}", testClass.getName());
5262
testContext.markApplicationContextDirty(null);
63+
leakageManager.handleAfterClassGroup(testClass);
5364
} else {
5465
logger.debug("Reusing context after {}", testClass.getName());
5566
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import java.lang.management.ManagementFactory;
4+
import java.lang.management.MemoryMXBean;
5+
import java.util.Arrays;
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
/**
10+
*
11+
* @author Sergey Chernov
12+
*/
13+
public class HeapResourceLeakageDetector extends ResourceLeakageDetector {
14+
15+
private final MemoryMXBean memoryMXBean;
16+
17+
public HeapResourceLeakageDetector() {
18+
super(Arrays.asList("committed", "used"));
19+
this.memoryMXBean = ManagementFactory.getMemoryMXBean();
20+
}
21+
22+
@Override
23+
public Map<String, Long> getIndicators() {
24+
Map<String, Long> map = new HashMap<>();
25+
map.put("committed", memoryMXBean.getHeapMemoryUsage().getCommitted());
26+
map.put("used", memoryMXBean.getHeapMemoryUsage().getUsed());
27+
// map.put("init", memoryMXBean.getHeapMemoryUsage().getInit());
28+
// map.put("max", memoryMXBean.getHeapMemoryUsage().getMax());
29+
return map;
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import java.io.*;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
import static java.nio.charset.StandardCharsets.UTF_8;
9+
10+
/**
11+
*
12+
* @author Sergey Chernov
13+
*/
14+
public class ResourceLeakageCsvLogWriter extends ResourceLeakageLogWriter {
15+
16+
private final PrintWriter out;
17+
private final List<String> headers;
18+
19+
public ResourceLeakageCsvLogWriter(File outputFile, List<String> headers) {
20+
try {
21+
FileOutputStream fileOutputStream = new FileOutputStream(outputFile, false);
22+
out = new PrintWriter(new OutputStreamWriter(fileOutputStream, UTF_8), true);
23+
} catch (FileNotFoundException e) {
24+
throw new UncheckedIOException(e);
25+
}
26+
27+
this.headers = new ArrayList<>(headers);
28+
StringBuilder fileHeader = new StringBuilder("Timestamp,testClass,event,testGroup,test");
29+
for (String header : headers) {
30+
fileHeader.append(",").append(header);
31+
}
32+
out.println(fileHeader);
33+
}
34+
35+
@Override
36+
public void write(
37+
Map<String, Long> indicators,
38+
Class<?> testClass,
39+
String event,
40+
int testGroupNumber,
41+
int testNumber
42+
) {
43+
String timestamp = getTimestamp();
44+
StringBuilder line = new StringBuilder(timestamp)
45+
.append(",").append(testClass.getSimpleName())
46+
.append(",").append(event)
47+
.append(",").append(testGroupNumber)
48+
.append(",").append(testNumber);
49+
for (String header : headers) {
50+
Long value = indicators.get(header);
51+
if (value == null) {
52+
line.append(",");
53+
} else {
54+
line.append(",").append(value);
55+
}
56+
}
57+
out.println(line);
58+
}
59+
60+
@Override
61+
public void close() {
62+
out.close();
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
/*
9+
threads
10+
docker containers
11+
heap memory
12+
opened files
13+
opened sockets
14+
loaded classes
15+
CPU history
16+
*/
17+
/**
18+
*
19+
* @author Sergey Chernov
20+
*/
21+
public abstract class ResourceLeakageDetector {
22+
23+
private final List<String> indicatorKeys;
24+
25+
List<Class<?>> testClasses;
26+
27+
protected ResourceLeakageDetector(List<String> indicatorKeys) {
28+
this.indicatorKeys = Collections.unmodifiableList(new ArrayList<>(indicatorKeys));
29+
}
30+
31+
public final List<String> getIndicatorKeys() {
32+
return indicatorKeys;
33+
}
34+
35+
public abstract Map<String, Long> getIndicators();
36+
37+
public void handleBeforeClassGroup() {
38+
this.testClasses = new ArrayList<>();
39+
}
40+
41+
public void handleAfterClass(Class<?> testClass) {
42+
// testClasses can be null in case if a single test is executed
43+
if (this.testClasses != null) {
44+
this.testClasses.add(testClass);
45+
}
46+
}
47+
48+
public void handleAfterClassGroup() {
49+
this.testClasses = null;
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import java.io.Closeable;
4+
import java.util.Map;
5+
import java.util.concurrent.TimeUnit;
6+
7+
/**
8+
*
9+
* @author Sergey Chernov
10+
*/
11+
public abstract class ResourceLeakageLogWriter implements Closeable {
12+
13+
private final long startNanoTime = System.nanoTime();
14+
15+
public abstract void write(
16+
Map<String, Long> indicators,
17+
Class<?> testClass,
18+
String event,
19+
int testGroupNumber,
20+
int testNumber
21+
);
22+
23+
protected String getTimestamp() {
24+
long now = System.nanoTime();
25+
long totalSeconds = TimeUnit.NANOSECONDS.toSeconds(now - startNanoTime);
26+
27+
return formatTimestamp(totalSeconds);
28+
}
29+
30+
static String formatTimestamp(long totalSeconds) {
31+
long seconds = totalSeconds % 60;
32+
long minutes = (totalSeconds = totalSeconds / 60) % 60;
33+
long hours = totalSeconds / 60;
34+
return hours + ":" + String.format("%02d", minutes) + ":" + String.format("%02d", seconds);
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import org.springframework.lang.Nullable;
4+
5+
import java.io.File;
6+
import java.util.Arrays;
7+
import java.util.HashMap;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.concurrent.atomic.AtomicInteger;
11+
import java.util.stream.Collectors;
12+
13+
/**
14+
*
15+
* @author Sergey Chernov
16+
*/
17+
public class ResourceLeakageManager {
18+
19+
private final AtomicInteger testGroupNumber = new AtomicInteger();
20+
private final AtomicInteger testNumber = new AtomicInteger();
21+
22+
private final List<ResourceLeakageDetector> resourceLeakageDetectors;
23+
@Nullable
24+
private final ResourceLeakageLogWriter resourceLeakageLogWriter;
25+
26+
private static final ResourceLeakageManager instance = initInstance();
27+
28+
private static ResourceLeakageManager initInstance() {
29+
return new ResourceLeakageManager();
30+
}
31+
32+
private ResourceLeakageManager() {
33+
// todo service discovery with priority
34+
this(Arrays.asList(
35+
new ThreadsResourceLeakageDetector(),
36+
new HeapResourceLeakageDetector()
37+
));
38+
}
39+
40+
private ResourceLeakageManager(List<ResourceLeakageDetector> resourceLeakageDetectors) {
41+
/*@Nullable*/
42+
File reportsBaseDir = ResourceLeakageUtils.getReportsBaseDir();
43+
44+
this.resourceLeakageDetectors = resourceLeakageDetectors;
45+
if (reportsBaseDir == null) {
46+
this.resourceLeakageLogWriter = null;
47+
} else {
48+
File outputFile = new File(reportsBaseDir, "report.csv");
49+
List<String> headers = resourceLeakageDetectors.stream()
50+
.flatMap(detector -> detector.getIndicatorKeys().stream())
51+
.collect(Collectors.toList());
52+
resourceLeakageLogWriter = new ResourceLeakageCsvLogWriter(outputFile, headers);
53+
}
54+
}
55+
56+
public static ResourceLeakageManager getInstance() {
57+
return instance;
58+
}
59+
60+
public void handleBeforeClassGroup() {
61+
testGroupNumber.incrementAndGet();
62+
resourceLeakageDetectors.forEach(ResourceLeakageDetector::handleBeforeClassGroup);
63+
}
64+
65+
public void handleBeforeClass(Class<?> testClass) {
66+
testNumber.incrementAndGet();
67+
logIndicators(testClass, "BC");
68+
}
69+
70+
public void handleAfterClass(Class<?> testClass) {
71+
logIndicators(testClass, "AC");
72+
for (ResourceLeakageDetector resourceLeakageDetector : resourceLeakageDetectors) {
73+
resourceLeakageDetector.handleAfterClass(testClass);
74+
}
75+
}
76+
77+
public void handleAfterClassGroup(Class<?> testClass) {
78+
if (Boolean.getBoolean("testsmartcontext.handleAfterClassGroup.gc")) {
79+
System.gc();
80+
}
81+
logIndicators(testClass, "ACG");
82+
for (ResourceLeakageDetector resourceLeakageDetector : resourceLeakageDetectors) {
83+
resourceLeakageDetector.handleAfterClassGroup();
84+
}
85+
}
86+
87+
private void logIndicators(Class<?> testClass, String event) {
88+
if (resourceLeakageLogWriter != null) {
89+
Map<String, Long> indicators = new HashMap<>();
90+
resourceLeakageDetectors.forEach(detector -> indicators.putAll(detector.getIndicators()));
91+
resourceLeakageLogWriter.write(indicators, testClass, event,
92+
testGroupNumber.get(), testNumber.get());
93+
}
94+
}
95+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import java.io.File;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.springframework.lang.Nullable;
7+
8+
/**
9+
*
10+
* @author Sergey Chernov
11+
*/
12+
final class ResourceLeakageUtils {
13+
14+
private static final Logger logger = LoggerFactory.getLogger(ResourceLeakageUtils.class);
15+
16+
@Nullable
17+
static File getReportsBaseDir() {
18+
// todo target
19+
// "basedir" is provided by Maven
20+
String basedirProperty = System.getProperty("basedir");
21+
if (basedirProperty == null) {
22+
return null;
23+
}
24+
25+
File basedir = new File(basedirProperty, "leakage-detector");
26+
if ((basedir.mkdir() || basedir.exists()) && basedir.isDirectory()) {
27+
return basedir;
28+
}
29+
logger.warn("Failed to create {}", basedir);
30+
return null;
31+
}
32+
}

0 commit comments

Comments
 (0)