December 18, 2012

IntelliJ and General Development on a Windows 7 Corporate Laptop...

The more development I do with IntelliJ, the more attached I become to it. It works well with my development style and I don't mind the licence costs as a result.

I recently started working on a new project with a company and discovered that they were in the middle of a project to move all its developers over to using a new laptop build based on Windows 7.

They have a great team working on this but they were relatively inexperienced at dealing with developers requirements so they needed some guinea pigs and our team fitted the bill nicely.

When the proposed laptop was first trialled a misconfiguration of the anti-virus meant that they were very slow, as a result our boss was able to demand SSDs... Which was nice. I've been running SSDs on my home development machines to provide the best possible build performance though I always make sure my machines and repositories are backed up to spinning rust.

The laptops with SSDs finally arrived and it turned out that I was the first of the development focused guys to get our hands on it.

Some clever tooling meant that while none of us developers had admin privileges, there was a clear and sensible route for installation of any software that we required. The technique involved meant that temporary admin privileges were given to the install process which meant that our normal users were unable to alter the installed files in any way. This had several implications for the installation of software, IntelliJ in particular.

I encountered a number of teething problems - the first one was that our user profiles were configured to be read from a network share. Fine as long as we were plugged into our head office networks, less so when we were on the road. The most visible problem was the corporate software provisioning tools having a snit fit and not wing able to launch various items of software if we were connecting using a VPN. The VPN could only be launched after login for various good reasons.

Fixing this problem was easy as it only required using the Offline Sync capabilities in Windows 7 on the necessary parts of our profile.

This however pointed to a more significant problem for developers. By default IntelliJ, Maven and other tools keep a lot of their configuration and caches in the profile. This means a large volume of data needs to be sync-ed initially and as the IntelliJ caches are updated and new Maven dependencies are added even more sync-ing needs to happen. Not great across a VPN over ADSL.

The solution is to reconfigure your tools to store these high entropy folders in the local drive away for your profile. This also makes the support team happy as it stops the developer profiles getting too large and clogging up the networks.

I created a 'development' folder specifically for these folders and my projects.

Configuring Maven is easy: create a settings.xml file in your profile fold if you haven't already and override the default local repository location. At the same time you may as well be sorting out your proxies or any corporate Nexus repositories. Set the Maven environment properties on the user environment variables and you're done.

IntelliJ is not too hard as well. I copied the idea.properties file from the IntelliJ bin directory (I couldn't edit it directly as it had been written with admin privileges) and edited it to move my .IntellijIDEA folder to my development folder. I replaced the ${user.home} entry in all the relevant properties with a hardcoded value as replacing it with a self declared property like ${configuration.home} caused the logging to end up in a directory under IntelliJ called ${configuration.home}.

I then needed to tell IntelliJ to use the new idea.properties file so I had to set up the IDEA_PROPERTIES environment variable. This could not be done as a system environment variable but fortunately user level environment variables do the job. It is worth noting that you should be able override the Path environment variable that is set in the system environment variables by setting one up in the user ones. Anecdotally this can be a bit temperamental if you don't match the case of the system one and you may have problems if the admins have locked it down too.

The same process of configuration needs to be repeated for any other tools that like to put things in the profile.

The next issue encountered was when starting IntelliJ. IntelliJ 11 would start but not be able to index the project whereas IntelliJ 12 would fail completely, complaining about being unable to start webserver processes. It turned out that the McAfee firewall had been configured too aggressively and local loop back calls (localhost and 127.0.0.1) were blocked. A quick chat with the support team sorted this one out.

The last major issue was with performance due to McAfee Data Loss Prevention (DLP). I'd encountered this before on other corporate sites. You need to ensure that your development folder is excluded. This is due to the number of files typically deleted due to a Maven clean install cycle; DLP just eats CPU and slows disk access to a crawl when this happens. Once more the support team came to the rescue.

Once these issues were sorted I was delighted to have a corporate laptop that was entirely pleasant to develop on. As a bonus we were given widescreen 27" monitors, I was able to rotate mine to landscape mode and view entire classes at a time!

- Posted using BlogPress from my iPad

April 21, 2012

Towards a richer GPGPU Mandelbrot Sets and OpenCL example.

Nearly 3 years ago I started playing with OpenCL on the GPGPU front and posted about my experiences which excited some interest. Since then I've worked on a number of different technologies but I do keep coming back to OpenCL.

I've decided to work on enhancing my Mandelbrot generator and thought I'd share some of my experience doing so. My intention is to create a richer and more capable Mandelbrot Set generator that properly leverages the capabilities of OpenCL and allows someone to more interactively explore the set.

I'm having to dust off my Swing skills to do this as I want to create a richer UI experience. So first off lets talk about current features in my Mandelbrot Generator:

Current Features

  • Single, Double and Fixed Precision (128bit) calculation modes.
  • Simple zooming using the mouse left button to zoom in and the mouse right button to zoom out.
  • Re-sizable window.
  • Simple Benchmarking application using the different calculation modes to demonstrate GPU performance using Single, Double and Integer mathematics.
  • Automatic detection of Work Group size and appropriate buffering.

Future Features

  • Configurable colour palettes.
  • Julia Set generation - for the current location in the Mandelbrot set..
  • UI components to be able to configure the calculation mode and a few other options.
  • Further, richer Fixed Precision modes (256 and 512 bit seem a natural fit for uint8 and uint16)
  • Interactive Benchmarking
  • Ability to save specific 'views' and 'journeys' through the Mandelbrot Set and interact with them.
  • Rendering of ultra high resolution images, either directly rendered in one pass on the GPU or rendered as tiles and stitched together.
  • Render to OpenGL contexts and real-time fly throughs of the set.
Today I'm going to talk about some key components of my Mandelbrot Generator:
  • JavaCL
  • MandelbrotView Interface
  • Fixed Precision algorithm

JavaCL

I've been looking at the various Java to OpenCL bindings and having played with a number of them I keep on coming back to Olivier Chafik's JavaCL for the cleanest and best experience. I have yet to use the Maven bindings generator but even without it, the code is very clean.

My MandelbrotGenerator class which handles all the generation modes looks like:
package co.uk.fvdl.mandelbrot;

import co.uk.fvdl.mathematics.FixedPrecision128;
import co.uk.fvdl.util.TwoTuple;
import com.nativelibs4java.opencl.*;

import java.io.IOException;
import java.io.InputStreamReader;

/**
 * Generic class for generating Mandelbrot set using OpenCL. May be able to use as the basis for generating other
 * fractals such as Julia sets by making it possible to choose different OpenCL programs as parameters.
 */
public class MandelbrotGenerator {

    private static final int ESCAPE_THRESHOLD = 4;
    private final CLKernel mandelbrotFloatKernel;
    private final CLKernel mandelbrotDoubleKernel;
    private final CLKernel mandelbrotFixedPrecision128Kernel;
    private final CLQueue queue;
    private final CLContext context;
    private int workgroupWidth;
    private int workgroupHeight;
    private boolean profiling;


    public MandelbrotGenerator(CLContext context, boolean profiling) throws IOException {
        this(context, Utility.detectLargest2DWorkGroupSize(context), profiling);
    }

    public MandelbrotGenerator(CLContext context, TwoTuple<integer, integer> workgroupSize, boolean profiling) throws IOException {
        this.profiling = profiling;
        System.out.println("Workgroup size: " + workgroupSize);
        this.context = context;
        queue = context.createDefaultQueue();
        queue.setProperty(CLDevice.QueueProperties.ProfilingEnable, profiling);

        //Will always support integers...
        String fixedPrecision128Source = MandelbrotViewer.readFully(
                new InputStreamReader(this.getClass().getResourceAsStream("opencl/MandelbrotFixedPrecision128.cl")),
                new StringBuilder()
        ).toString();
        String fixedPoint128LibrarySource = MandelbrotViewer.readFully(
                new InputStreamReader(this.getClass().getResourceAsStream("opencl/FixedPrecision128.cl")),
                new StringBuilder()
        ).toString();
        CLProgram fixedPrecision128Program = context.createProgram(fixedPoint128LibrarySource, fixedPrecision128Source);
        mandelbrotFixedPrecision128Kernel = fixedPrecision128Program.createKernel("MandelbrotFixedPrecision128");

        //Will always support floats...
        String floatSource = MandelbrotViewer.readFully(
                new InputStreamReader(this.getClass().getResourceAsStream("opencl/MandelbrotFloat.cl")),
                new StringBuilder()
        ).toString();
        CLProgram floatProgram = context.createProgram(floatSource);
        mandelbrotFloatKernel = floatProgram.createKernel("MandelbrotFloat");

        //May not always support doubles...
        if(context.isDoubleSupported()){
            //Read the source file.
            String doubleSource = MandelbrotViewer.readFully(
                    new InputStreamReader(this.getClass().getResourceAsStream("opencl/MandelbrotDouble.cl")),
                    new StringBuilder()
            ).toString();
            CLProgram doubleProgram = context.createProgram(doubleSource);
            mandelbrotDoubleKernel = doubleProgram.createKernel("MandelbrotDouble");
        } else {
            mandelbrotDoubleKernel = null;
        }

        this.workgroupWidth = workgroupSize.getFirstElement();
        this.workgroupHeight = workgroupSize.getSecondElement();
    }

    public GenerationResult generateMandelbrot(MandelbrotView mandelbrotView) throws IOException {

        Padded2DBuffer padded2DBuffer = new Padded2DBuffer(mandelbrotView.getRealAxisPixelCount(), mandelbrotView.getImaginaryAxisPixelCount(), workgroupWidth, workgroupHeight);
        CLBuffer<integer> outputBuffer =  context.createBuffer(CLMem.Usage.Output, padded2DBuffer.getPointer());

        CLEvent event;
        CLKernel mandelbrotKernel = null;
        switch (mandelbrotView.getMandelbrotGenerationStrategy()){
            case DOUBLE:
                if(context.isDoubleSupported()){
                    mandelbrotKernel = mandelbrotDoubleKernel;
                    mandelbrotKernel.setArgs(
                            new double[]{mandelbrotView.getRealAxisPixelSize().doubleValue(), mandelbrotView.getImaginaryAxisPixelSize().doubleValue()},
                            new double[]{mandelbrotView.getRealAxisMinimum().doubleValue(), mandelbrotView.getImaginaryAxisMinimum().doubleValue()},
                            mandelbrotView.getMaxIter(), ESCAPE_THRESHOLD, padded2DBuffer.getActualWidth(), outputBuffer);
                    break;
                }
            case SINGLE:
                mandelbrotKernel = mandelbrotFloatKernel;
                mandelbrotKernel.setArgs(
                        new float[]{mandelbrotView.getRealAxisPixelSize().floatValue(), mandelbrotView.getImaginaryAxisPixelSize().floatValue()},
                        new float[]{mandelbrotView.getRealAxisMinimum().floatValue(), mandelbrotView.getImaginaryAxisMinimum().floatValue()},
                        mandelbrotView.getMaxIter(), ESCAPE_THRESHOLD, padded2DBuffer.getActualWidth(), outputBuffer);
                break;
            case FP128:
                mandelbrotKernel = mandelbrotFixedPrecision128Kernel;
                mandelbrotKernel.setArgs(
                        FixedPrecision128.convertToFixedPrecision128(mandelbrotView.getRealAxisPixelSize()), FixedPrecision128.convertToFixedPrecision128(mandelbrotView.getImaginaryAxisPixelSize()),
                        FixedPrecision128.convertToFixedPrecision128(mandelbrotView.getRealAxisMinimum()), FixedPrecision128.convertToFixedPrecision128(mandelbrotView.getImaginaryAxisMinimum()),
                        mandelbrotView.getMaxIter(), ESCAPE_THRESHOLD, padded2DBuffer.getActualWidth(), outputBuffer);
                break;
        }
        event = mandelbrotKernel.enqueueNDRange(queue, new int[]{padded2DBuffer.getActualWidth(), padded2DBuffer.getActualHeight()}, new int[]{workgroupWidth, workgroupHeight});

        outputBuffer.read(queue, padded2DBuffer.getPointer(), true, event);

        if(profiling){
            return new GenerationResult(padded2DBuffer, event.getProfilingCommandEnd() - event.getProfilingCommandStart());
        } else {
            return new GenerationResult(padded2DBuffer, Long.MIN_VALUE);
        }
    }

    public boolean isDoubleAvailable() {
        return context.isDoubleSupported();
    }
}
I've pushed all the JavaCL code into this class and am able to reuse it in both my interactive and benchmarking applications.

MandelbrotView

To provide a coherent view of the set to my Java code I've created a standard interface that defines the place in the Mandelbrot Set that we are looking at and how we are looking at it:
package co.uk.fvdl.mandelbrot;

import java.math.BigDecimal;

/**
 * Classes implementing this interface represent views onto the Mandelbrot set coordinate space. It is fundamentally
 * represented in terms of the centre of the view on the set and in terms of the zoom level represented by the span of a
 * pixel in that coordinate space. This representation of the view makes it easier to handle re-sizing of the containing
 * window, treating it as a viewport on to the Mandelbrot set.
 */
public interface MandelbrotView {

    /**
     * Get the size of pixels on the Real Axis.
     * @return the size of pixels on the Real Axis.
     */
    BigDecimal getRealAxisPixelSize();

    /**
     * Get the size of pixels on the Imaginary Axis.
     * @return the size of pixels on the Imaginary Axis.
     */
    BigDecimal getImaginaryAxisPixelSize();

    /**
     * Get the number of pixels currently used in the view on the real axis.
     * @return the number of pixels currently used in the view on the real axis.
     */
    int getRealAxisPixelCount();

    /**
     * Get the number of pixels currently used in the view on the imaginary axis.
     * @return the number of pixels currently used in the view on the imaginary axis.
     */
    int getImaginaryAxisPixelCount();

    /**
     * Set the number of pixels to be used in the view on the real axis.
     * @param realAxisPixelCount the number of pixels currently used in the view on the real axis.
     */
    void setRealAxisPixelCount(int realAxisPixelCount);

    /**
     * Set the number of pixels to be used in the view on the imaginary axis.
     * @param imaginaryAxisPixelCount the number of pixels currently used in the view on the imaginary axis.
     */
    void setImaginaryAxisPixelCount(int imaginaryAxisPixelCount);

    /**
     * Calculates the minimum value that the current view has on the real axis.
     * @return the minimum value that the current view has on the real axis.
     */
    BigDecimal getRealAxisMinimum();

    /**
     * Calculates the minimum value that the current view has on the imaginary axis.
     * @return the minimum value that the current view has on the imaginary axis.
     */
    BigDecimal getImaginaryAxisMinimum();

    /**
     * Create a new MandelbrotView for a sub-rectangle of the current view. It will preserve the same pixel dimensions
     * as the original and so will choose the scaling axis to ensure that the whole of the selected rectangle is
     * visible within the new MandelbrotViewImpl instance.
     * @param realAxisPosition The pixel position on the real axis within the current view of the top left corner of the
     *                         rectangle.
     * @param imaginaryAxisPosition  The pixel position on the imaginary axis within the current view of the top left
     *                               corner of the rectangle.
     * @param realAxisPixels The pixel width on the real acis of the rectangle
     * @param imaginaryAxisPixels The pixel width on the imaginary axis of the rectangle
     * @return a new MandelbrotViewImpl representing the sub-rectangle of the current view.
     */
    MandelbrotView selectSubRectangle(
            int realAxisPosition, int imaginaryAxisPosition,
            int realAxisPixels, int imaginaryAxisPixels);

    /**
     * Create a new MandelbrotView for a super-rectangle of the current view where the current view will be contained
     * within the selected rectangle. It will preserve the same pixel dimensions as the original.
     * @param realAxisPosition The pixel position on the real axis within the new view of the top left corner of the
     *                         rectangle containing the current view.
     * @param imaginaryAxisPosition  The pixel position on the imaginary axis within the new view of the top left
     *                               corner of the rectangle containing the current view.
     * @param realAxisPixels The pixel width on the real axis of the rectangle
     * @param imaginaryAxisPixels The pixel width on the imaginary axis of the rectangle
     * @return a new MandelbrotViewImpl representing the super-rectangle of the current view.
     */
    MandelbrotView selectSuperRectangle(
            int realAxisPosition, int imaginaryAxisPosition,
            int realAxisPixels, int imaginaryAxisPixels);

    /**
     * Get the preferred generation strategy.
     * @return the preferred generation strategy.
     */
    MandelbrotGenerationStrategy getMandelbrotGenerationStrategy();

    /**
     * The maximum number if iterations to use before considering that we have escaped the set.
     * @return the maximum number of iterations to use.
     */
    int getMaxIter();

    /**
     * Set the maximum number if iterations to use before considering that we have escaped the set.
     * @param maxIter the maximum number of iterations to use.
     */
    void setMaxIter(int maxIter);
}

While the view interface talks about the 'minimums' and the number of pixels which maps well to what we need to render, the underlying implementation defines the view in terms of the centre of the view and the pixel size. This means that when we resize the window containing the view the view remains centred on what we intend:
package co.uk.fvdl.mandelbrot;

import co.uk.fvdl.mathematics.BigDecimalArithmetic;

import java.math.BigDecimal;

import static co.uk.fvdl.util.Preconditions.checkNotNull;

/**
 * This class represents a view onto the Mandelbrot set coordinate space. It is fundamentally represented in terms of
 * the centre of the view on the set and in terms of the zoom level represented by the span of a pixel in that coordinate
 * space. This representation of the view makes it easier to handle re-sizing of the containing window, treating it as a
 * viewport on to the Mandelbrot set.
 */
public class MandelbrotViewImpl implements MandelbrotView {
    private final BigDecimal realAxisCentre;
    private final BigDecimal imaginaryAxisCentre;
    private final BigDecimal realAxisPixelSize;
    private final BigDecimal imaginaryAxisPixelSize;
    private final BigDecimalArithmetic numberArithmetic;
    private final MandelbrotGenerationStrategy mandelbrotGenerationStrategy;

    private int realAxisPixelCount;
    private int imaginaryAxisPixelCount;
    private int maxIter;

    /**
     * Construct an instance of the Mandelbrot view for the following parameters.
     * @param realAxisCentre the centre of the view on the Real Axis.
     * @param imaginaryAxisCentre the centre of the view on the Imaginary Axis.
     * @param realAxisPixelSize the size of pixels on the Real Axis.
     * @param imaginaryAxisPixelSize the size of pixels on the Imaginary Axis.
     * @param realAxisPixelCount the number of pixels to be used in the view on the real axis.
     * @param imaginaryAxisPixelCount the number of pixels currently used in the view on the imaginary axis.
     * @param mandelbrotGenerationStrategy the prefered strategy to use when generating the Mandelbrot set.
     * @param maxIter The maximum iteration to use for generating the set.
     */
    @SuppressWarnings("unchecked")
    public MandelbrotViewImpl(BigDecimal realAxisCentre, BigDecimal imaginaryAxisCentre,
                              BigDecimal realAxisPixelSize, BigDecimal imaginaryAxisPixelSize,
                              int realAxisPixelCount, int imaginaryAxisPixelCount, MandelbrotGenerationStrategy mandelbrotGenerationStrategy, int maxIter) {
        this.mandelbrotGenerationStrategy = mandelbrotGenerationStrategy;
        this.maxIter = maxIter;
        this.numberArithmetic = new BigDecimalArithmetic();
        this.realAxisCentre = checkNotNull(realAxisCentre, "Real Axis centre is null.");
        this.imaginaryAxisCentre = checkNotNull(imaginaryAxisCentre, "Imaginary Axis centre is null.");
        this.realAxisPixelSize = checkNotNull(realAxisPixelSize, "Real Axis pixel size is null.");
        this.imaginaryAxisPixelSize = checkNotNull(imaginaryAxisPixelSize, "Imaginary Axis pixel size is null.");
        this.realAxisPixelCount = realAxisPixelCount;
        this.imaginaryAxisPixelCount = imaginaryAxisPixelCount;
    }

    /**
     * Get the centre of the view on the Real Axis.
     * @return the centre of the view on the Real Axis.
     */
    public BigDecimal getRealAxisCentre() {
        return realAxisCentre;
    }

    /**
     * Get the centre of the view on the Imaginary Axis.
     * @return the centre of the view on the Imaginary Axis.
     */
    public BigDecimal getImaginaryAxisCentre() {
        return imaginaryAxisCentre;
    }

    @Override
    public BigDecimal getRealAxisPixelSize() {
        return realAxisPixelSize;
    }

    @Override
    public BigDecimal getImaginaryAxisPixelSize() {
        return imaginaryAxisPixelSize;
    }

    @Override
    public int getRealAxisPixelCount() {
        return realAxisPixelCount;
    }

    @Override
    public int getImaginaryAxisPixelCount() {
        return imaginaryAxisPixelCount;
    }

    @Override
    public void setRealAxisPixelCount(int realAxisPixelCount) {
        this.realAxisPixelCount = realAxisPixelCount;
    }

    @Override
    public void setImaginaryAxisPixelCount(int imaginaryAxisPixelCount) {
        this.imaginaryAxisPixelCount = imaginaryAxisPixelCount;
    }

    @Override
    public BigDecimal getRealAxisMinimum() {
        return numberArithmetic.subtract(
                realAxisCentre, 
                numberArithmetic.divide(
                        numberArithmetic.multiply(
                                realAxisPixelSize, 
                                numberArithmetic.fromInteger(realAxisPixelCount)
                        ),
                        numberArithmetic.fromInteger(2)));
    }

    @Override
    public BigDecimal getImaginaryAxisMinimum() {
        return numberArithmetic.subtract(
                imaginaryAxisCentre, 
                numberArithmetic.divide( 
                        numberArithmetic.multiply(
                                imaginaryAxisPixelSize, 
                                numberArithmetic.fromInteger(imaginaryAxisPixelCount)
                        ),numberArithmetic.fromInteger(2)));
    }

    @Override
    public MandelbrotViewImpl selectSubRectangle(
            int realAxisPosition, int imaginaryAxisPosition,
            int realAxisPixels, int imaginaryAxisPixels) {
        BigDecimal scale;
        if((realAxisPixelCount / imaginaryAxisPixelCount) > (realAxisPixels/imaginaryAxisPixels)){ //use the imaginary axis to provide the scaling
            scale = numberArithmetic.divide(
                    numberArithmetic.fromInteger(imaginaryAxisPixels),
                    numberArithmetic.fromInteger(imaginaryAxisPixelCount)
            );
        } else { //use the real axis to provide scaling.
            scale = numberArithmetic.divide(
                    numberArithmetic.fromInteger(realAxisPixels),
                    numberArithmetic.fromInteger(realAxisPixelCount)
            );
        }

        float newRealPixelCentre = (float)realAxisPosition + (float)realAxisPixels / 2.0F;
        float newImaginaryPixelCentre = (float)imaginaryAxisPosition + (float)imaginaryAxisPixels / 2.0F;
        float currentRealPixelCentre = (float)realAxisPixelCount / 2.0F;
        Float currentImaginaryPixelCentre = (float)imaginaryAxisPixelCount / 2.0F;

        BigDecimal deltaRealPixels = numberArithmetic.fromFloat(newRealPixelCentre - currentRealPixelCentre);
        BigDecimal deltaImaginaryPixels = numberArithmetic.fromFloat(newImaginaryPixelCentre - currentImaginaryPixelCentre);
        
        BigDecimal newRealAxisCentre = numberArithmetic.add(
                realAxisCentre,
                numberArithmetic.multiply(
                        deltaRealPixels,
                        realAxisPixelSize));
        
        BigDecimal newImaginaryAxisCentre = numberArithmetic.add(
                imaginaryAxisCentre,
                numberArithmetic.multiply(
                        deltaImaginaryPixels,
                        imaginaryAxisPixelSize));

        BigDecimal newRealAxisPixelSize = numberArithmetic.multiply(realAxisPixelSize, scale);
        BigDecimal newImaginaryAxisPixelSize = numberArithmetic.multiply(imaginaryAxisPixelSize, scale);

        return new MandelbrotViewImpl(
                newRealAxisCentre, newImaginaryAxisCentre,
                newRealAxisPixelSize, newImaginaryAxisPixelSize,
                realAxisPixelCount, imaginaryAxisPixelCount, mandelbrotGenerationStrategy, maxIter);
    }

    @Override
    public MandelbrotViewImpl selectSuperRectangle(
            int realAxisPosition, int imaginaryAxisPosition,
            int realAxisPixels, int imaginaryAxisPixels) {
        BigDecimal scale;
        if((realAxisPixelCount / imaginaryAxisPixelCount) > (realAxisPixels/imaginaryAxisPixels)){ //use the imaginary axis to provide the scaling
            scale = numberArithmetic.divide(
                    numberArithmetic.fromInteger(imaginaryAxisPixelCount),
                    numberArithmetic.fromInteger(imaginaryAxisPixels)
            );
        } else { //use the real axis to provide scaling.
            scale = numberArithmetic.divide(
                    numberArithmetic.fromInteger(realAxisPixelCount),
                    numberArithmetic.fromInteger(realAxisPixels)
            );
        }

        int newRealPixelCentre = realAxisPixelCount / 2;
        int newImaginaryPixelCentre = imaginaryAxisPixelCount / 2;
        int currentRealPixelCentre = realAxisPosition + realAxisPixels / 2;
        int currentImaginaryPixelCentre = imaginaryAxisPosition + imaginaryAxisPixels / 2;

        BigDecimal deltaRealPixels = numberArithmetic.fromInteger(newRealPixelCentre - currentRealPixelCentre);
        BigDecimal deltaImaginaryPixels = numberArithmetic.fromInteger(newImaginaryPixelCentre - currentImaginaryPixelCentre);

        BigDecimal newRealAxisPixelSize = numberArithmetic.multiply(realAxisPixelSize, scale);
        BigDecimal newImaginaryAxisPixelSize = numberArithmetic.multiply(imaginaryAxisPixelSize, scale);

        BigDecimal newRealAxisCentre = numberArithmetic.add(
                realAxisCentre,
                numberArithmetic.multiply(
                        deltaRealPixels,
                        newRealAxisPixelSize));

        BigDecimal newImaginaryAxisCentre = numberArithmetic.add(
                imaginaryAxisCentre,
                numberArithmetic.multiply(
                        deltaImaginaryPixels,
                        newImaginaryAxisPixelSize));


        return new MandelbrotViewImpl(
                newRealAxisCentre, newImaginaryAxisCentre,
                newRealAxisPixelSize, newImaginaryAxisPixelSize,
                realAxisPixelCount, imaginaryAxisPixelCount, mandelbrotGenerationStrategy, maxIter);

    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer();
        sb.append("MandelbrotViewImpl");
        sb.append("{maxIter=").append(maxIter);
        sb.append(", realAxisCentre=").append(realAxisCentre);
        sb.append(", imaginaryAxisCentre=").append(imaginaryAxisCentre);
        sb.append(", realAxisPixelCount=").append(realAxisPixelCount);
        sb.append(", imaginaryAxisPixelCount=").append(imaginaryAxisPixelCount);
        sb.append(", realAxisPixelSize=").append(realAxisPixelSize);
        sb.append(", imaginaryAxisPixelSize=").append(imaginaryAxisPixelSize);
        sb.append(", mandelbrotGenerationStrategy=").append(mandelbrotGenerationStrategy);
        sb.append('}');
        return sb.toString();
    }

    @Override
    public MandelbrotGenerationStrategy getMandelbrotGenerationStrategy() {
        return mandelbrotGenerationStrategy;
    }

    @Override
    public int getMaxIter() {
        return maxIter;
    }

    @Override
    public void setMaxIter(int maxIter) {
        this.maxIter = maxIter;
    }
}
The final component of the MandelbrotView is the MandelbrotViewHolder which acts as a delegate for the multiple MandelbrotView instances that we progress through as we zoom. The intent is that eventually the MandelbrotViewHolder will morph into something that allows us to trace the history of a session exploring the MandelbrotSet.
package co.uk.fvdl.mandelbrot;

import java.math.BigDecimal;

import static co.uk.fvdl.util.Preconditions.checkNotNull;

/**
 * This class provides a level of indirection that allows us to work with immutable implementations of the
 * MandelbrotView interface without having to deal with a large amount of reference passing in UI components as the
 * view is modified.
 */
public class MandelbrotViewHolder implements MandelbrotView {

    private MandelbrotView wrappedInstance;

    /**
     * Construct and instance using the wrapped instance.
     * @param wrappedInstance the instance of MandelbrotView to wrap.
     */
    public MandelbrotViewHolder(MandelbrotView wrappedInstance) {
        this.wrappedInstance = checkNotNull(wrappedInstance, "The MandelbrotView instance to be wrapped may not be null");
    }

    @Override
    public BigDecimal getRealAxisPixelSize() {
        return wrappedInstance.getRealAxisPixelSize();
    }

    @Override
    public BigDecimal getImaginaryAxisPixelSize() {
        return wrappedInstance.getImaginaryAxisPixelSize();
    }

    @Override
    public int getRealAxisPixelCount() {
        return wrappedInstance.getRealAxisPixelCount();
    }

    @Override
    public int getImaginaryAxisPixelCount() {
        return wrappedInstance.getImaginaryAxisPixelCount();
    }

    @Override
    public void setRealAxisPixelCount(int realAxisPixelCount) {
        wrappedInstance.setRealAxisPixelCount(realAxisPixelCount);
    }

    @Override
    public void setImaginaryAxisPixelCount(int imaginaryAxisPixelCount) {
        wrappedInstance.setImaginaryAxisPixelCount(imaginaryAxisPixelCount);
    }

    @Override
    public BigDecimal getRealAxisMinimum() {
        return wrappedInstance.getRealAxisMinimum();
    }

    @Override
    public BigDecimal getImaginaryAxisMinimum() {
        return wrappedInstance.getImaginaryAxisMinimum();
    }

    /**
     * Calls the underlying wrapped instance to create a new view on the Mandelbrot set for the sub-rectangle, replacing
     * it with the new one.
     * @param realAxisPosition The pixel position on the real axis within the current view of the top left corner of the
     *                         rectangle.
     * @param imaginaryAxisPosition  The pixel position on the imaginary axis within the current view of the top left
     *                               corner of the rectangle.
     * @param realAxisPixels The pixel width on the real acis of the rectangle
     * @param imaginaryAxisPixels The pixel width on the imaginary axis of the rectangle
     * @return this instance of the MandelbrotViewHolder which now wraps the new instance of the view.
     */
    @Override
    public MandelbrotView selectSubRectangle(int realAxisPosition, int imaginaryAxisPosition,
                                                int realAxisPixels, int imaginaryAxisPixels) {
        wrappedInstance = wrappedInstance.selectSubRectangle(
                realAxisPosition, imaginaryAxisPosition,
                realAxisPixels, imaginaryAxisPixels);
        return this;
    }

    /**
     * Calls the underlying wrapped instance to create a new view on the Mandelbrot set for the super-rectangle, replacing
     * it with the new one.
     * @param realAxisPosition The pixel position on the real axis within the new view of the top left corner of the
     *                         rectangle containing the current view.
     * @param imaginaryAxisPosition  The pixel position on the imaginary axis within the new view of the top left
     *                               corner of the rectangle containing the current view.
     * @param realAxisPixels The pixel width on the real axis of the rectangle
     * @param imaginaryAxisPixels The pixel width on the imaginary axis of the rectangle
     * @return a new MandelbrotViewImpl representing the super-rectangle of the current view.
     */
    @Override
    public MandelbrotView selectSuperRectangle(int realAxisPosition, int imaginaryAxisPosition, int realAxisPixels, int imaginaryAxisPixels) {
        wrappedInstance = wrappedInstance.selectSuperRectangle(
                realAxisPosition, imaginaryAxisPosition,
                realAxisPixels, imaginaryAxisPixels);
        return this;
    }

    @Override
    public MandelbrotGenerationStrategy getMandelbrotGenerationStrategy() {
        return wrappedInstance.getMandelbrotGenerationStrategy();
    }

    @Override
    public int getMaxIter() {
        return wrappedInstance.getMaxIter();
    }

    @Override
    public void setMaxIter(int maxIter) {
        wrappedInstance.setMaxIter(maxIter);
    }
}

Fixed Precision Mathematics

Having delved deep into the Mandelbrot set using both single and double precision implementations I discovered that with the raw performance of OpenCL on a decent graphic card it is entirely too easy to run into the limits of precision of the numeric representation. Having read around the problem I came upon the idea of Fixed Precision Mathematics to go deeper and came across an excellent piece of work by a gentleman called Eric Bainville at this site.

Using his work as a jumping off point I started seeing if I could integrate Fixed Precision calculations into my Mandelbrot Generator. The only problem I had was that I didn't fully understand the code that I was using and wanted to be able to extend it to greater precisions so I decided to write my own implementation from first principles. I did reuse a useful hack that he used replacing all the insignificant digit carry calculations with a '3'.

In the end I created a stripped down library to service my own Fixed Precision Mandelbrot generation.

It was interesting to compare his and my Fixed Precision libraries to see the similarities and differences - I deliberately didn't take some of the optimisation paths (e.g. dedicated squaring logic) - but I did end up with a mixture of similar and different methods. The largest difference was in the Positive / Positive multiplication where I decided to try to take advantage of the vector methods:
#define MAX_UINT4 (uint4)(0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF)
#define MOST_SIGNIFICANT_BIT_MASK 0x80000000U
uint4 addFP128(uint4 firstAddend, uint4 secondAddend)
{
    uint4 naiveAddition = firstAddend + secondAddend;
    uint4 propagatableVector = convert_uint4(naiveAddition == MAX_UINT4);
    uint4 overflowCarries = (convert_uint4(naiveAddition < firstAddend) & (uint4)(0,1,1,1)).yzwx;
    uint4 propagatedCarries = (uint4)(
                    ( (overflowCarries.z & propagatableVector.z) | overflowCarries.y ) & propagatableVector.y,
                    overflowCarries.z & propagatableVector.z,
                    0,
                    0);

    return naiveAddition + overflowCarries + propagatedCarries;
}

uint4 doubleFP128(uint4 operand)
{
    uint4 propagated = (operand & (uint4)(0,MOST_SIGNIFICANT_BIT_MASK, MOST_SIGNIFICANT_BIT_MASK, MOST_SIGNIFICANT_BIT_MASK)).yzwx  >>(uint4)(31U);
    return (operand<<(uint4)(1U)) + propagated;
}

uint4 incrementFP128(uint4 operand)
{
    uint4 propagatableVector = convert_uint4(operand == MAX_UINT4); // Determine if any of the operand elements are equivalent to 0xFFFFFFFF
    uint4 carriedIncrement = (uint4)(
                    propagatableVector.y & propagatableVector.z & propagatableVector.w & 1, //Carry up to x
                    propagatableVector.z & propagatableVector.w & 1, //Carry up to y
                    propagatableVector.w&1, //Carry up to z
                    1); //increment to be added to w
    return operand + carriedIncrement;
}

uint4 negateFP128(uint4 operand)
{
    uint4 complement = operand ^ MAX_UINT4;
    return incrementFP128(complement);
}

uint4 absoluteFP128(uint4 value) {
    return ((value.x & MOST_SIGNIFICANT_BIT_MASK)?negateFP128(value):value);
}

uint4 multiplyFP128PositivePositive(uint4 firstMultiplicand, uint4 secondMultiplicand)
{
    uint4 wXYZW = firstMultiplicand * (uint4)(secondMultiplicand.w);
    uint4 wXYZWHigh = mul_hi(firstMultiplicand, (uint4)(secondMultiplicand.w));
    uint4 zXYZW = firstMultiplicand * (uint4)(secondMultiplicand.z);
    uint4 zXYZWHigh = mul_hi(firstMultiplicand, (uint4)(secondMultiplicand.z));
    uint4 yXYZW = firstMultiplicand * (uint4)(secondMultiplicand.y);
    uint4 yXYZWHigh = mul_hi(firstMultiplicand, (uint4)(secondMultiplicand.y));
    uint4 xXYZW = firstMultiplicand * (uint4)(secondMultiplicand.x);
    uint4 xXYZWHigh = mul_hi(firstMultiplicand, (uint4)(secondMultiplicand.x));

    uint4 resultPart1 = addFP128((uint4)(yXYZWHigh.x, zXYZWHigh.x, wXYZWHigh.x, 3), (uint4)(xXYZWHigh.y, yXYZWHigh.y, zXYZWHigh.y, wXYZWHigh.y));
    uint4 resultPart2 = addFP128((uint4)(0, xXYZWHigh.z, yXYZWHigh.z, zXYZWHigh.z),   (uint4)(0, 0, xXYZWHigh.w, yXYZWHigh.w));
    uint4 resultPart3 = addFP128((uint4)(0, 0, 0, wXYZW.x), (uint4)(0, 0, zXYZW.x, zXYZW.y));
    uint4 resultPart4 = addFP128((uint4)(0, yXYZW.x, yXYZW.y, yXYZW.z), (uint4)(xXYZW.x, xXYZW.y, xXYZW.z, xXYZW.w));
    return addFP128(addFP128(resultPart1, resultPart2), addFP128(resultPart3, resultPart4));
}

//TODO consider if a dedicated squaring function would be a good idea.

uint4 multiplyFP128AnyAny(uint4 firstMultiplicand, uint4 secondMultiplicand)
{
      // Get the absolute product;
      uint4 p = multiplyFP128PositivePositive(absoluteFP128(firstMultiplicand), absoluteFP128(secondMultiplicand));
      //Return it with the appropriate sign
      return ((firstMultiplicand.x & MOST_SIGNIFICANT_BIT_MASK)^(secondMultiplicand.x & MOST_SIGNIFICANT_BIT_MASK))?negateFP128(p):p;
}
I implemented my Fixed Precision 128 Mandelbrot Kernel using it looking like:
__kernel void MandelbrotFixedPrecision128(
    const uint4 realDelta,
    const uint4 imaginaryDelta,
    const uint4 realMinimum,
    const uint4 imaginaryMinimum,
 const unsigned int maxIter,
 const unsigned int escapeNumber,
 const unsigned int hRes,
 __global int* outputi
)
{
 int realId = get_global_id(0);
 int imaginaryId = get_global_id(1);

    uint4 realPosition = addFP128(realMinimum, multiplyFP128AnyAny(realDelta, realId));
    uint4 squaredRealValue = multiplyFP128AnyAny(realPosition, realPosition);
    uint4 realValue = realPosition;

    uint4 imaginaryPosition = addFP128(imaginaryMinimum, multiplyFP128AnyAny(imaginaryDelta, imaginaryId));
    uint4 squaredImaginaryValue = multiplyFP128AnyAny(imaginaryPosition, imaginaryPosition);
    uint4 imaginaryValue = imaginaryPosition;

 int iter = 0;
 while ( (iter < maxIter) && (addFP128(squaredRealValue,squaredImaginaryValue).x < escapeNumber) )
 {
     imaginaryValue = addFP128(doubleFP128(multiplyFP128AnyAny(realValue, imaginaryValue)), imaginaryPosition);
     realValue = addFP128(addFP128(squaredRealValue, negateFP128(squaredImaginaryValue)), realPosition);

        squaredRealValue = multiplyFP128AnyAny(realValue, realValue);
        squaredImaginaryValue = multiplyFP128AnyAny(imaginaryValue, imaginaryValue);

  iter++;
 }
 if(iter >= maxIter)
  iter = maxIter + 1;

 outputi[imaginaryId * hRes + realId] = iter;
}
This new implementation gives me the ability to delve much deeper into the Mandelbrot Set but at the cost of significantly reduced performance.
On my AMD 6970 Radeon graphics card:
Workgroup size: TwoTuple{firstElement=16, secondElement=16}
10 runs of MandelbrotViewImpl{maxIter=512, realAxisCentre=-0.75, imaginaryAxisCentre=0, realAxisPixelCount=1280, imaginaryAxisPixelCount=1280, realAxisPixelSize=0.00234375, imaginaryAxisPixelSize=0.00234375, mandelbrotGenerationStrategy=FP128} took on average 157.7323556 milliseconds
10 runs of MandelbrotViewImpl{maxIter=512, realAxisCentre=-0.75, imaginaryAxisCentre=0, realAxisPixelCount=1280, imaginaryAxisPixelCount=1280, realAxisPixelSize=0.00234375, imaginaryAxisPixelSize=0.00234375, mandelbrotGenerationStrategy=DOUBLE} took on average 7.4757887 milliseconds
10 runs of MandelbrotViewImpl{maxIter=512, realAxisCentre=-0.75, imaginaryAxisCentre=0, realAxisPixelCount=1280, imaginaryAxisPixelCount=1280, realAxisPixelSize=0.00234375, imaginaryAxisPixelSize=0.00234375, mandelbrotGenerationStrategy=SINGLE} took on average 6.429966899999999 milliseconds

March 09, 2012

Selenium, HtmlUnit, JavaScript and JQuery - Writing Performant Tests

I'm working on a project that uses a certain amount of JavaScript to build a dynamic UI, the project team were of the opinion that we used barely any JavaScript and were flummoxed as to why the integration tests using Selenium with the HtmlUnit Driver were so slow.

I finally got time a couple of weeks ago to investigate the problem and because I tend to avoid JavaScript like the plague, it took me a little while to garner the necessary skills and utilities to diagnose the problems.

I thought I would share the lessons I learnt in case anyone else comes across similar problems.

The first thing I did was to upgrade all the libraries to their latest versions this often fixes performance bugs and can highlight problems with the code base. In my case it only garnered a minor speed up but highlighted the fact that the old version of Selenium we were using allowed interaction with hidden and disabled HTML elements and a number of our tests were badly written.

The second thing I discovered is that the Selenium / HtmlUnit event model means that SELECT element change events were only fired when focus changed to another element in the page.

Seeing no major speed increase I instead reverted to profiling the Java processes. We don't have access to a commercial profiler but a remarkable amount can be achieved using VisualVM which is included as part of the Sun JDK since about version 6.012.

The first thing you should always do when installing this utility is to increase the memory available using the VisualVM options file in the JDK configuration folders.

I was very careful to limit what I profiled to only a certain subset of the classes involved as even with the tweaked memory settings VisualVM is a little flaky when profiling large, long-running processes.

I used the CPU profiling capability and focused in on the projects test classes, the Selenium classes and the HtmlUnit classes.

After a certain amount of faffing around I discovered that a large amount of time was spent in the JavaScript and particularly around event bubbling and DOM change listening.

Unfortunately there is a certain level of indirection involved in the JavaScript implementation and could not discover precisely which JavaScript elements were involved even when using Java debugging.

I instead reverted to using FireBug and FireQuery to profile and navigate through the JavaScript within Firefox. I quickly discovered some obvious JavaScript performance problems and fixed them. Equally rapidly I discovered that the performance characteristics of the Firefox JavaScript engine is very different to those of the HtmlUnit JavaScript engine - Mozilla Rhino.

FireBug and FireQuery were excellent for helping me to understand what was executing and would be perfect for profiling browser performance issues, I would have to look elsewhere for the root cause of the HtmlUnit performance problems.

I resorted to old-school - comment stuff out until it broke or performed faster.

The problem was rapidly resolved to being the use of JQuery live() methods. They are designed to handle events highly dynamic DOMs by listening to events that have bubbled all the way up to the root and then matching the source of the event to a selector before calling the method intended to handle the event.

I replaced them with JQuery bind() and delegate() methods and saw a massive performance increase: A UI test suite that took 26 minutes to run completed in 10.

I learnt that highly dynamic JQuery based scripting could cause severe test performance problems and learnt a lot about how it worked. Longer term I'm going to move to statically bound JavaScript as much as possible particularly when I need to be able to test it.

A framework like JQuery is a little like an iceberg, it may look like you're barely using any JavaScript but under the surface...

- Posted using BlogPress from my iPad