2

I have a .net web service (Web API 2) that takes requests to get data from a database. The returned data is not very large, just 1-3 kb usually (about 1-15 rows from the database). I set it up in IIS and ran some bulk testing on it, where it gets hit a lot for an hour or so, so it is getting several thousand requests. When I look at the w3wp.exe (IIS Worker process), the memory just keeps increasing until it gets to over 5 gb and the CPU usage gets near 100% and then eventually the web service stops working. The server has 16 gb of RAM, we just increased it from 8. Is 5 gb of memory normal? That seems like a lot, I would have assumed garbage collection would handle this better. I am not very experienced in using the performance monitor or other things to troubleshoot problems like this.

Any suggestions on what I can look at? Could there be a memory leak? Should I try having the app pool recycle when it hits a certain memory amount?

Update- we have another .net web service that does not even access a database or any external files or anything, and it is acting the same way- memory just keeps increasing. Our plan is to probably set the app pool for each to recycle every x minutes or when it gets to a certain memory amount.

Here is an example of a typical function in the API:

[Route("patient/{patientKey}/activations")]
public async Task<IHttpActionResult> GetActivations(int patientKey)
{
     try
     {
          ActivationList actList = new ActivationList();

          actList.Activations = await _actProc.GetByPatient(patientKey);

          return Ok<ActivationList>(actList);
     }
     catch (Exception ex)
     {
         return new BadRequestWithInfoResult(Translators.makeXML<ErrorReturn>(CreateError(ex)));
     }
}

public async Task<List<Activation>> GetByPatient(long patientKey)
{
     using (var dbConn = _database.CreateConnection())
     {
          List<DBActivation> lst_Activation = await dbConn.FetchAsync<DBActivation>("select * from fact.Activation where PatientKey = " + patientKey.ToString());

          List<Activation> Activations = lst_Activation.Select(x => _mapper.Map<Activation>(x)).ToList<Activation>();              

          return Activations;
     }
}
Kelly
  • 945
  • 2
  • 18
  • 31
  • Try fetching patients with normal ado.net objects, like dbsets or datareaders and others. It seems the problem not related to above two methods. – isaeid Apr 16 '19 at 14:22
  • @isaeid I suppose I could try that. I am using NPoco, an open source ORM. – Kelly Apr 16 '19 at 14:25
  • @isaeid That would probably only serve to make things worse. DataSet is notoriously memory heavy. As for a data reader, Micro ORM's don't use that much memory on top of them. – mason Apr 16 '19 at 14:29
  • Rather than guessing what's using your memory, perhaps you should actively investigate. Profile your memory usage using a tool. That will tell you what's using all the memory. – mason Apr 16 '19 at 14:30
  • @mason I am trying that, I am so beginner on this, I'm not sure what's best. I am looking at Processor Explorer (looking at the .net performance tab of w3wp.exe), but haven't found anything that strikes me or know what to make of the results. How high is too high? What does it mean the % of time in GC is only 1 or 2, is that bad? The Gen 2 heap size is bigger than Gen 1 heap size, but I don't know if they're too big. – Kelly Apr 16 '19 at 14:35
  • @mason, yes dataset consumes a lot of memory, but not as high as above, and kelly can try with datareader or other orms, Probably the problem is from pocco orm – isaeid Apr 16 '19 at 14:36
  • @isaeid I highly doubt the problem is with NPoco. Anyways, rather than just taking the shotgun blast approach of swapping things out at random, the proper thing to do is actually investigate what's in memory using a profiler. – mason Apr 16 '19 at 15:29
  • @Kelly Process Explorer can tell you how much memory is being used by the application, but it won't tell you how that memory is allocated. What you need to find is a profiler for .NET memory. I'm sure if you do a web search for that, you'll find plenty of tools. – mason Apr 16 '19 at 15:52
  • 1
    I have a hunch it might be your mapping causing the performance, could you try doing `_mapper.Map>(lst_Activation)` – penleychan Apr 16 '19 at 16:04
  • @penleychan I tried that, it did not make a difference. I was actually only doing that on that class only, too- every other function doing mapping had that better "syntax" – Kelly Apr 16 '19 at 21:08
  • I used Process Explorer and Redgate's ANTS Memory Profiler but they aren't revealing much to me except that System.Threading is a class identified as a potential culprit. I'm returning Tasks as the return type to the functions but not doing any explicit multithreading myself. – Kelly Apr 16 '19 at 21:12

3 Answers3

0

What's in the Activation object? If activation has many children, during the mapping, it might try to populate all the children and children of the children and on... If you are using EF lazy loading, EF will create a subquery for each child population.

brian
  • 664
  • 6
  • 8
  • In this case, the fetching of the database is done directly and with the statements of SQl, and as you can see, related datas not fetching with sql command. – isaeid Apr 16 '19 at 19:49
  • Activation has several fields, no children of children. Not using lazy loading – Kelly Apr 16 '19 at 21:13
0

OP answer- I found some offending code (and no, it's not normal for a web service like this to use more than 5 gb of memory). A while back, when trying to do my own namespaces in the xml returned by the web service, I had added the class CustomNamespaceXmlFormatter specified in this post under the answer by @Konamiman: Remove namespace in xml The commenter below it mentions a memory leak issue. While the ANTS Memory Profiler never showed my web service generating multiple dynamic assemblies, I nevertheless updated the code to use something similar to a singleton pattern for creating the instances of XmlSerializer like below and now my memory usage is way under control (and actually goes down when it's done processing requests!).

public class CustomNamespaceXmlFormatter : XmlMediaTypeFormatter
{
    private readonly string defaultRootNamespace;

    public CustomNamespaceXmlFormatter() : this(string.Empty)
    {
    }

    public CustomNamespaceXmlFormatter(string defaultRootNamespace)
    {
        this.defaultRootNamespace = defaultRootNamespace;
    }

    public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
    {
        if (type == typeof(String))
        {
            //If all we want to do is return a string, just send to output as <string>value</string>
            return base.WriteToStreamAsync(type, value, writeStream, content, transportContext);
        }
        else
        {
            XmlRootAttribute xmlRootAttribute = (XmlRootAttribute)type.GetCustomAttributes(typeof(XmlRootAttribute), true)[0];
            if (xmlRootAttribute == null)
                xmlRootAttribute = new XmlRootAttribute(type.Name)
                {
                    Namespace = defaultRootNamespace
                };
            else if (xmlRootAttribute.Namespace == null)
                xmlRootAttribute = new XmlRootAttribute(xmlRootAttribute.ElementName)
                {
                    Namespace = defaultRootNamespace
                };

            var xns = new XmlSerializerNamespaces();
            xns.Add(string.Empty, xmlRootAttribute.Namespace);

            return Task.Factory.StartNew(() =>
            {
                //var serializer = new XmlSerializer(type, xmlRootAttribute); **OLD CODE**
                var serializer = XmlSerializerInstance.GetSerializer(type, xmlRootAttribute);
                serializer.Serialize(writeStream, value, xns);                    
            });
        }
    }
}

public static class XmlSerializerInstance
{
    public static object _lock = new object();
    public static Dictionary<string, XmlSerializer> _serializers = new Dictionary<string, XmlSerializer>();
    public static XmlSerializer GetSerializer(Type type, XmlRootAttribute xra)
    {
        lock (_lock)
        {
            var key = $"{type}|{xra}";
            if (!_serializers.TryGetValue(key, out XmlSerializer serializer))
            {
                if (type != null && xra != null)
                {
                    serializer = new XmlSerializer(type, xra);
                }

                _serializers.Add(key, serializer);
            }

            return serializer;
        }
    }
}
Kelly
  • 945
  • 2
  • 18
  • 31
-1

Seems to be a deadlock because of not using ConfigureAwait in the library,

[Route("patient/{patientKey}/activations")]
public async Task<IHttpActionResult> GetActivations(int patientKey)
{
     try
     {
          ActivationList actList = new ActivationList();

          actList.Activations = await _actProc.GetByPatient(patientKey).ConfigureAwait(false);

          return Ok<ActivationList>(actList);
     }
     catch (Exception ex)
     {
         return new BadRequestWithInfoResult(Translators.makeXML<ErrorReturn>(CreateError(ex)));
     }
}

public async Task<List<Activation>> GetByPatient(long patientKey)
{
     using (var dbConn = _database.CreateConnection())
     {
          List<DBActivation> lst_Activation = await dbConn.FetchAsync<DBActivation>("select * from fact.Activation where PatientKey = " + patientKey.ToString()).ConfigureAwait(false);

          List<Activation> Activations = lst_Activation.Select(x => _mapper.Map<Activation>(x)).ToList<Activation>();              

          return Activations;
     }
}
M P
  • 280
  • 1
  • 6
  • I don't think ConfigureAwait is not needed here. The service is working fine for thousands of requests (done very quickly) and then sends http 503 "service unavailable" message and stops working until the app pool is recycled. Trying to see how I can get it to handle its own memory better. – Kelly Apr 17 '19 at 13:27