While we were working on performance improvement of our project, we found that one foreach loop is taking long time to execute and hence, decreasing the performance rapidly. The code looked like something following -
if (!String.IsNullOrWhiteSpace(lstScreenIds))
{
// Create business class object
BLVisitList objVisitList = new BLVisitList();
// Loop through each Screen and call DoPrevInfo
var screenIds = lstScreenIds.Split(',').ToList();
foreach (var screenId in screenIds)
{
objVisitList.DoPrevInfo(ActualUserId, PatientId, ActualRoleId, visitId, screenId, IpAddress);
}
}
Here, lstScreenIds is a comma-separated list of some screen-ids and DoPrevInfo was supposed to be called for each screen id in the list.
DoPrevInfo is doing some big Inserts and Updates in DB and hence, if the number of screen ids is more (generally we had some 20-25 screen ids), it was taking huge time to execute and complete the operation. It was taking almost 50 seconds to complete the operation.
This 50 seconds is the sum of the times taken by the method for all the screen ids. Say, for screenId 'A', the method takes 10 seconds to execute, for 'B' it takes 15 seconds and for 'C' it takes 25 seconds - as the method is called for one screenId only after it completes the execution for the previous one (in serial way), so, the total time taken is (10 + 15 + 25) = 50 seconds.
To fix this, we used parallel processing approach as I am describing below. We changed the code to use tasks -
if (!String.IsNullOrWhiteSpace(lstScreenIds))
{
BLVisitList objVisitList = new BLVisitList();
//Loop through each Screen and Call the DoPrevInfo Function
var screenIds = lstScreenIds.Split(',').ToList();
// This line creates a list of tasks that we will be using in future
List<Task> tasks = new List<Task>();
// Saving the current context
var current = HttpContext.Current;
foreach (var screenId in screenIds)
{
// Starting a new task for each screenId to make it execute parallely
tasks.Add(Task.Factory.StartNew(() =>
{
// Setting current context
HttpContext.Current = current;
// Call the method
objVisitList.DoPrevInfo(ActualUserId, PatientId, ActualRoleId, visitId, screenId, IpAddress);
}));
}
// Wait until all the tasks are over, then leave the code block.
Task.WaitAll(tasks.ToArray());
}
With this change, we made the foreach loop to use separate tasks for separate screen id and go on parallel way - at the same time. So, the time taken now is the longest time that is taken by the method for individual screenIds.
To explain, from our above example, now the complete process will take 25 seconds (that is the maximum time among 'A'/'B'/'C' the method takes to complete execution) - given that, the execution for the other screen ids (10 seconds and 15 seconds respectively) will be completed within that maximum time span!
This approach made our code to execute only in 9 seconds from 50 seconds (Obviously that is not 'only' if you are concerned on performance, I am saying from the old time-taken reference :) ) without touching/changing/optimizing the DoPrevInfo method or any DB query/stroed-procedure - This is only due to this tasking.
Please refer to the comments to understand what the code is doing. I will still explain some code and why we needed these.
1. Inside DoPrevInfo method, we were accessing HttpContext.Current object, but after the task implementation, we found that we are not getting the HttpContext.Current object value being inside tasks. So, there we need to set the HttpContext.Current object value everytime we enter a task.
var current = HttpContext.Current; - This line fetches the Current object value being outside the task and after that, we are setting the value by
HttpContext.Current = current; inside the task. This works fine :)
2. Task.WaitAll(tasks.ToArray()); - This line makes the program control to wait for all the tasks to complete before leaving the code block - Otherwise, the control leaves the code block after the first task is completed.


