2 Replies Latest reply on Sep 6, 2018 1:36 PM by mvisconte

    What is the most efficient way to use the summary function for a list of tags and time ranges.


      I use AF-SDK to retrieve history for a list of HistorianValues, defined as:


      public class HistorianValue
          public DateTime BeginTime { get; }
          public DateTime EndTime { get; }
          public string Name { get; }


      The aim is to get, for each item in the list, the average of the tag 'Name' between BeginTime and EndTime.

      The historian server is on a different continent than the server running this code, therefore, round-trip-times are huge, and I am trying to optimize the code.


      For the time being, I grouping the tags by time range, and call PIPointList.Summary for each time range, but that can amount for a good amount of calls. (Grouping by tag name is usually worse, as there is more different tag names than different time ranges).


       PIPagingConfiguration config = new PIPagingConfiguration(PIPageType.TagCount, 100);
      var groupsOfSameTimeRange = tagList.GroupBy(t => new { t.BeginTime, t.EndTime });
      foreach (var groupOfSameTimeRange in groupsOfSameTimeRange)
              AFTime AFBeginTime = new AFTime(groupOfSameTimeRange.Key.BeginTime);
              AFTime AFEndTime = new AFTime(groupOfSameTimeRange.Key.EndTime);
              AFTimeRange oAFTimeRange = new AFTimeRange() { StartTime = AFBeginTime, EndTime = AFEndTime };
              PIPointList PIPointList = new PIPointList(PIPoint.FindPIPoints(PIServer, groupOfSameTimeRange.Select(t => t.Name)));
              IEnumerable<IDictionary<AFSummaryTypes, AFValue>> PIAggregates = PIPointList.Summary(oAFTimeRange,  AFSummaryTypes.Average, AFCalculationBasis.TimeWeightedDiscrete, AFTimestampCalculation.Auto, config);
             //Put the values in the return object


      Is there any way to optimize this further, and, ideally, get the data for all (tags,range) couple in a single call?


      Thank you for your help.

        • Re: What is the most efficient way to use the summary function for a list of tags and time ranges.
          Rick Davin

          Greetings Maxime,


          I see opportunity for improvement with your code.  I think the loop is trying to do too much per iteration.  Instead, I would suggest you adhere more to a Separation of Concerns with your code.  For instance, retrieving any or all PI tags from a PI Data Archive can be separated from retrieving data in specific time ranges.


          I am not thrilled about the name HistorianValue because it really does not contain what one would think of as a historized value.  How is the List<HistorianValue> created?  Do you read items from a text file?  You may want to add a PIPoint property so that once you fetch a PIPoint object you may keep working with it rather than its name.


          Overall, I would want a structure of such an app to do the following:


          1. Read list of tag names and desired time range from some source.
          2. Without regard to time range, retrieve all PI tags once and in bulk from the PI Data Archive.  Save the PIPoint object back to your class based on matching name.  Once this is done, you should only work with the PIPoint and not the name anymore.
          3. Because you have different time ranges, you must unfortunately issue different PIPointList.Summary calls for each unique time range.  If you have 100 tags and each has a unique time range, then it would be the equivalent of making individual calls despite wrapping it inside a bulk Summary call.


          Towards that end, you may consider creating more methods that perform discrete steps.  Consider the following as a learning example:


          private IDictionary<PIPoint, HistorianValue> GetTagsInBulk(PIServer pida, List<HistorianValue> tagList)
              var pointDict = new Dictionary<PIPoint, HistorianValue>();
              // Temporarily convert simple tagList into a dictionary keyed by case-insenstive Name
              var nameMap = tagList.ToDictionary(k => k.Name, v => v, StringComparer.InvariantCultureIgnoreCase);
              var tags = PIPoint.FindPIPoints(pida, tagList.Select(x => x.Name), null);
              foreach (var tag in tags)
                  pointDict[tag] = nameMap[tag.Name];
              return pointDict;
          private IDictionary<AFTimeRange, PIPointList> GetPointsPerTimeRange(IDictionary<PIPoint, HistorianValue> pointDict)
              var dict = new Dictionary<AFTimeRange, PIPointList>();
              foreach (var item in pointDict)
                  var timeRange = new AFTimeRange(item.Value.BeginTime, item.Value.EndTime);
                  if (!dict.TryGetValue(timeRange, out PIPointList pointList))
                      pointList = new PIPointList();
                   dict[timeRange] = pointList;
              return dict;


          The GetTagsInBulk method gets all PIPoint objects once.  At this point, you would prefer working specifically with the PIPoint object rather than a Name.


          The GetPointsPerTimeRange gets the unique time ranges - as AFTimeRange - as well as builds a PIPointList of the points associated with that time range.  UPDATE: I added line #30 to make sure we write the pointList back to the dict entry.


          Now you can modify your code to incorporate those methods:


          PIPagingConfiguration config = new PIPagingConfiguration(PIPageType.TagCount, 100);
          // Fetch all PIPoints once, and let them be a key to a dictionary of HistorianValue.
          IDictionary<PIPoint, HistorianValue> pointDict = GetTagsInBulk(piserver, tagList);
          IDictionary<AFTimeRange, PIPointList> pointsPerTimeRange = GetPointsPerTimeRange(pointDict);
          foreach (var item in pointsPerTimeRange)
              // These variables are for readability and not really needed.
              AFTimeRange timeRange = item.Key;
              PIPointList pointList = item.Value;
              IEnumerable<IDictionary<AFSummaryTypes, AFValue>> PIAggregates = pointList.Summary(timeRange, AFSummaryTypes.Average, AFCalculationBasis.TimeWeightedDiscrete, AFTimestampCalculation.Auto, config);
              //Put the values in the return object