The server side needs to know the client has removed the connectors to be able to do cleanup Change-Id: Ic3d41cc5cbab035a53bf5c99496d74858c376e73tags/7.7.0.alpha3
json.getValueMap("dd")); | json.getValueMap("dd")); | ||||
} | } | ||||
unregisterRemovedConnectors(connectorHierarchyUpdateResult.detachedConnectorIds); | |||||
int removed = unregisterRemovedConnectors(connectorHierarchyUpdateResult.detachedConnectorIds); | |||||
if (removed > 0 && !isResponse(json)) { | |||||
// Must acknowledge the removal using an XHR or server | |||||
// memory usage will keep growing | |||||
getUIConnector().sendAck(); | |||||
} | |||||
getLogger() | getLogger() | ||||
.info("handleUIDLMessage: " | .info("handleUIDLMessage: " | ||||
+ (Duration.currentTimeMillis() - processUidlStart) | + (Duration.currentTimeMillis() - processUidlStart) | ||||
Profiler.leave("verifyConnectorHierarchy - this is only performed in debug mode"); | Profiler.leave("verifyConnectorHierarchy - this is only performed in debug mode"); | ||||
} | } | ||||
private void unregisterRemovedConnectors( | |||||
private int unregisterRemovedConnectors( | |||||
FastStringSet detachedConnectors) { | FastStringSet detachedConnectors) { | ||||
Profiler.enter("unregisterRemovedConnectors"); | Profiler.enter("unregisterRemovedConnectors"); | ||||
JsArrayString detachedArray = detachedConnectors.dump(); | JsArrayString detachedArray = detachedConnectors.dump(); | ||||
for (int i = 0; i < detachedArray.length(); i++) { | |||||
int nrDetached = detachedArray.length(); | |||||
for (int i = 0; i < nrDetached; i++) { | |||||
ServerConnector connector = getConnectorMap().getConnector( | ServerConnector connector = getConnectorMap().getConnector( | ||||
detachedArray.get(i)); | detachedArray.get(i)); | ||||
verifyConnectorHierarchy(); | verifyConnectorHierarchy(); | ||||
} | } | ||||
getLogger().info( | |||||
"* Unregistered " + detachedArray.length() | |||||
+ " connectors"); | |||||
getLogger() | |||||
.info("* Unregistered " + nrDetached + " connectors"); | |||||
Profiler.leave("unregisterRemovedConnectors"); | Profiler.leave("unregisterRemovedConnectors"); | ||||
return nrDetached; | |||||
} | } | ||||
private JsArrayString createConnectorsIfNeeded(ValueMap json) { | private JsArrayString createConnectorsIfNeeded(ValueMap json) { |
private static Logger getLogger() { | private static Logger getLogger() { | ||||
return Logger.getLogger(UIConnector.class.getName()); | return Logger.getLogger(UIConnector.class.getName()); | ||||
} | } | ||||
/** | |||||
* Send an acknowledgement RPC to the server. This allows the server to know | |||||
* which messages the client has received, even when the client is not | |||||
* sending any other traffic. | |||||
*/ | |||||
public void sendAck() { | |||||
getRpcProxy(UIServerRpc.class).acknowledge(); | |||||
} | |||||
} | } |
public void poll() { | public void poll() { | ||||
fireEvent(new PollEvent(UI.this)); | fireEvent(new PollEvent(UI.this)); | ||||
} | } | ||||
@Override | |||||
public void acknowledge() { | |||||
// Nothing to do, just need the message to be sent and processed | |||||
} | |||||
}; | }; | ||||
private DebugWindowServerRpc debugRpc = new DebugWindowServerRpc() { | private DebugWindowServerRpc debugRpc = new DebugWindowServerRpc() { | ||||
@Override | @Override |
* should always be called to ensure the message is flushed right away. | * should always be called to ensure the message is flushed right away. | ||||
*/ | */ | ||||
public void poll(); | public void poll(); | ||||
@NoLoadingIndicator | |||||
public void acknowledge(); | |||||
} | } |
package com.vaadin.tests.push; | |||||
import java.util.concurrent.Executors; | |||||
import java.util.concurrent.ScheduledExecutorService; | |||||
import java.util.concurrent.ScheduledFuture; | |||||
import java.util.concurrent.TimeUnit; | |||||
import org.apache.commons.lang.SerializationUtils; | |||||
import com.vaadin.annotations.Push; | |||||
import com.vaadin.data.Property.ValueChangeEvent; | |||||
import com.vaadin.data.Property.ValueChangeListener; | |||||
import com.vaadin.server.VaadinRequest; | |||||
import com.vaadin.tests.components.AbstractTestUIWithLog; | |||||
import com.vaadin.ui.AbstractOrderedLayout; | |||||
import com.vaadin.ui.Button; | |||||
import com.vaadin.ui.Button.ClickEvent; | |||||
import com.vaadin.ui.Button.ClickListener; | |||||
import com.vaadin.ui.CheckBox; | |||||
import com.vaadin.ui.HorizontalLayout; | |||||
import com.vaadin.ui.Label; | |||||
@Push | |||||
public class PushRemoveConnectors extends AbstractTestUIWithLog { | |||||
private transient final ScheduledExecutorService threadPool = Executors | |||||
.newScheduledThreadPool(5); | |||||
static final String START = "start"; | |||||
static final String STOP = "stop"; | |||||
private AbstractOrderedLayout verticalLayout; | |||||
private transient ScheduledFuture<?> task = null; | |||||
@Override | |||||
protected void setup(VaadinRequest request) { | |||||
final CheckBox pollingEnabled = new CheckBox("Polling enabled"); | |||||
pollingEnabled.addValueChangeListener(new ValueChangeListener() { | |||||
@Override | |||||
public void valueChange(ValueChangeEvent event) { | |||||
setPollInterval(pollingEnabled.getValue() ? 1000 : -1); | |||||
} | |||||
}); | |||||
Button start = new Button("start"); | |||||
start.setId(START); | |||||
start.addClickListener(new ClickListener() { | |||||
@Override | |||||
public void buttonClick(ClickEvent event) { | |||||
task = threadPool.scheduleAtFixedRate(new Runnable() { | |||||
@Override | |||||
public void run() { | |||||
access(new Runnable() { | |||||
public void run() { | |||||
populate(); | |||||
log("Serialized session size: " | |||||
+ getSessionSize()); | |||||
} | |||||
}); | |||||
} | |||||
}, 1, 1, TimeUnit.SECONDS); | |||||
} | |||||
}); | |||||
Button stop = new Button("stop"); | |||||
stop.setId(STOP); | |||||
stop.addClickListener(new ClickListener() { | |||||
@Override | |||||
public void buttonClick(ClickEvent event) { | |||||
if (task != null) { | |||||
task.cancel(true); | |||||
task = null; | |||||
} | |||||
} | |||||
}); | |||||
verticalLayout = new HorizontalLayout(); | |||||
populate(); | |||||
addComponents(pollingEnabled, start, stop, verticalLayout); | |||||
} | |||||
private void populate() { | |||||
verticalLayout.removeAllComponents(); | |||||
for (int i = 0; i < 500; i++) { | |||||
Label l = new Label("."); | |||||
l.setSizeUndefined(); | |||||
verticalLayout.addComponent(l); | |||||
} | |||||
} | |||||
private int getSessionSize() { | |||||
return SerializationUtils.serialize(getSession()).length; | |||||
} | |||||
} |
package com.vaadin.tests.push; | |||||
import org.junit.Assert; | |||||
import org.junit.Test; | |||||
import com.vaadin.testbench.elements.ButtonElement; | |||||
import com.vaadin.tests.tb3.SingleBrowserTest; | |||||
public class PushRemoveConnectorsTest extends SingleBrowserTest { | |||||
@Test | |||||
public void testNoMemoryLeak() throws InterruptedException { | |||||
openTestURL(); | |||||
$(ButtonElement.class).id(PushRemoveConnectors.START).click(); | |||||
Thread.sleep(5000); | |||||
int last = getMemoryUsage(); | |||||
int i = 0; | |||||
while (i++ < 10) { | |||||
Thread.sleep(5000); | |||||
int now = getMemoryUsage(); | |||||
System.out.println("Memory usage: "+now); | |||||
if (last == now) | |||||
break; | |||||
last = now; | |||||
} | |||||
$(ButtonElement.class).id(PushRemoveConnectors.STOP).click(); | |||||
Assert.assertNotEquals(10, i); | |||||
} | |||||
private int getMemoryUsage() { | |||||
return Integer.parseInt(getLogRow(0).replaceFirst( | |||||
".*Serialized session size: ", "")); | |||||
} | |||||
} |