Download JointCode.Shuttle and samples
Introduction
This article compares performance of two different technologies for making cross-AppDomain calls.
Background
Cross-AppDomain calls can be terrible. Tests show that cross-AppDomain method calls is hundreds to a thousand times slower than normal method calls within a same AppDomain.
Now we know that when an object is passed from an execution boundry (AppDomain) to another, it needs to be converted into a form that can be transferred; and then when it arrives, it’s rebuilt and converted back to the original form in memory. This process is called ‘marshaling’.
For example, in .NET Remoting, a MarshalByrefObject-derived object will be packed into a serializable ObjRef instance, this is what ‘ByRef’ in the type name means, it refers to the form of ObjRef.
By marshaling, it incurs too much overhead through the way, like reflections, security checks, serialization/deserialization, etc. With so many operations involved in, the performance punishment is inevitable, and unfortunately, we don’t have much change to optimize it.
As such, if you care about performance, it's recommended that you keep away from AppDomains, as we always do usually. But sometimes, you just needs to use another AppDomain, for example when you are implementing a plug-in architecture and needs to load and unload assemblies at runtime without stopping the application.
In these situations, a fast cross-AppDomain marshaler would be much helpful. This is where the JointCode.Shuttle comes in.
How to use it
In the JointCode.Shuttle distribution package, it contains two files: JointCode.Shuttle.dll and JointCode.Shuttle.Library.dll, where JointCode.Shuttle.dll is a library written in managed language, and JointCode.Shuttle.Library.dll is a component written in non-managed language, which is depended by the former.
Prepare
To use JointCode.Shuttle, we first need to refer to JointCode.Shuttle.dll in our project, and copy the JointCode.Shuttle.Library.dll to the folder where JointCode.Shuttle.dll is compiled (for example, after the project is compiled, if the JointCode.Shuttle.dll is copied to the c:/projects/sampleproject folder, you need to manually copy the JointCode.Shuttle.Library.dll to this folder).
Using the code
As the JointCode.Shuttle is interface-oriented, so we first need to create a service interface (also called service contract), and apply ServiceInterface
attribute to it.
[ServiceInterface]
public interface IServiceFunctionTest
{
}
Then we create a service class that implements the contract and apply the ServiceClass
attribute to it.
[ServiceClass(typeof(IServiceFunctionTest), Lifetime = LifetimeEnum.Transient)]
public class ServiceFunctionTest : IServiceFunctionTest
{
}
Because we want to make cross-AppDomain
calls, we need to write a class for starting/stopping the remote services and let it inherit from MarshalByRefObject
.
public abstract class RemoteServiceEnd : MarshalByRefObject
{
protected ShuttleDomain _shuttleDomain;
public void CreateShuttleDomain()
{
var key = this.GetType().Name;
_shuttleDomain = ShuttleDomainHelper.Create(key, key);
}
public void DisposeShuttleDomain()
{
if (_shuttleDomain != null)
_shuttleDomain.Dispose();
}
public abstract void RegisterServices();
public abstract void ConsumeServices();
}
public class RemoteServiceEnd1 : RemoteServiceEnd
{
public override void RegisterServices()
{
var guid = Guid.NewGuid();
_shuttleDomain.RegisterServiceGroup(ref guid,
new ServiceTypePair(typeof(ICommonService), typeof(CommonService)),
new ServiceTypePair(typeof(IServiceFunctionTest), typeof(ServiceFunctionTest)));
}
public override void ConsumeServices()
{
IFakeService fakeService;
if (_shuttleDomain.TryGetService(out fakeService))
{
Console.WriteLine("AppDomain [{0}], before calling the remote service: ", AppDomain.CurrentDomain.FriendlyName);
var result = fakeService.PrintAndReturn("JointCode.Shuttle");
Console.WriteLine("AppDomain [{0}], after calling the remote service with result [{1}] ", AppDomain.CurrentDomain.FriendlyName, result);
Console.WriteLine();
}
}
}
Now, we are ready to use JointCode.Shuttle, like this:
ShuttleDomain.Initialize();
_remoteDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, null);
_serviceEnd1 = (RemoteServiceEnd)_remoteDomain.CreateInstanceAndUnwrap
(_serviceEnd1AsmName, ServiceEnd1Type);
_serviceEnd1.CreateShuttleDomain();
var key = Guid.NewGuid().ToString();
_shuttleDomain = ShuttleDomainHelper.Create(key, key);
_serviceEnd1.RegisterServices();
_shuttleDomain.TryGetService(out _shuttleFunctionTest);
_shuttleFunctionTest.CallSimpleMethod();
Test
To compare the cross-AppDomain calling performance of JointCode.Shuttle and MarshalByrefObject and try to understand the various factors that might affect performance, I built a test. This test showed us two kind of benchmarks:
- Create remote service object and call object method repeatedly: This is to compare the performance of both calls as a whole, because the object creating and method invocation all costs.
- Create a remote service object and cache it into a local field, and then repeatedly call the object method: This is to measure the performance of method invocations with different kind and number of parameters, because the parameters needs to be marshaled across AppDomain, and that costs too. Please note that this is only for the convenience of testing, never cache remote service object to the field in practise. Because the lifetime of remote service object is controlled by the remote end, it is possible that the remote object has been expired and garbage collected at remote end without known by the local caller. Under this circumstance, if we continues to call its method, an exception will be thrown.
You can check out the full code for the benchmark application in the source code download. Here is the screenshot of part of the result :
Conclusion
The JointCode.Shuttle can significantly improve the performance of cross-AppDomain communication, and it brings many benefits that other technologies can not provide.
Future
JointCode.Shuttle is still a young project, and it lacks some features, such as service registering/unregistering notification, cross-AppDomain events, etc. The author will continue to improve existing functions, as well as introduce more new features in the future, but there are some limitations right now, including:
- Only supports 32-bit applications (x86 target platforms)
- Only supports Windows (only supports for .net framework, no mono support at this time)
- No events supported in the service interface
- No support for cross-AppDomain events
- Not thoroughly tested