13

I realize it's a chicken and egg problem and that it's not possible to accurately resolve the time it took to render a page (or the size of response) and insert that number into the page itself without affecting either measure. Nevertheless, I'm looking for a way to insert either number partially in a page of a JSF/Facelets/Seam application.

E.g., at the bottom of a .jsf page somewhere:

<!-- page size: 10.3Kb -->
<!-- render time: 0.2s -->

I've come across JSFUnit's JSFTimer, which is really handy. However, the phase listener approach doesn't allow the results of RENDER_RESPONSE phase to be inserted into the page. Not sure how to access the size of the response encoded so far either.

Is there a quick and dirty way to hook up to some sort of post-processing event at or after the end of RENDER_RESPONSE and to inject both numbers into the page about to be rendered? One way of approaching this is perhaps through servlet filters, but I'm looking for something simpler; perhaps a trick with Seam or Facelets...

Thanks,
-A

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
anikitin
  • 163
  • 1
  • 8

3 Answers3

23

This is a perfect use case for the Apache Commons IO CountingOutputStream. You need to create a Filter which uses HttpServletResponseWrapper to replace the OutputStream of the response with this one and replaces the Writer as well which should wrap the wrapped OutputStream. Then get hold of the HttpServletResponseWrapper instance in the request scope so that you can get the getByteCount() from the CountingOutputStream.

Here's a kickoff example of the CountingFilter:

public class CountingFilter implements Filter {

    @Override
    public void init(FilterConfig arg0) throws ServletException {
        // NOOP.
    }

    @Override
    public void doFilter(ServletRequest request, final ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse httpres = (HttpServletResponse) response;
        CountingServletResponse counter = new CountingServletResponse(httpres);
        HttpServletRequest httpreq = (HttpServletRequest) request;
        httpreq.setAttribute("counter", counter);
        chain.doFilter(request, counter);
        counter.flushBuffer(); // Push the last bits containing HTML comment.
    }

    @Override
    public void destroy() {
        // NOOP.
    }

}

The CountingServletResponse:

public class CountingServletResponse extends HttpServletResponseWrapper {

    private final long startTime;
    private final CountingServletOutputStream output;
    private final PrintWriter writer;

    public CountingServletResponse(HttpServletResponse response) throws IOException {
        super(response);
        startTime = System.nanoTime();
        output = new CountingServletOutputStream(response.getOutputStream());
        writer = new PrintWriter(output, true);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return output;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return writer;
    }

    @Override
    public void flushBuffer() throws IOException {
        writer.flush();
    }

    public long getElapsedTime() {
        return System.nanoTime() - startTime;
    }

    public long getByteCount() throws IOException {
        flushBuffer(); // Ensure that all bytes are written at this point.
        return output.getByteCount();
    }

}

The CountingServletOutputStream:

public class CountingServletOutputStream extends ServletOutputStream {

    private final CountingOutputStream output;

    public CountingServletOutputStream(ServletOutputStream output) {
        this.output = new CountingOutputStream(output);
    }

    @Override
    public void write(int b) throws IOException {
        output.write(b);
    }

    @Override
    public void flush() throws IOException {
        output.flush();
    }

    public long getByteCount() {
        return output.getByteCount();
    }

}

You can use it in any (even non-JSF) page as follows:

<!DOCTYPE html>
<html 
    xmlns="http://www.w3.org/1999/xhtml" 
    xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <title>Counting demo</title>
    </h:head>
    <h:body>
        <h1>Hello World</h1>
    </h:body>
</html>
<!-- page size: #{counter.byteCount / 1000}KB -->
<!-- render time: #{counter.elapsedTime / 1000000}ms -->
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • As much as I wanted to avoid filters, this looks like the best approach. Thanks for suggesting! Here are some things to be aware of: 1) with Seam, filters are pretty easy to setup with annotations, e.g.: @Scope(APPLICATION) @Name("org.myorg.countingFilter") @BypassInterceptors @Filter(around = "org.jboss.seam.web.ajax4jsfFilter") CountingFilter 2) due to chicken/egg problem noted above the numbers aren't completely accurate. Under JSF, the ViewState is left out of the byteCount, which may be a significant number. Same applies to render time. Both are good enough for a rough picture though. – anikitin Jul 12 '10 at 19:03
  • Great stuff again. However, I have several postback requests, which are generating several responses. How do I find the correct response, that is the one that I meant to profile? – Kawu Feb 28 '13 at 18:33
  • @Kawu: Alter the URL pattern of the filter accordingly so that it only runs on the desired URLs. – BalusC Feb 28 '13 at 18:34
  • What about partial (AJAX) requests/responses? It seems to show the right one, but I want to make sure it's not coincidence. – Kawu Feb 28 '13 at 21:44
  • @Kawu: Ajax requests are identified by presence of `Faces-Request` header with value of `Partial/Ajax`. – BalusC Feb 28 '13 at 23:52
  • Beware, `writer = new PrintWriter(output, true);` does use the system's default encoding. Change it to `writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output, "UTF-8")), true);` to always use `UTF-8` or whatever you need. – Tim Büthe Feb 06 '14 at 14:04
  • Where do you write the string "page size?" – GC_ Feb 14 '14 at 20:19
  • @Grae: At the bottom of the JSF page, exactly as shown in the last code snippet. – BalusC Feb 15 '14 at 08:29
  • @BalusC I didn't know you could do that. – GC_ Feb 18 '14 at 20:30
  • Should this work with JSPs too, or is this facelets only? Maybe because of the time EL expressions get evaluated? Because in my case, I allways get a byteCount of 0. – Tim Büthe Mar 11 '14 at 14:40
  • If I understand correctly I need to register this filter in web.xml, am I right? – Anatoly Oct 24 '14 at 11:50
  • servlet 3.1 has two more abstract method to implement for ServletOutputStream. I think the code above needs to be updated. – fer.marino Nov 10 '15 at 13:15
2

I wrote a blog post explaining how you could create an Interceptor that would measure each method call your seam compontents where using.

You can find the blog post here. You need to scroll down to the second part.

Basically, all you need to do is annotate the method you want to measure with @MeasureCalls and it will automatically be picked up by the interceptor

@Name("fooBean")
@MeasureCalls
public class FooBean

An output would be something like this, showing the time it took in milliseconds and how many times each method was called:

284.94 ms   1   FooBean.getRandomDroplets()
284.56 ms   1   GahBean.getRandomDroplets()
201.60 ms   2   SohBean.searchRatedDoodlesWithinHead()
185.94 ms   1   FroBean.doSearchPopular()
157.63 ms   1   FroBean.doSearchRecent()
 42.34 ms   1   FooBean.fetchMostRecentYodel()
 41.94 ms   1   GahBean.getMostRecentYodel()
 15.89 ms   1   FooBean.getNoOfYodels()
 15.00 ms   1   GahBean.getNoOfYodels()
  9.14 ms   1   SohBean.mainYodels()
  1.11 ms   2   SohBean.trackHoorayEvent()
  0.32 ms   1   FroBean.reset()
  0.22 ms  43   NohBean.thumbPicture()
  0.03 ms  18   FooBean.getMostRecentYodels()
  0.01 ms   1   NohBean.profilePicture()
  0.01 ms   1   FroBean.setToDefault()
  0.01 ms   1   FroBean.getRecentMarker() 
Shervin Asgari
  • 23,901
  • 30
  • 103
  • 143
  • Looks handy. Hint for your `toString()`: there's a `Class#getSimpleName()` and `String#format()`. – BalusC Jan 22 '11 at 20:05
  • @Shervin, thank you for your solution. I tried to use it with cdi beans (no Seam framework) and it almost works but I can't understand how you display statistics when loading finished? Or you constantly print in console renewed console? Is there way to found out when loading of multiple page application is completed? Thank you. – Anatoly Oct 25 '14 at 12:23
0

Or have an asynch Javascript call that gets the response time and size from the server after its ready? Treat it like a callback to be performed after the page is done loading and the values are ready to insert.

duffymo
  • 305,152
  • 44
  • 369
  • 561