Friday, April 1, 2011

WCF: Memory leak with TypedMessageConverter when using XmlSerializer

Ok, hint is given in the title, but here is the code for you to observe and tell before you go any further what kind exactly of memory leak its causing and why :).

   1: private Message CreateResponseMessage(GetServiceStatusResponse result, Message message)
   2: {
   3:     TypedMessageConverter converter = TypedMessageConverter.Create(typeof(getServiceStatusResponse1), "*", "http://www.sitronics.com/V2/SCAdapter", new XmlSerializerFormatAttribute() );
   4:     
   5:     Message reply = converter.ToMessage(new getServiceStatusResponse1
   6:     {
   7:         GetServiceStatusResponse = result,
   8:         OutboundServiceData = new OutboundServiceData { MsgCorrelations = new OutboundServiceDataMsgCorrelations { CorrelationID = RouterService.CorrelationId } }
   9:     }, OperationContext.Current.IncomingMessageVersion);
  10:  
  11:  
  12:     RouterService.CopyRequestToReply(message, reply);
  13:  
  14:     return reply;
  15: }

Previous debugging points into classic assembly heap loader leak with XmlSerializer temp assemblies (here is the example for how to debug from Tess: http://blogs.msdn.com/b/tess/archive/2006/02/15/532804.aspx)

It is obvious that there should be XmlSerializer somewhere inside. Road to finding it starts with how the TypedMessageConverter is created:

   1: public static TypedMessageConverter Create(Type messageContract, string action, string defaultNamespace, XmlSerializerFormatAttribute formatterAttribute)
   2: {
   3:     if (messageContract == null)
   4:     {
   5:         throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ArgumentNullException("messageContract"));
   6:     }
   7:     if (defaultNamespace == null)
   8:     {
   9:         defaultNamespace = "http://tempuri.org/";
  10:     }
  11:     return new XmlMessageConverter(GetOperationFormatter(messageContract, formatterAttribute, defaultNamespace, action));
  12: }
  13:  

I’ll cut the story saying that inside that routes to creation of SerializerGenerationContext to instantiate the required serializers:

   1: private XmlSerializer[] GenerateSerializers()
   2: {
   3:     List<XmlMembersMapping> list = new List<XmlMembersMapping>();
   4:     int[] numArray = new int[this.Mappings.Count];
   5:     for (int i = 0; i < this.Mappings.Count; i++)
   6:     {
   7:         XmlMembersMapping item = this.Mappings[i];
   8:         int index = list.IndexOf(item);
   9:         if (index < 0)
  10:         {
  11:             list.Add(item);
  12:             index = list.Count - 1;
  13:         }
  14:         numArray[i] = index;
  15:     }
  16:     XmlSerializer[] serializerArray = this.CreateSerializersFromMappings(list.ToArray(), this.type);
  17:     if (list.Count == this.Mappings.Count)
  18:     {
  19:         return serializerArray;
  20:     }
  21:     XmlSerializer[] serializerArray2 = new XmlSerializer[this.Mappings.Count];
  22:     for (int j = 0; j < this.Mappings.Count; j++)
  23:     {
  24:         serializerArray2[j] = serializerArray[numArray[j]];
  25:     }
  26:     return serializerArray2;
  27: }
  28:  

This “pre-cached” set of serializers is then used:

   1: internal XmlSerializer GetSerializer(int handle)
   2: {
   3:     if (handle < 0)
   4:     {
   5:         return null;
   6:     }
   7:     if (this.serializers == null)
   8:     {
   9:         lock (this.thisLock)
  10:         {
  11:             if (this.serializers == null)
  12:             {
  13:                 this.serializers = this.GenerateSerializers();
  14:             }
  15:         }
  16:     }
  17:     return this.serializers[handle];
  18: }
  19:  
  20:  

And just to complete the cycle of information, here is the inners of the XmlSerializer to return serializers from mappings:

   1: [PermissionSet(SecurityAction.LinkDemand, Name="FullTrust")]
   2: public static XmlSerializer[] FromMappings(XmlMapping[] mappings, Type type)
   3: {
   4:     if ((mappings == null) || (mappings.Length == 0))
   5:     {
   6:         return new XmlSerializer[0];
   7:     }
   8:     XmlSerializerImplementation contract = null;
   9:     Assembly assembly = (type == null) ? null : TempAssembly.LoadGeneratedAssembly(type, null, out contract);
  10:     TempAssembly tempAssembly = null;
  11:     if (assembly == null)
  12:     {
  13:         if (XmlMapping.IsShallow(mappings))
  14:         {
  15:             return new XmlSerializer[0];
  16:         }
  17:         if (type != null)
  18:         {
  19:             return GetSerializersFromCache(mappings, type);
  20:         }
  21:         tempAssembly = new TempAssembly(mappings, new Type[] { type }, null, null, null);
  22:         XmlSerializer[] serializerArray = new XmlSerializer[mappings.Length];
  23:         contract = tempAssembly.Contract;
  24:         for (int j = 0; j < serializerArray.Length; j++)
  25:         {
  26:             serializerArray[j] = (XmlSerializer) contract.TypedSerializers[mappings[j].Key];
  27:             serializerArray[j].SetTempAssembly(tempAssembly, mappings[j]);
  28:         }
  29:         return serializerArray;
  30:     }
  31:     XmlSerializer[] serializerArray2 = new XmlSerializer[mappings.Length];
  32:     for (int i = 0; i < serializerArray2.Length; i++)
  33:     {
  34:         serializerArray2[i] = (XmlSerializer) contract.TypedSerializers[mappings[i].Key];
  35:     }
  36:     return serializerArray2;
  37: }
  38:  
  39:  
  40:  
  41:  

So, now we can quite easily say what would be the problem of implementation on the top. Unmercifully, it leads to generating temp assemblies for the types in question every time it is called. Code snippets provided are to confirm and understand when/why that would happen or not.

So solution will be to “cache” the instance of TypedMessageConverter per type required, same as a usual solution when using XmlSerializer itself with “non-caching” constructors.

No comments: