2

We are using below Trigger framework

    public virtual class TriggerHandler {

  // static map of handlername, times run() was invoked
  private static Map<String, LoopCount> loopCountMap;
  private static Set<String> bypassedHandlers;

  static {
    loopCountMap = new Map<String, LoopCount>();
    bypassedHandlers = new Set<String>();
  }

  public TriggerHandler() {

  }

  //method that will be called during execution
  public void run() {

    if(!validateRun()) return;

    addToLoopCount();

    if(Trigger.isBefore && Trigger.isInsert) {
      this.beforeInsert();
    } else if(Trigger.isBefore && Trigger.isUpdate) {
      this.beforeUpdate();
    } else if(Trigger.isBefore && Trigger.isDelete) {
      this.beforeDelete();
    } else if(Trigger.isAfter && Trigger.isInsert) {
      this.afterInsert();
    } else if(Trigger.isAfter && Trigger.isUpdate) {
      this.afterUpdate();
    } else if(Trigger.isAfter && Trigger.isDelete) {
      this.afterDelete();
    } else if(Trigger.isAfter && Trigger.isUndelete) {
      this.afterUndelete();
    }

  }

  public void setMaxLoopCount(Integer max) {
    String handlerName = getHandlerName();
    if(!TriggerHandler.loopCountMap.containsKey(handlerName)) {
      TriggerHandler.loopCountMap.put(handlerName, new LoopCount(max));
    } else {
      TriggerHandler.loopCountMap.get(handlerName).setMax(max);
    }
  }

  public void clearMaxLoopCount() {
    this.setMaxLoopCount(-1);
  }

  public static void bypass(String handlerName) {
    TriggerHandler.bypassedHandlers.add(handlerName);
  }

  public static void clearBypass(String handlerName) {
    TriggerHandler.bypassedHandlers.remove(handlerName);
  }

  public static Boolean isBypassed(String handlerName) {
    return TriggerHandler.bypassedHandlers.contains(handlerName);
  }

  public static void clearAllBypasses() {
    TriggerHandler.bypassedHandlers.clear();
  }

  //allows trigger methods to be deactivated from Custom Setting configuration
  public static Boolean methodShouldRun(String methodName){
    return Trigger_Method_Control__c.getValues(methodName) == null ? true : !Trigger_Method_Control__c.getValues(methodName).Deactivated__c;
  }

  private void addToLoopCount() {
    String handlerName = getHandlerName();
    if(TriggerHandler.loopCountMap.containsKey(handlerName)) {
      Boolean exceeded = TriggerHandler.loopCountMap.get(handlerName).increment();
      if(exceeded) {
        Integer max = TriggerHandler.loopCountMap.get(handlerName).max;
        throw new TriggerHandlerException('Maximum loop count of ' + String.valueOf(max) + ' reached in ' + handlerName);
      }
    }
  }

  // make sure this trigger should continue to run
  private Boolean validateRun() {
    if(!Trigger.isExecuting) {
      throw new TriggerHandlerException('Trigger handler called outside of Trigger execution');
    }
    if(TriggerHandler.bypassedHandlers.contains(getHandlerName())) {
      return false;
    }
    return true;
  }

  private String getHandlerName() {
    return String.valueOf(this).substring(0,String.valueOf(this).indexOf(':'));
  }


  // context-specific methods for override
  protected virtual void beforeInsert(){}
  protected virtual void beforeUpdate(){}
  protected virtual void beforeDelete(){}
  protected virtual void afterInsert(){}
  protected virtual void afterUpdate(){}
  protected virtual void afterDelete(){}
  protected virtual void afterUndelete(){}


  // inner class for managing the loop count per handler
  private class LoopCount {
    private Integer max;
    private Integer count;

    public LoopCount() {
      this.max = 5;
      this.count = 0;
    }

    public LoopCount(Integer max) {
      this.max = max;
      this.count = 0;
    }

    public Boolean increment() {
      this.count++;
      return this.exceeded();
    }

    public Boolean exceeded() {
      if(this.max < 0) return false;
      if(this.count > this.max) {
        return true;
      }
      return false;
    }

    public Integer getMax() {
      return this.max;
    }

    public Integer getCount() {
      return this.count;
    }

    public void setMax(Integer max) {
      this.max = max;
    }
  }

  // exception class
  public class TriggerHandlerException extends Exception {}

}

and this is our trigger handler class:

public class CaseTriggerHandler extends TriggerHandler {  

    //private members represent the trigger data passed in constructor
    private List<Case> oldCases;
    private List<Case> newCases;
    private Map<Id,Case> oldCaseMap;
    private Map<Id,Case> newCaseMap;
    public static Integer runCount = 0;

    //constructor populates private members and handles typecasting for ease of later use
    public CaseTriggerHandler(){
        //this.setMaxLoopCount(1);

        this.oldCases = (List<Case>)Trigger.old;
        this.newCases = (List<Case>)Trigger.new;
        this.oldCaseMap = (Map<Id,Case>)Trigger.oldMap;
        this.newCaseMap = (Map<Id,Case>)Trigger.newMap;
    }

    public override void beforeInsert() {
        System.debug('case before insert'); 

        CaseUtils.setRecordType(newCases);
        CaseUtils.LeadForm(newCases,null);
    }

    public override void afterInsert() {
        System.debug('case after insert'); 
        CaseUtils.addOwnerAsTeamMember(null, newCaseMap);
        CustomerUtils.checkChildCaseForSLADateFix(newCases);
        CustomerUtils.manageREOmniMarkers(newCaseMap);
        System.debug('after case insert');
    }

    public override void beforeUpdate() {
        System.debug('case before update'); 

        runCount++;
        if(runCount <= 3){        
            CaseUtils.LeadForm(newCases,oldCaseMap);
            CustomerUtils.SLAmanagement(oldCases, newCaseMap);
            TaskUtils.taskManagement(oldCases, newCaseMap);
        }    

    }

    public override void afterUpdate() {
        System.debug('case after update'); 
        if(runCount <= 3){
            CaseUtils.addOwnerAsTeamMember(oldCaseMap, newCaseMap);
            CaseUtils.updateMOCase(oldCaseMap, newCaseMap);
            FarmUtils.ForSalesUpdate(oldCases,newCaseMap);
            OtherCustomerUtils.ForCustomerSalesUpdate(oldCases,newCaseMap);
            FarmChatterUtils.autoFollowRecord(oldCases, newCaseMap);
        }        

    }

    public override void beforeDelete() {}

    public override void afterDelete() {
        CustomerUtils.deleteAssignedCaseMembers(oldCaseMap.keySet());
    }

    public override void afterUndelete() {}

}

We are hitting CPU time limit when trying to update a list of 8 records from which one being parent record and the rest are child records. We also have a bunch of Processes, Flows and Workflows on Case object. The error thrown wasn't much helpful because a simple field update was shown as a culprit though it is not the actual root cause but when i checked the logs, i could see that below methods are being called multiple times which is consuming more time all together.

enter image description here

Is there a better way to avoid this and improvise this pattern to be more efficient?

Thanks in advance!

3

One major performance would be to cache the value used, as well as use a simple subStringBefore instead of using String.valueOf twice:

String handlerName;
String getHandlerName() {
    return handlerName == null?
        (handlerName = String.valueOf(this).substringBefore(':')):
         handlerName;
}

Alternatively, it'd make more sense to have getHandlerName be protected virtual:

protected virtual String getHandlerName() { return null; }

Which would allow you to just use a plain String value. This works because the framework presumes the use of this value in several places, so you'd get much better performance.

Alternatively, you could get much better performance if just used Type values instead:

static Set<Type> bypassedHandlers;

...

byPassedHandlers = new Set<Type>();

...

protected virtual Type getType() { return null; }

...

public static Boolean isBypassed(Type handler) {
    return bypassedHandlers.contains(handler);
}

...

Using Type instead of String avoids a lot of potential problems. The dynamic class detection function is "nice", but it'd be far more convenient to use a Type token instead, at the cost of a little bit of code.

There are a number of ways to optimize this framework, though, so I suggest you experiment and see what works best for you.

2
  • Thank you very much for your response @sfdcfox. Those changes indeed increased the performance and also dodging hitting the cpu limit for now. we are calling bypass(String) method in many different classes which would require an update with Type token. – SunnyG Nov 21 '19 at 0:49
  • @SunnyG You're welcome. The latter suggestion was more of a forward-looking thing, if you get the time to do it, it'd be worth it, but I'm glad that just adding a bit of cache improved performance well enough for now. – sfdcfox Nov 21 '19 at 3:47

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.