brush clojure

Thursday, October 23, 2008

Measuring user session size with HtmlUnit and remote JMX

Keeping the user session small is a very important requirement for the application I am working on. The application architecture has tried to keep the size as small as possible, but how big is it eventually? The best way to know is to measure how much memory each new session costs. This turned out pretty straightforward with standard Java and open source tools:
- HtmlUnit provides a compact Web client. With little programming you can emulate some user action. An example follows which logs a new user.
- Start the Application Server in a separate JVM. Allow for remote JMX Monitoring.
- Start thousands of new sessions en follow what happens to memory usage in the server JVM using MemoryMXBean. JConsole is the easiest way to monitor the memory usage, but if you want to do some calculations or logging you'll have to gain access to the remote MXBean programmatically.

The method is independent of any server technology. You don't even need the application sources. A pic to illustrate what is going on:



and some implementation details:

Start the server and allow for remote JMX monitoring
Add the following to the java arguments:

-Dcom.sun.management.jmxremote.port=1444
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false


Program a simple web client to perform some user action

HtmlUnit's Webclient is a beauty. Here's an example of logging in in the Pluto portlet container an clicking on a link:


public void loginAndStartUseCase() throws Exception {
final WebClient webClient = new WebClient();
HtmlPage page = (HtmlPage) webClient.getPage("http://localhost:8080/pluto/portal/MyPage");
List forms = page.getForms();
boolean loginFormSubmitted = false;
for (HtmlForm htmlForm : forms) {
if (htmlForm.getActionAttribute().equals("j_security_check")) {
htmlForm.getInputByName("j_username").setValueAttribute("pluto");
htmlForm.getInputByName("j_password").setValueAttribute("pluto");
htmlForm.submit(htmlForm.getInputByName("login"));
loginFormSubmitted = true;
break;
}
}
if (!loginFormSubmitted) {
throw new RuntimeException("Login Form expected here");
}
page = (HtmlPage) webClient.getPage("http://localhost:8080/pluto/portal/MyPage");
page = clickLink(page, "MyUseCase");

page.getAllHtmlChildElements();
}

private static HtmlPage clickLink(HtmlPage page, String linkText) throws IOException {
LOG.debug("click " + linkText);
List anchors = page.getAnchors();
for (HtmlAnchor htmlAnchor : anchors) {
if (htmlAnchor.getFirstChild().asText().equals(linkText)) {
return (HtmlPage) htmlAnchor.click();
}
}
throw new RuntimeException("No link " + linkText);
}


Obtain a remote memory monitoring MXBean proxy

private static Object getMXBean(String beanType, Class beanClass, int port) throws MalformedURLException,
IOException, MalformedObjectNameException {
JMXServiceURL u = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:" + port + "/jmxrmi");
JMXConnector jmxConnector = JMXConnectorFactory.connect(u);
MBeanServerConnection beanServerConnection = jmxConnector.getMBeanServerConnection();

Hashtable props = new Hashtable();
props.put("type", beanType);
ObjectName on = new ObjectName("java.lang", props);

Set found = beanServerConnection.queryMBeans(on, null);
if (found.size() != 1) {
throw new RuntimeException("1 bean expected");
}

ObjectInstance objectInstance = found.iterator().next();

Object mxBean = ManagementFactory.newPlatformMXBeanProxy(beanServerConnection, objectInstance.getObjectName()
.toString(), beanClass);
return mxBean;
}


Start measuring!

Now you can start a new user session (or 10, or 1000), measure the memory impact and calculate whatever you want


MemoryMXBean memoryBean = (MemoryMXBean) getMXBean("Memory", MemoryMXBean.class, 1444);
for (many times) {
for(many_times) { loginAndStartUseCase(); }
memoryBean.gc();
long used = memoryBean.getHeapMemoryUsage().getUsed();
. . .



That's what my memory usage looks like after 10000 sessions:



This is an Excel chart built from comma-separated logging output.

The blue line shows the total memory usage - it usage grows consistently, it looks like no sessions have been released. The red line shows the calculated average session size. The average is pretty unreliable in the begin but stabilizes after a while - the cost of a new user just logging in and starting the use case seems to be around 3k.

Few remarks:
- Programming the Html WebClient for a more complex use case can be time consuming. Is there a good scripting alternative? Selenium was great, but starting a real browser for each user new session is not an option
- Make sure no sessions are released during the measurement. The test goes pretty fast, a normal session timeout of 15-30 min will be more than enough to reach your users limit.

2 comments:

  1. Glad to hear that you've found HtmlUnit to be useful. Feel free to submit suggestions to the HtmlUnit team for making the API less verbose, or if you know (or want to learn) Ruby, you might be able to use Celerity, which is a JRuby wrapper around HtmlUnit which provides a Watir-like API. Take care!

    ReplyDelete
  2. Thanks for the comment, I didn't know about Watir.

    The point is, I don't use HtmlUnit here for tests, but as a *minimal* GUI-less browser to drive the application to a certain state, not to test it. I am not interested in the application output.

    Watir doesn't sound like an interesting option in the scenario when I want to start 10,000 sessions, even if it was a better option than Selenium for some reason.

    That's not the first time I use HtmlUnit for something else than testing - it is great for Html scraping. A beautiful minimal GUI-less browser.

    ReplyDelete