Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Memory testing utilities #3

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,18 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.21.0</version>
<configuration>
<systemPropertyVariables>
<heapdump.path>${project.build.directory}/heapdump-%s.hprof</heapdump.path>
</systemPropertyVariables>
<forkMode>always</forkMode>
<argLine>-ea -XX:+UnlockDiagnosticVMOptions</argLine>
</configuration>
</plugin>
</plugins>
</build>


</project>
17 changes: 13 additions & 4 deletions src/main/java/com/test/MainUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.vaadin.ui.Button;
import com.vaadin.ui.Grid;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.TextField;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
Expand All @@ -24,12 +25,13 @@ public class MainUI extends UI {
final TextField filter;

private final Button addNewBtn;
private final Button memoryLeak;

@Override
protected void init(VaadinRequest request) {

// build layout
HorizontalLayout actions = new HorizontalLayout(filter, addNewBtn);
HorizontalLayout actions = new HorizontalLayout(filter, addNewBtn, memoryLeak);
VerticalLayout verticalLayout = new VerticalLayout(actions, grid, editor);
verticalLayout.setSizeFull();
setContent(verticalLayout);
Expand All @@ -50,9 +52,7 @@ protected void init(VaadinRequest request) {
filter.addValueChangeListener(e -> listCustomers(e.getValue()));

// Connect selected Customer to editor or hide if none is selected
grid.asSingleSelect().addValueChangeListener(e -> {
editor.editCustomer(e.getValue());
});
grid.asSingleSelect().addValueChangeListener(e -> editor.editCustomer(e.getValue()));

// Instantiate and edit new Customer the new button is clicked
addNewBtn.addClickListener(e -> editor.editCustomer(new Customer("", "")));
Expand All @@ -65,6 +65,14 @@ protected void init(VaadinRequest request) {

// Initialize listing
listCustomers(null);

// Add a component with a listener without adding it to the design
memoryLeak.addClickListener(e ->
{
Label label = new Label();
addNewBtn.addClickListener(btnClick -> label.setValue(label.getValue() + "Clicked again!"));
});

}

public void listCustomers(String filterText) {
Expand All @@ -82,5 +90,6 @@ public MainUI(CustomerRepository repo, CustomerEditor editor) {
this.grid = new Grid<>(Customer.class);
this.filter = new TextField();
this.addNewBtn = new Button("New customer", VaadinIcons.PLUS);
this.memoryLeak = new Button("Memory Leak", VaadinIcons.BOMB);
}
}
76 changes: 74 additions & 2 deletions src/test/java/com/test/ApplicationTests.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
package com.test;

import com.github.karibu.testing.MockVaadin;
import com.test.utils.HeapDump;
import com.test.utils.HeapInfo;
import com.test.utils.MemoryLeakFailure;
import com.test.utils.SingletonBeanStoreRetrievalStrategy;
import com.vaadin.data.provider.Query;
import com.vaadin.spring.internal.UIScopeImpl;
import com.vaadin.ui.Button;
import com.vaadin.ui.Grid;
import com.vaadin.ui.TextField;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import java.util.function.Predicate;
import java.util.stream.Stream;

import static com.github.karibu.testing.LocatorJ.*;
import static com.github.karibu.testing.LocatorJ._click;
import static com.github.karibu.testing.LocatorJ._get;
import static com.github.karibu.testing.LocatorJ._setValue;
import static org.junit.Assert.assertTrue;

@RunWith(SpringRunner.class)
Expand All @@ -31,6 +39,7 @@ public class ApplicationTests {

@Autowired
private BeanFactory beanFactory;
private final Predicate<String> classesToWatch = name -> name.contains("vaadin") || name.startsWith("com.test.");

@Before
public void setup() {
Expand All @@ -52,4 +61,67 @@ public void createNewCustomer() {
Stream<Customer> customerStream = grid.getDataProvider().fetch(new Query<>());
assertTrue(customerStream.map(Customer::getFirstName).anyMatch("Halk"::equals));
}

@Test(expected = MemoryLeakFailure.class)
public void testMemoryLeak() {
Button memoryLeak = _get(Button.class, spec -> spec.withCaption("Memory Leak"));
_click(memoryLeak);

HeapInfo.tryGC();
HeapInfo heapInfo1 = new HeapInfo().classStatistics(classesToWatch);

_click(memoryLeak);
_click(memoryLeak);
_click(memoryLeak);
_click(memoryLeak);
_click(memoryLeak);
_click(memoryLeak);

HeapInfo.tryGC();
HeapInfo heapInfo2 = new HeapInfo().classStatistics(classesToWatch);
HeapInfo delta = heapInfo2.delta(heapInfo1);

if (delta.values().stream()
.map(HeapInfo.ClassHeapInfo::getClassName)
.anyMatch(s -> s.startsWith("com.vaadin.ui."))) {
LoggerFactory.getLogger(this.getClass()).error(delta.toString());
throw new MemoryLeakFailure("Memory Leak Detected: " + delta.toString(System.lineSeparator()));
}
}

@Test
public void createNew2Customers() {

_click(_get(Button.class, spec -> spec.withCaption("New customer")));
_setValue(_get(TextField.class, spec -> spec.withCaption("First name")), "Halk");
_click(_get(Button.class, spec -> spec.withCaption("Save")));

HeapInfo.tryGC();
HeapInfo heapInfo1 = new HeapInfo().classStatistics(classesToWatch);

_click(_get(Button.class, spec -> spec.withCaption("New customer")));
_setValue(_get(TextField.class, spec -> spec.withCaption("First name")), "Van Helsing");
_click(_get(Button.class, spec -> spec.withCaption("Save")));

HeapInfo.tryGC();
HeapInfo heapInfo2 = new HeapInfo().classStatistics(classesToWatch);

System.out.println("****** Class usage differences START *****");
HeapInfo delta = heapInfo2.delta(heapInfo1);
System.out.println(delta.toString(System.lineSeparator()));
System.out.println("******* Class usage differences END ******");

Grid<Customer> grid = _get(Grid.class);
Stream<Customer> customerStream = grid.getDataProvider().fetch(new Query<>());
assertTrue(customerStream.map(Customer::getFirstName).anyMatch("Halk"::equals));
customerStream = grid.getDataProvider().fetch(new Query<>());
assertTrue(customerStream.map(Customer::getFirstName).anyMatch("Van Helsing"::equals));
}

@AfterClass
public static void heapDump() {

HeapDump.heapDump(ApplicationTests.class);
}

}
79 changes: 79 additions & 0 deletions src/test/java/com/test/utils/HeapDump.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.test.utils;

import com.sun.management.HotSpotDiagnosticMXBean;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.LoggerFactory;
import org.springframework.data.util.Lazy;

import javax.management.MBeanServer;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;

public class HeapDump implements TestRule {
public static final String HEAPDUMP_PATH = "heapdump.path";
private static final String HOTSPOT_DIAGNOSTICS_BEAN_NAME =
"com.sun.management:type=HotSpotDiagnostic";

public HeapDump() {

}

public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
base.evaluate();
heapDump(description.getTestClass());
}
};
}


private static final Lazy<HotSpotDiagnosticMXBean> diagnosticMXBean =
new Lazy<>(() ->
{
try {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
return ManagementFactory.newPlatformMXBeanProxy(server, HOTSPOT_DIAGNOSTICS_BEAN_NAME, HotSpotDiagnosticMXBean.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
);


public static void heapDump(Class clazz) {
heapDump(clazz.getSimpleName());
}

public static void heapDump(String qualifier) {
String dumpLocation = System.getProperty(HEAPDUMP_PATH);
if (dumpLocation != null && !dumpLocation.isEmpty()) {
dumpLocation = String.format(dumpLocation, qualifier);
System.out.println("dumpLocation = " + dumpLocation);
heapDumpToFile(dumpLocation);
} else {
LoggerFactory.getLogger(HeapDump.class).info("Property \"" + HEAPDUMP_PATH + "\"%s is not defined, heap dump skipped");
}

}

public static void heapDumpToFile(String dumpLocation) {
try {
System.gc();
Thread.sleep(300);
System.gc();
//noinspection ResultOfMethodCallIgnored
new File(dumpLocation).delete();
LoggerFactory.getLogger(HeapDump.class).info("Dumping heap to {}", dumpLocation);
diagnosticMXBean.get().dumpHeap(dumpLocation, true);
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}


}
Loading