Everything I'm told says that WCF should be at least as fast as remoting. I have a specific scenario here, however, where it isn't even close, and I'm wondering if someone can spot something obvious that I'm doing wrong. I'm looking into the possibility of replacing remoting with wcf for the in-process intra-appdomain communication heavy lifting. Here's the code:
[ServiceContract]
interface IWorkerObject
{
[OperationContract] Outcome DoWork(Input t);
}
[DataContract]
[Serializable]
class Input
{
[DataMember] public int TaskId { get; set; }
[DataMember] public int ParentTaskId { get; set; }
[DataMember] public DateTime DateCreated { get; set; }
[DataMember] public string TextData { get; set; }
[DataMember] public byte[] BinaryData { get; set; }
}
[DataContract]
[Serializable]
class Outcome
{
[DataMember] public string Result { get; set; }
[DataMember] public string TextData { get; set; }
[DataMember] public byte[] BinaryData { get; set; }
}
class Program
{
static void Main(string[] args)
{
run_rem_test();
run_wcf_test();
run_rem_test();
run_wcf_test();
}
static void run_rem_test()
{
var dom = AppDomain.CreateDomain("remoting domain", null);
var obj = dom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as IWorkerObject;
RunTest("remoting", obj);
AppDomain.Unload(dom);
}
static void run_wcf_test()
{
var dom = AppDomain.CreateDomain("wcf domain", null);
var dcnt = dom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as WorkerObject;
var fact = new ChannelFactory<IWorkerObject>(new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");
var chan = fact.CreateChannel();
dcnt.OpenChannel();
RunTest("wcf", chan);
fact.Close();
dcnt.CloseChannel();
AppDomain.Unload(dom);
}
static void RunTest(string test, IWorkerObject dom)
{
var t = new Input()
{
TextData = new string('a', 8192),
BinaryData = null,
DateCreated = DateTime.Now,
TaskId = 12345,
ParentTaskId = 12344,
};
var sw = System.Diagnostics.Stopwatch.StartNew();
for( var i = 0; i < 1000; i++ )
dom.DoWork(t);
sw.Stop();
Console.WriteLine("{1} test run in {0}ms", sw.ElapsedMilliseconds, test);
}
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class WorkerObject : MarshalByRefObject, IWorkerObject
{
ServiceHost m_host;
public void OpenChannel()
{
m_host = new ServiceHost(this);
m_host.AddServiceEndpoint(typeof(IWorkerObject), new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");
m_host.Open();
}
public void CloseChannel()
{
m_host.Close();
}
public Outcome DoWork(Input t)
{
return new Outcome()
{
TextData = new string('b', 8192),
BinaryData = new byte[1024],
Result = "the result",
};
}
}
When I run this code I get numbers that look like this:
remoting test run in 386ms wcf test run in 3467ms remoting test run in 499ms wcf test run in 1840ms
UPDATE: So it turns out that it's just the initial setup that is so costly for WCF (Thanks, Zach!). Because I was recreating the AppDomains in each test, I was paying that price over and over. Here's the updated code:
[ServiceContract]
interface IWorkerObject
{
[OperationContract] Outcome DoWork(Input t);
}
[DataContract]
[Serializable]
class Input
{
[DataMember] public int TaskId { get; set; }
[DataMember] public int ParentTaskId { get; set; }
[DataMember] public DateTime DateCreated { get; set; }
[DataMember] public string TextData { get; set; }
[DataMember] public byte[] BinaryData { get; set; }
}
[DataContract]
[Serializable]
class Outcome
{
[DataMember] public string Result { get; set; }
[DataMember] public string TextData { get; set; }
[DataMember] public byte[] BinaryData { get; set; }
}
class Program
{
static void Main(string[] args)
{
var rem_dom = AppDomain.CreateDomain("remoting domain", null);
var rem_obj = rem_dom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as IWorkerObject;
var wcf_dom = AppDomain.CreateDomain("wcf domain", null);
var mgr_obj = wcf_dom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as WorkerObject;
var fact = new ChannelFactory<IWorkerObject>(new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");
var wcf_obj = fact.CreateChannel();
var rem_tot = 0L;
var wcf_tot = 0L;
mgr_obj.OpenChannel();
for( var i = 0; i < 10; i++ )
{
rem_tot += RunTest("remoting", i, rem_obj);
wcf_tot += RunTest("wcf", i, wcf_obj);
}
fact.Close();
mgr_obj.CloseChannel();
AppDomain.Unload(rem_dom);
AppDomain.Unload(wcf_dom);
Console.WriteLine();
Console.WriteLine("remoting total: {0}", rem_tot);
Console.WriteLine("wcf total: {0}", wcf_tot);
}
static long RunTest(string test, int iter, IWorkerObject dom)
{
var t = new Input()
{
TextData = new string('a', 8192),
BinaryData = null,
DateCreated = DateTime.Now,
TaskId = 12345,
ParentTaskId = 12344,
};
var sw = System.Diagnostics.Stopwatch.StartNew();
for( var i = 0; i < 1000; i++ )
dom.DoWork(t);
sw.Stop();
Console.WriteLine("{1,-8} {2,2} test run in {0}ms", sw.ElapsedMilliseconds, test, iter);
return sw.ElapsedMilliseconds;
}
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class WorkerObject : MarshalByRefObject, IWorkerObject
{
ServiceHost m_host;
public void OpenChannel()
{
m_host = new ServiceHost(typeof(WorkerObject));
m_host.AddServiceEndpoint(typeof(IWorkerObject), new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");
m_host.Open();
}
public void CloseChannel()
{
m_host.Close();
}
public Outcome DoWork(Input t)
{
return new Outcome()
{
TextData = new string('b', 8192),
BinaryData = new byte[1024],
Result = "the result",
};
}
}
This code gives numbers like this:
remoting 0 test run in 377ms wcf 0 test run in 2255ms remoting 1 test run in 488ms wcf 1 test run in 353ms remoting 2 test run in 507ms wcf 2 test run in 355ms remoting 3 test run in 495ms wcf 3 test run in 351ms remoting 4 test run in 484ms wcf 4 test run in 344ms remoting 5 test run in 484ms wcf 5 test run in 354ms remoting 6 test run in 483ms wcf 6 test run in 346ms remoting 7 test run in 491ms wcf 7 test run in 347ms remoting 8 test run in 485ms wcf 8 test run in 358ms remoting 9 test run in 494ms wcf 9 test run in 338ms remoting total: 4788 wcf total: 5401
For the most part, the time is spent setting up and tearing down the WCF channel. The other lions share of time appears to be consumed by running within a debugger. Keep in mind this is really informal type testing.. :)
I was able to reproduce your numbers, so i started from there. This showed WCF numbers were roughly 3xs greater than the remoting numbers.
By caching the channel factory (and respective remoting objects), the numbers dropped so that WCF was only 2x's greater than the remoting numbers.
After trying a dozen other different tweaks, the only one that seemed to shave any real measurable time off the numbers was to clear all the debug service behaviors (added by default) - this resulted in roughly ~100ms. Not enough to really get excited about.
Then, on a whim, I ran the code outside the debugger, in a release configuration. The numbers dropped to roughly the equivalent of the remoting numbers, if not better. Forehead smacked the desk and called it done.
In a nutshell, you should see roughly the same, with a possibility of better performance using WCF and gain a better programming model to boot.
remoting 1 test run in 347ms
wcf 1 test run in 1544ms
remoting 2 test run in 493ms
wcf 2 test run in 324ms
remoting 3 test run in 497ms
wcf 3 test run in 336ms
remoting 4 test run in 449ms
wcf 4 test run in 289ms
remoting 5 test run in 448ms
wcf 5 test run in 284ms
remoting 6 test run in 447ms
wcf 6 test run in 282ms
remoting 7 test run in 439ms
wcf 7 test run in 281ms
remoting 8 test run in 441ms
wcf 8 test run in 278ms
remoting 9 test run in 441ms
wcf 9 test run in 278ms
remoting 10 test run in 438ms
wcf 10 test run in 286ms
Note - this code has been completely bastardized. I apologize. :)
using System;
using System.ServiceModel;
using System.Runtime.Serialization;
[ServiceContract]
interface IWorkerObject
{
[OperationContract]
Outcome DoWork(Input t);
}
[DataContract]
[Serializable]
class Input
{
[DataMember]
public int TaskId { get; set; }
[DataMember]
public int ParentTaskId { get; set; }
[DataMember]
public DateTime DateCreated { get; set; }
[DataMember]
public string TextData { get; set; }
[DataMember]
public byte[] BinaryData { get; set; }
}
[DataContract]
[Serializable]
class Outcome
{
[DataMember]
public string Result { get; set; }
[DataMember]
public string TextData { get; set; }
[DataMember]
public byte[] BinaryData { get; set; }
}
class Program
{
static AppDomain dom = AppDomain.CreateDomain("wcf domain", null);
static WorkerObject dcnt = dom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as WorkerObject;
static ChannelFactory<IWorkerObject> fact = new ChannelFactory<IWorkerObject>(new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");
static IWorkerObject chan = fact.CreateChannel();
static AppDomain remdom = AppDomain.CreateDomain("remoting domain", null);
static IWorkerObject remoteObject;
static void Main(string[] args)
{
remoteObject = remdom.CreateInstanceFromAndUnwrap(System.Reflection.Assembly.GetExecutingAssembly().Location, typeof(WorkerObject).FullName) as IWorkerObject;
dcnt.OpenChannel();
int numberOfIterations = 10;
for (int i = 1; i <= numberOfIterations; i++)
{
run_rem_test(i);
run_wcf_test(i);
}
fact.Close();
dcnt.CloseChannel();
AppDomain.Unload(dom);
AppDomain.Unload(remdom);
}
static void run_rem_test(int iteration)
{
RunTest("remoting " + iteration, remoteObject);
}
static void run_wcf_test(int iteration)
{
RunTest("wcf " + iteration, chan);
}
static void RunTest(string test, IWorkerObject dom)
{
var t = new Input()
{
TextData = new string('a', 8192),
BinaryData = null,
DateCreated = DateTime.Now,
TaskId = 12345,
ParentTaskId = 12344,
};
var sw = System.Diagnostics.Stopwatch.StartNew();
for (var i = 0; i < 1000; i++)
dom.DoWork(t);
sw.Stop();
Console.WriteLine("{1} test run in {0}ms", sw.ElapsedMilliseconds, test);
}
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class WorkerObject : MarshalByRefObject, IWorkerObject
{
ServiceHost m_host;
public void OpenChannel()
{
m_host = new ServiceHost(typeof(WorkerObject));
m_host.AddServiceEndpoint(typeof(IWorkerObject), new NetNamedPipeBinding(NetNamedPipeSecurityMode.None), "net.pipe://localhost/the_channel");
// cache our ServiceBehaviorAttribute, clear all other behaviors (mainly debug)
// and add our ServiceBehavior back
//
var b = m_host.Description.Behaviors[0] as ServiceBehaviorAttribute;
m_host.Description.Behaviors.Clear();
m_host.Description.Behaviors.Add(b);
m_host.Open();
}
public void CloseChannel()
{
m_host.Close();
}
public Outcome DoWork(Input t)
{
return new Outcome()
{
TextData = new string('b', 8192),
BinaryData = new byte[1024],
Result = "the result",
};
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With