Skip to content

Commit b24be7c

Browse files
jvangaalenvlsi
authored andcommitted
Fix memory leak when transaction has parents
Improve sampler cleanup during thread run
1 parent 1f17592 commit b24be7c

2 files changed

Lines changed: 109 additions & 2 deletions

File tree

src/core/src/main/java/org/apache/jmeter/samplers/SampleResult.java

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
import org.apache.jmeter.assertions.AssertionResult;
3333
import org.apache.jmeter.gui.Searchable;
3434
import org.apache.jmeter.testelement.TestPlan;
35+
import org.apache.jmeter.threads.JMeterContext;
36+
import org.apache.jmeter.threads.JMeterContextService;
3537
import org.apache.jmeter.threads.JMeterContext.TestLogicalAction;
38+
import org.apache.jmeter.control.TransactionSampler;
3639
import org.apache.jmeter.util.JMeterUtils;
3740
import org.apache.jorphan.util.JOrphanUtils;
3841
import org.apache.jorphan.util.StringUtilities;
@@ -1595,10 +1598,62 @@ public void setStartNextThreadLoop(boolean startNextThreadLoop) {
15951598
}
15961599

15971600
/**
1598-
* Clean up cached data
1601+
* Clean up cached data, but only if this is a root result (no parent)
1602+
* and not part of a parent transaction.
15991603
*/
16001604
public void cleanAfterSample() {
1605+
// Only clean if this is a root result (no parent)
1606+
if (parent != null) {
1607+
return;
1608+
}
1609+
1610+
// Check if we're part of a parent transaction by walking up the sampler hierarchy
1611+
JMeterContext context = JMeterContextService.getContext();
1612+
if (context != null) {
1613+
Sampler currentSampler = context.getCurrentSampler();
1614+
if (currentSampler instanceof TransactionSampler) {
1615+
TransactionSampler transSampler = (TransactionSampler) currentSampler;
1616+
// Get the parent sampler from the transaction
1617+
Sampler parentSampler = transSampler.getSubSampler();
1618+
// If there's a parent sampler and it's a transaction, we're nested
1619+
if (parentSampler instanceof TransactionSampler) {
1620+
return;
1621+
}
1622+
}
1623+
}
1624+
1625+
cleanRecursively();
1626+
}
1627+
1628+
/**
1629+
* Internal method to clean this result and all its sub-results
1630+
*/
1631+
private void cleanRecursively() {
1632+
// Clean sub-results first
1633+
if (subResults != null) {
1634+
for (SampleResult subResult : subResults) {
1635+
if (subResult != null) {
1636+
subResult.cleanRecursively();
1637+
}
1638+
}
1639+
subResults.clear();
1640+
subResults = null;
1641+
}
1642+
1643+
// Clean assertion results
1644+
if (assertionResults != null) {
1645+
assertionResults.clear();
1646+
assertionResults = null;
1647+
}
1648+
1649+
// Clear only memory-heavy data and caches, preserve samplerData
1650+
this.parent = null;
16011651
this.responseDataAsString = null;
1652+
this.responseData = EMPTY_BA;
1653+
this.responseHeaders = "";
1654+
this.requestHeaders = "";
1655+
this.samplerData = null;
1656+
16021657
}
16031658

16041659
@Override
@@ -1666,4 +1721,50 @@ public TestLogicalAction getTestLogicalAction() {
16661721
public void setTestLogicalAction(TestLogicalAction testLogicalAction) {
16671722
this.testLogicalAction = testLogicalAction;
16681723
}
1724+
1725+
/**
1726+
* Create a deep copy for async listeners
1727+
* @return A deep copy of this result
1728+
*/
1729+
public SampleResult cloneForListeners() {
1730+
// Create clone with the correct type
1731+
SampleResult clone;
1732+
try {
1733+
// Use the same constructor that was used to create this instance
1734+
clone = this.getClass().getConstructor(this.getClass()).newInstance(this);
1735+
} catch (Exception e) {
1736+
// Fallback to base class if constructor is not available
1737+
log.debug("Could not create clone with type: " + this.getClass().getName() + ", using base class", e);
1738+
clone = new SampleResult(this);
1739+
}
1740+
1741+
// Deep copy mutable fields that the copy constructor doesn't handle deeply
1742+
if (responseData != EMPTY_BA) {
1743+
clone.responseData = responseData.clone();
1744+
}
1745+
1746+
// Deep copy subResults
1747+
if (subResults != null) {
1748+
clone.subResults = new ArrayList<>(subResults.size());
1749+
for (SampleResult sub : subResults) {
1750+
SampleResult subClone = sub.cloneForListeners();
1751+
subClone.setParent(clone);
1752+
clone.subResults.add(subClone);
1753+
}
1754+
}
1755+
1756+
// Deep copy assertion results
1757+
if (assertionResults != null) {
1758+
clone.assertionResults = new ArrayList<>(assertionResults.size());
1759+
for (AssertionResult assertionResult : assertionResults) {
1760+
clone.assertionResults.add(assertionResult); // AssertionResult is immutable
1761+
}
1762+
}
1763+
1764+
// Clear only the caches and unnecessary references in the clone
1765+
clone.responseDataAsString = null;
1766+
clone.parent = null; // Parent reference not needed in the clone
1767+
1768+
return clone;
1769+
}
16691770
}

src/core/src/main/java/org/apache/jmeter/threads/JMeterThread.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,12 @@ private SampleResult doEndTransactionSampler(
672672
notifyListeners(transactionPack.getSampleListeners(), transactionResult);
673673
}
674674
compiler.done(transactionPack);
675+
676+
// Clean up the transaction result after listeners have processed it
677+
if (transactionResult != null) {
678+
transactionResult.cleanAfterSample();
679+
}
680+
675681
return transactionResult;
676682
}
677683

@@ -1028,7 +1034,7 @@ void notifyTestListeners() {
10281034
}
10291035

10301036
private void notifyListeners(List<SampleListener> listeners, SampleResult result) {
1031-
SampleEvent event = new SampleEvent(result, threadGroup.getName(), threadVars);
1037+
SampleEvent event = new SampleEvent(result.cloneForListeners(), threadGroup.getName(), threadVars);
10321038
notifier.notifyListeners(event, listeners);
10331039
}
10341040

0 commit comments

Comments
 (0)