2

I am simply trying to utilize different BusinessHours and the BusinessHours class to determine if a given date is within x number of business days from another date. The following are true:

  1. The hour does not come into play
  2. Business hours are NOT set to 24 hours
  3. No every day is a business day (sun and mon are not for example)
  4. Holidays need to be taken into account

Reason for the question: This does not perform well on bulk so I am looking for a optimization help

What I am Doing Now

Initial Method

public boolean isLast_x_Days(Date st, Integer days){

    //days to date is a map that store previous calculation for a given number of days
    if(!days_to_date.containsKey(days)){
        get_date_x_days_back(st,days);
    }
    return st >= days_to_date.get(days);

}

Get date that is X days back

    public Date get_date_x_days_back(Date s, Integer days){
        Integer cnt = days;


        //If start date is in the future return
        //End date is defined in the class
        if(s > end_date || s == null || end_date == null)
            return null;

        Date calculated_last_day_within;
        Date workingDate = end_date;

        for(integer x=0;cnt > 0;x++){
            Date tmp = workingDate.addDays(-x);
            //Set the time to 13:30 so if a business day hours will not matter
            DateTime tst = datetime.newInstance(tmp.year(),tmp.month(),tmp.day(),13,30,0);
            if(businessHours.isWithin(business_hours.id,tst)){
                cnt -= 1;
                calculated_last_day_within = tst.date();
            }           
        }

        days_to_date.put(days,calculated_last_day_within);

        return calculated_last_day_within;

}

It all works well in small batches of 200 or so, but when updated without a scope size CPU Limits get reached.

The reason limit get reached:

  1. In the trigger there are two different dates on a record and each use their own business hours. This requires two different instantiations of the class
  2. No real way to cache results as the dates on the records are completely different.

Trigger for context

trigger Populate_Last_x_Days on AM_Sample__c (before insert, before update) {

    //Allows the test methods to insert records without setting values so we can test the batch functionality
    if(Calculate_Last_x_Days_Tests.STOPTRIGGER) return;

    TrainingDaysSettings__c tConfig = TrainingDaysSettings__c.getOrgDefaults();

    BusinessHours_Utilities bu = New BusinessHours_Utilities(Date.today());
    BusinessHours_Utilities buTrainingBefore = New BusinessHours_Utilities(tConfig.Business_Hours_Name__c);
    BusinessHours_Utilities buTrainingAfter = New BusinessHours_Utilities(tConfig.Business_Hours_Name__c);


    for(AM_Sample__c am : trigger.new){
        try{
            am.Last_20_Days__c = bu.isLast_x_Days(am.AM_Report_Date__c,20);

            am.Last_365_Days__c = am.Last_20_Days__c ? am.Last_20_Days__c : bu.isLast_x_Days(am.AM_Report_Date__c, 365);

            //Calculate the days since training using the Training Business Hours and Training Date
            buTrainingBefore.setEndDate(am.WP_Training_Date__c);
            buTrainingAfter.setEndDate(am.AM_Report_Date__c);

            if(tConfig.Days_Within__c != null){
                am.X60_Days_Before_Test__c = buTrainingBefore.isLast_x_Days(am.AM_Report_Date__c,tConfig.Days_Within__c.intValue());
                am.X60_Days_After_Test__c = buTrainingAfter.isLast_x_Days(am.WP_Training_Date__c,(tConfig.Days_Within__c).intValue());
            }else{
                //Default to 60;
                am.X60_Days_Before_Test__c = buTrainingBefore.isLast_x_Days(am.AM_Report_Date__c,60);
                am.X60_Days_After_Test__c = buTrainingAfter.isLast_x_Days(am.WP_Training_Date__c,60);

            }

            am.Days_Since_Training__c = buTrainingBefore.businessDaysSince(am.WP_Training_Date__c,am.AM_Report_Date__c);

        }catch(Exception e){
            system.debug(logginglevel.error,'Unable to set the value: ' + e.getMessage() + ' - ' + e.getStackTraceString());
        }
    }

}

Q What optimization can be made so that I do not have to reduce the scope size on the batch to 200 to prevent CPU Limits?

  • If you can make the assumption every Business Day is 8 hours, you can use BusinessHours.diff and convert milliseconds to hours, and from there to days. You still have to traverse but you can at least start closer to the correct endpoint. I built out a pretty slick API with that assumption but I can't really share all 200 LOC. I can share some helpful snippets though if you are interested in that approach. – Adrian Larson Nov 3 '15 at 16:24
  • Well the API I built is really around 80 LOC but then I built out some utilities around it. It's not that complicated! – Adrian Larson Nov 3 '15 at 16:27
  • @AdrianLarson - Unfortunately I cannot, thats what brought this about. Some are 24 hours, some are 8.... (hence the fudge for the time in the class). Also I needed to make it future proof. Appreciate the response very much. – Eric Nov 3 '15 at 16:27
  • Shucks. Well, you should probably use Datetime.newInstanceGMT to avoid time zone issues, but it does not look like you can make your loop structure much more efficient. – Adrian Larson Nov 3 '15 at 16:38
  • @AdrianLarson - I did not check, I thought about the GMT thing - So question would be if I make a GMT time as 0900 would it be counted as isWithin if the BH are 0900-1700 EST? I do not think it would so the 13:30 thing was a quick fix. I guess this is really a new question / test scenario – Eric Nov 3 '15 at 16:56
3

One of the biggest efficiency gains available to you is to move your Datetime instantiation outside the loop. Including that, you have five methods in your loop that are taking up CPU time unnecessarily: Datetime.newInstance, Date.year, Date.month, Date.day, and Datetime.date. Just instantiate one Datetime and modify it in place as you go, since they will all have the same Time.

static final Time IN_BUSINESS_DAY = Time.newInstance(13, 30, 0, 0);
static final Date end_date; // = something;

public static Date getBusinessDaysBack(Integer target)
{
    Datetime pointer = Datetime.newInstanceGMT(end_date, IN_BUSINESS_DAY);
    while (target > 0)
    {
        pointer = pointer.addDays(-1);
        if (BusinessHours.isWithin(standardHoursId, pointer)) target--;
    }
    return pointer.date();
}
public static Boolean isWithin(Date startDate, Integer target)
{
    // what does end_date == null mean?
    if (startDate == null || startDate > end_date) return false;
    return startDate > getBusinessDaysBack(target);
}

You can involve caching in the above as needed. The loop structure should be more efficient, though I have not profiled it.

  • Thank you SOOO Much. Conceptualizing this type of OO structure is a weak point for me. I really appreciate the time you have spent creating this answer. I will give it a run through and report back. Oh, and end_date == null is a class property that is set so I am just ensuring it is not null so the code would not needlessly execute – Eric Nov 3 '15 at 17:12
  • Right, end_date == null should make all the calculations bail, it seems. – Adrian Larson Nov 3 '15 at 17:17
  • 1
    Just fyi - this cut the processing time in half and solved the issue completely (batch has no issues now). That you for helping me rethink how I was doing things, I really appreciate it..... – Eric Nov 3 '15 at 19:58
  • Awesome! Glad it worked! – Adrian Larson Nov 3 '15 at 20:01
  • Ugh - Spoke to soon. On batch 380 out of 390 it CPU timed out....Oh well, still better performance that before. Reducing batch to 100 keeps it going. (Also could be due to Winter 16 as CPU Timeout has increased greatly for many for code that was working) – Eric Nov 3 '15 at 20:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.