Test details

Here’s the details of the performance testing listed below, hardware etc and the raw IOZone numbers.

Client Details:

  • Dual-Core AMD Opteron(tm) Processor 1220 (Sun 2100)
  • 2.8Ghz, 2GB Memory, 1Gbps network, RHEL4

Server Details:

  • Intel Xeon 5120, 1.86Ghz (dell 1950)
  • 1GB Memory, 1Gbps network, RHEL5
  • Perc5i raid controller setup in 2 7-disk raid 5 volumes
  • Disk: Dell MD1000 w/ 750Gb SATA 7200rpm drives

Untuned results (4G test, 4k records) in KB/s

                                                   random  random    bkwd      
     KB  reclen   write rewrite    read    reread    read   write    read 
4194304    4096   93137   68662    28869    29300   23118   57440   23186 

Tuned results:

                                                   random  random    bkwd
     KB  reclen   write rewrite    read    reread    read   write    read
4194304    4096   95360   71281   108200   112603   67761   69528   68227

Notice the huge gains in read performance 28MB/s to 108MB/s for sequential and 23MB/s to 67MB/s for non-sequential.

Here’s the theoretical max based on running iozone on the server. It looks like nfs is adding overhead on rewrites and possibly random reads and writes with sequential reads/writes bottlenecking at the 1Gbps connection

                                                   random  random    bkwd      
     KB  reclen   write rewrite    read    reread    read   write    read 
2097152    4096  302379  234327   174385   196905  102032  144843   99866
  • Raw server performance:
  • Untuned performance: xls txt
  • Tuned performance: xls txt
  • Server to raid xls txt

NFS Performance

We’re testing a moderately fast nfs server. Testing on our data set of large files, ~100M we were seeing performance a little over 100MB/s. Now, when we shared the same filesystem out via nfs performance dropped to a mere 44MB/s and no matter what the r/wsize of the client, would not increase. Both machines are connected via a 1Gbps link that has been shown to move 941mb/s. Here’s a snapshot what the server workload looked like on the server. The first line shows the local testing, notice the 108MB/s and the second shows the new access. The interesting part is the device utilization during the tests, even though we were pushing half the requests/second and moving half the bandwidth, we were at 90% with a higher wait time 4ms vs 1.7ms.

Device:         rrqm/s   wrqm/s   r/s   w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await  svctm  %util
sdb              30.50     0.00 906.00  0.00 108530.00     0.00   239.58     1.61    1.78   1.08  98.15
sdb             870.00     0.00 500.50  0.00 44626.00     0.00   178.33     2.06    4.09   1.83  91.55

Now, let’s tweak the ioscheduling a little by setting slice_idle to 1 as recommended in the dell guide below. ‘echo 1 > /sys/block/sdb/queue/iosched/slice_idle’ This affects how long the io scheduler will wait in each queue before looking for more work. On local access where you will have lots of low latency requests you probably want to wait for more work, however over a network with lots of clients you will want to switch as soon as possible

sdb            1164.18     0.00 1049.75  0.00 71150.25     0.00   135.56     2.34    2.23   0.87  91.79

let’s go a little further and set it to 0

sdb             144.00     0.00 2428.50  0.00 92034.00     0.00    75.79     3.95    1.63   0.33  79.60

Now we’re up to 92MB/s which is above 90% of what our network can do. Good enough!

Misc Pivot stuff

Padding choices

In wtkx, adding padding usually looks like: styles=”{padding:10}”. Nice if you want 10px all around, but you can also specify each side styles=”{padding:{top:1,left:2,bottom:3,right:4}}”.

Adding validators inline

Normally to add a validator to a component, you code something similar to

@WTKX
private TextInput maxFileSize;
...
...
maxFileSize.setValidator(new IntRangeValidator(0, Integer.MAX_VALUE));

Seems like a bit of a waste? In WTKX, you can include any object that has a no-argument constructor. So we can inline the whole thing.

  1. Include the package to your object as part of the xml namespace (notice xmlns:text)
  2. Define the validator.
  3. attach it to your component

 
     
 
 
...
...
   
...
 

SASL Plain Server

For some reason SUN includes support for sasl PLAIN in their SaslClient, but not SaslServer. Here’s a quick hack to implement a plain sasl server. As per rfc4616.txt, there is only message, three UTF-8 strings, max length 255 delimited by null (\u0000) sent from the client. There is no server response.

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;

/**
 * Simple sasl server to decode a
 * @author toaster
 */
public class PlainSaslServer implements SaslServer
{

    public boolean complete = false;
    private String authID = null;
    private CallbackHandler cbh;
    
    PlainSaslServer(CallbackHandler cbh)
    {
        this.cbh = cbh;
    }

    public String getMechanismName()
    {
        return SaslConstants.SASL_SERVICE;
    }

    public byte[] evaluateResponse(byte[] response) throws SaslException
    {
        String authnID = null;
        String authzID = null;
        String password = null;

        // Decode response as per rfc4616.txt
//       The formal grammar for the client message using Augmented BNF [ABNF]
//       follows.
//
//       message   = [authzid] UTF8NUL authcid UTF8NUL passwd
//       authcid   = 1*SAFE ; MUST accept up to 255 octets
//       authzid   = 1*SAFE ; MUST accept up to 255 octets
//       passwd    = 1*SAFE ; MUST accept up to 255 octets
//       UTF8NUL   = %x00 ; UTF-8 encoded NUL character
//
//       SAFE      = UTF1 / UTF2 / UTF3 / UTF4
//                   ;; any UTF-8 encoded Unicode character except NUL
//
//       UTF1      = %x01-7F ;; except NUL
//       UTF2      = %xC2-DF UTF0
//       UTF3      = %xE0 %xA0-BF UTF0 / %xE1-EC 2(UTF0) /
//                   %xED %x80-9F UTF0 / %xEE-EF 2(UTF0)
//       UTF4      = %xF0 %x90-BF 2(UTF0) / %xF1-F3 3(UTF0) /
//                   %xF4 %x80-8F 2(UTF0)
//       UTF0      = %x80-BF

        int start = 0;
        int end = 0;
        int elementIdx = 0;

        try
        {


            for (byte b : response)
            {
                if (b == '\u0000')
                {
                    // empty string, only authzid allows this
                    if (end - start == 0)
                    {
                        if (elementIdx != 0)
                        {
                           throw new SaslException("null auth data");
                        }

                    } // data, wa-hoo
                    else
                    {
                        String element = new String(response, start, end - start, "UTF-8");
                        start = end + 1;

                        switch (elementIdx)
                        {
                            case 0:
                                authzID = element;
                                break;
                            case 1:
                                authnID = element;
                                break;
                            default:
                                throw new SaslException("Unexpected data in packet");
                        }
                    }
                    elementIdx++;

                }
                end++;
            }

            if (start == end)
            {
                throw new SaslException("null auth data");
            }

            password = new String(response, start, end - start, "UTF-8");


        } catch (UnsupportedEncodingException e)
        {
            throw new SaslException("utf-8 encoding");
        }


        ExternalValidationCallback evc = new ExternalValidationCallback(authnID, password);
        AuthorizeCallback ac = new AuthorizeCallback(authnID, authzID);

        Callback[] cbList = new Callback[2];

        cbList[0] = evc;
        cbList[1] = ac;

        try
        {
            if (password == null || authnID == null)
            {
                throw new SaslException("null auth data");
            }

            cbh.handle(cbList);
            if (!evc.isValidated())
            {
                throw new SaslException("cannot validate password");
            }

            if (!ac.isAuthorized())
            {
                throw new SaslException("user not authorized");
            }

            complete = true;
            return null;

        } catch (UnsupportedCallbackException ex)
        {
            throw new SaslException("unsupported callback", ex);

        } catch (IOException e)
        {
            if (e instanceof SaslException)
            {
                throw (SaslException) e;
            }
            throw new SaslException("Callback error", e);
        }

    }

    public boolean isComplete()
    {
        return complete;
    }

    public String getAuthorizationID()
    {
        if (!complete)
        {
            throw new IllegalStateException("not complete");
        }
        return authID;
    }

    public byte[] unwrap(byte[] incoming, int offset, int len)
    {
        if (!complete)
        {
            throw new IllegalStateException("not complete");
        }
        final byte[] result = new byte[len];
        System.arraycopy(incoming, offset, result, 0, len);
        return result;

    }

    public byte[] wrap(byte[] outgoing, int offset, int len)
    {
        if (!complete)
        {
            throw new IllegalStateException("not complete");
        }

        final byte[] result = new byte[len];
        System.arraycopy(outgoing, offset, result, 0, len);
        return result;

    }

    public Object getNegotiatedProperty(String propName)
    {
        return null;
    }

    public void dispose() throws SaslException
    {
    }
}

This also uses the following callback to validate the client’s username and password. The SaslServer will throw a SaslException if the isValidated returns false.

public class ExternalValidationCallback implements Callback
{

    private String username;
    private String password;
    private boolean validated = false;

    public ExternalValidationCallback(String username, String password)
    {
        this.username = username;
        this.password = password;
    }

    public String getPassword()
    {
        return password;
    }

    public String getUsername()
    {
        return username;
    }

    public boolean isValidated()
    {
        return validated;
    }

    public void setValidated(boolean validated)
    {
        this.validated = validated;
    }
}

Now after we have the new callback and server we need to register it as a provider w/in java. This requires that we create a server factory and simple security provider to register the factory. The server factory will need to check the sasl property POLICY_NOPLAINTEXT to see if its allowed to return the PLAIN server.

public class PlainSaslServerFactory implements SaslServerFactory
{

    private static String[] mechanisms =
    {
        "PLAIN"
    };

    public SaslServer createSaslServer(String mechanism, String protocol,
            String serverName,
            Map props, CallbackHandler cbh) throws SaslException
    {

        if (!mechanisms[0].equals(mechanism) || cbh == null)
            return null;
       return new PlainSaslServer(cbh);

    }

    public String[] getMechanismNames(Map props)
    {
        if ("true".equals(props.get(Sasl.POLICY_NOPLAINTEXT)))
            return new String[0];
        return mechanisms;
    }
}

Now, create the java security provider.

public final class MySecurityProvider extends Provider
{

    public MySecurityProvider()
    {
        super("My Provider", 1.0 , "Simple sasl plain provider");
        put("SaslServerFactory.PLAIN", "org.swap.provider.PlainSaslServerFactory");
    }

}

Putting it all together

// register provider
Security.addProvider(new MySecurityProvider());
// create callback
CallbackHandler myHandler = new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
    {
 for (Callback cb : callbacks)
        {
            if (cb instanceof ExternalValidationCallback)
            {
                ExternalValidationCallback evc = (ExternalValidationCallback) cb;
                // Add your password validation, unless bob works for you
                evc.setValidated("bob".equals(evc.getUsername() && "password".equals()evc.getPassword());
            }
            else if (cb instanceof AuthorizeCallback)
            {
                AuthorizeCallback ac = (AuthorizeCallback) cb;
                // Add your test to see if client is authorized to use requested
                ac.setAuthorized(true);
            }
            else
            {
                throw new UnsupportedCallbackException(cb, "Unrecognized Callback");
            }
    }
};

SaslServer ss = Sasl.createSaslServer("PLAIN", "myProtocol", serverName, null, myHandler);
ss.evaluateResponse(clientPacket);

Pivot Tree List labels and lazy loading

Here’s how to lazy load a tree structure in pivot and supply nice(er) labels for all nodes.

1. Create a LabeledList that extends List

public class LabeledList extends ArrayList
{

private V label;

public LabeledList(V label)
{
    this.label = label;
}

public V getLabel()
{
    return label;
}

@Override
public boolean equals(Object o)
{
    return (o instanceof LabeledList && ((LabeledList) o).label.equals(label)
           && super.equals(o));
}

@Override
public int hashCode()
{
    int hash = 5;
    hash = 29 * hash + (this.label != null ? this.label.hashCode() : 0)
            + super.hashCode();
    return hash;
}
}
,v>

2. Create a simple renderer to display your new list

public class NodeTreeViewRenderer extends TreeViewNodeRenderer
{

 @Override
 public void render(Object o, Path path, int i, TreeView tv, boolean bln,
         boolean bln1, NodeCheckState ncs, boolean bln2, boolean bln3)
 {
     if (o != null && o instanceof LabeledList)
     {
         super.render(((LabeledList) o).getLabel(), path, i, tv,
                 bln, bln1, ncs, bln2, bln3);
     } else
     {
         super.render(o, path, i, tv, bln, bln1, ncs, bln2, bln3);
     }
 }
}

3. Put it together.

@WTKX TreeView myTree;
...
...
public void startup(Display display,
          Map map) throws Exception
  {
      ....
      List rootList = new arrayList();
      rootList.add(new LabeledList(new Date()));
      myTree.setData(rootList);
  },>,>

Now we have the problem of loading all the child nodes when the tree is expanded. This is handled by creating a TreeViewBranchListener and expanding LabeledList a little to track if it’s been loaded or not.

In LabeledList, add the following:

    private boolean loaded = false;

  public boolean isLoaded()
  {
      return loaded;
  }

  public void setLoaded(boolean loaded)
  {
      this.loaded = loaded;
  }

Now create the Listener

public class LazyLoadBranchListener implements TreeViewBranchListener
{
  public void branchExpanded(TreeView tv, Path path)
  {

      List currList = tv.getTreeData();

      for (Integer i : path)
      {
          currList = (List) currList.get(i);
      }

      LabeledList lList = (LabeledList) currList;

     if (!lList.isLoaded())
      {
          // load data for into lList. Add additional LabeledLists for directories or
          // straight objects for end nodes.
          // For long running loads, do the load in another thread
          lList.setLoaded(true);
      }
  }

  public void branchCollapsed(TreeView tv, Path path)
  {}
}

In your application class, add the new listener

    myTree.getTreeViewBranchListeners().add(new LazyLoadBranchListener());