Monday, September 28, 2009

Last Week

It was not a quite productive week. I continued stress testing. Found a number of critical bugs, fixed them. Found a number of performance issues, fixed them too. Boring.

Reworked the callback system again. I did it a few times already. Hopefully this version is the final one. At least I do not see any significant weaknesses in it.

To be more specific, I reworked the way the messages were suspended/resumed. When Puppeteer executes a bulk operation, it normally suspends callbacks before starting and resumes them at the end. This is a standard performance approach.

This is how callbacks were implemented before the last week. The code here is simplified to be more clear:

public event EventHandler<ProjectEventArgs> OnProjectChange;

internal void Event(EventsMode aMode, object aSender, ProjectEventArgs aArgs)
{
if (mSuspendEventsCounter > 0)
{
if (mSuspendedEventsHead == null)
{
return;
}

SynchronizationContext context = Environment.SynchronizationContext;
if (context == null)
{
if (OnProjectChange != null)
{
OnProjectChange(aSender, aArgs);
}
return;
}

SendOrPostCallback handler = delegate(object aObject)
{
PuppeteerEventArgs args = aObject as ProjectEventArgs;
if (OnProjectChange != null)
{
OnProjectChange(aSender, aArgs);
}
};

if (aMode == EventsMode.Asynchronous)
{
context.Post(handler, aArgs);
}
else
{
context.Send(handler, aArgs);
}
}
}

internal void Event(ProjectEventArgs aArgs)
{
Event(Environment.EventsMode, aArgs);
}

public int SuspendEvents()
{
if (mSuspendEventsCounter < 0)
{
mSuspendEventsCounter = 0;
}
return ++mSuspendEventsCounter;
}

public int ResumeEvents()
{
if (mSuspendEventsCounter <= 0)
{
mSuspendEventsCounter = 0;
}
else
{
--mSuspendEventsCounter;
if (mSuspendEventsCounter <= 0)
{
Event(this, new ProjectEventArgs(ProjectAction.Reset));
}
}
return mSuspendEventsCounter;
}



As you can see the callbacks can be asynchronous, and they are thread safe. They are always executed in a synchronization context. Environment is an initialization class for the Puppeteer Asset Manager. This is how it is initialized in puppeteer's Main:

...
Environment.SynchronizationContext = SynchronizationContext.Current; Environment.EventsMode = EventsMode.Asynchronous;
...

Which means all events are executed in the main thread, and they are asynchronous. When the ResumeEvents is called it is sending Reset event. The controls reset their content on this event. The biggest drawback of this approach is that controls are loosing their state. For example, suppose you have a tree view of assets and containers:


Now you copy some assets to another container, and at the end of the operation discover that all brunches are closed an the selection is lost:

Not quite good. I tried to remove suspend/resume. All the messages are asynchronous anyway, so the performance hit during the bulk operation should not be too hard. Indeed, it is not. It hits me after the bulk operation. The SynchronizationContext.Post function is using Windows message queue to deliver the callback, so the event queue becomes overflowed. It still survives, but it takes a significant time for a program to return to a normal state.

The solution was to re-implement suspend/resume:

internal void Event(EventsMode aMode, ProjectEventArgs aArgs)
{
if (mSuspendEventsCounter > 0)
{
if (mSuspendedEventsHead == null)
{
mSuspendedEventsHead = aArgs;
mSuspendedEventsTail = aArgs;
}
else
{
mSuspendedEventsTail.Next = aArgs;
mSuspendedEventsTail = aArgs;
}
return;
}

SynchronizationContext context = Environment.SynchronizationContext;
if (context == null)
{
while (aArgs != null)
{
aArgs.Call();
aArgs = aArgs.Next;
}
return;
}

SendOrPostCallback handler = delegate(object aObject)
{
PuppeteerEventArgs args = aObject as ProjectEventArgs;
while (args != null)
{
args.Call();
args = args.Next;
}
};

if (aMode == EventsMode.Asynchronous)
{
context.Post(handler, aArgs);
}
else
{
context.Send(handler, aArgs);
}
}

// SuspendEbents stays the same

public int ResumeEvents()
{
if (mSuspendEventsCounter <= 0)
{
mSuspendEventsCounter = 0;
}
else
{
--mSuspendEventsCounter;
if (mSuspendEventsCounter <= 0 &amp;&amp; mSuspendedEventsHead != null)
{
PuppeteerEventArgs args = mSuspendedEventsHead;
ProjectEventArgs suspendOutput = new ProjectEventArgs(this, ProjectAction.SuspendOutput, this);
ProjectEventArgs resumeOutput = new ProjectEventArgs(this, ProjectAction.ResumeOutput, this);
suspendOutput.Next = args;
mSuspendedEventsTail.Next = resumeOutput;

mSuspendedEventsHead = null;
mSuspendedEventsTail = null;

Event(suspendOutput);
}
}
return mSuspendEventsCounter;
}



So all events since Suspend is called are collected to a linked list. Resume sends the whole list in one message. It also wraps them to a pair of SuspendOutput/ResumeOutput events. Controls can react on them by sending WM_SETREDRAW message.

It is not a huge change, and definitely it is not a rocket science. But it took a significant time for figuring it all out. This is just a good example of what I was doing last week.

No comments:

Post a Comment