Thursday, September 3, 2009

Darkstar Data structures comparison



This is lifted from a live discussion at the Darkstar forums. I thought maybe it deserved a more structured housing: Original discussion

Application description:

It's like a collaborative powerpoint presentation. There are many slides and each of the slides has a large number of users who are contributing content to it. When a user is on the same slide as me, that user sees the content I contribute. This is implemented with a channel for each slide. There is also a global channel on which a ping is sent to you each time any content is contributed, even if you are on a different slide from the originator. When you move to a slide it is necessary to obtain all of the content history from that slide, as well as to become receptive to all of the new content from that point until you leave.

Our load testing involves hundreds of simulated users moving in tandem from slide to slide. This means that at every new join they are contending heavily for the archived content.

Description over.

We've been trying several ways of optimizing these structures because it turns out that contention is basically what kills Darkstar performance - when two transactions go for the same object in the datastore, one of them has to cancel and try again later. If it was itself already the proud holder of the locks on some other objects that locking is wasted and it all has to happen again later.

Second attempt:


This one involves cloning the keyset from the map of users for that slide. I'm cloning them because I theorize that I can release the ManagedObject faster that way - as soon as I exit my transaction I will have put it back in the store, whereas if I passed the keys literally they might still be locking until the subtask released them. I could be wrong...

At each stage of these moveSlide is the first server call post message switching.


private void moveSlide(String destination, ByteBuffer message) {
Integer.parseInt(destination);//Just checking.
ClientSession session = session();
String key = session.getName()+"@"+destination;
ScalableHashMap slide;
try{
slide= (ScalableHashMap)AppContext.getDataManager().getBinding(destination);
}
catch(NameNotBoundException e){//If this slide does not previously exist
slide = new ScalableHashMap();
AppContext.getDataManager().setBinding(destination, slide);
}
for(Object userObject : slide.keySet())
{
String user = userObject.toString();
ScalableList userData = (ScalableList)slide.get(userObject);
AppContext.getTaskManager().scheduleTask(new StartDistributingUserStrokes(userData.toArray(), destination, user));
}

AppContext.getChannelManager().getChannel(GLOBAL_CHANNEL_NAME).send(session,
toMessageBuffer("/MOVE_SLIDE "+session.getName()+" "+destination));
}


So, for each cloned key spawn a subtask which does all the work of StartDistributingUserStrokes. As you can guess, that distributes the user strokes:


private class StartDistributingUserStrokes implements Serializable, Task
{
private Object[] allWork;
private String slide;
private String user;
private int packageSize;
private StartDistributingUserStrokes(Object[] userData, String destination, String author) {
slide = destination;
this.allWork = userData;
user = author;
packageSize = allWork.length;
}

@Override
public void run() throws Exception {
ClientSession session = session();
if(allWork.length!= 0)
{
Object[] strokes = allWork;
session.send(toMessageBuffer(strokes[0].toString()));
Object[] remainingWork = new Object[strokes.length -1];
if(strokes.length > 1)
{
for(int i =1; i < strokes.length; i++)
{
remainingWork[i-1] = strokes[i];
}
}
AppContext.getTaskManager().scheduleTask(new StartDistributingUserStrokes(remainingWork, slide, user));
}
else if(allWork.length == 0){
session.send(toMessageBuffer("/ALL_CONTENT_SENT "+slide));
session.send(toMessageBuffer("/PING "+user+" "+slide+" "+packageSize));
return;
}
}
}

No comments:

Post a Comment