Event subscription and performance

When we design and write our code we need to think about performance.

We have been used to thinking about database performance, using FindFirst(), FindSet(), IsEmpty() where appropriate.

We also need to think about performance when we create our subscriber Codeunits.

Let’s consider this Codeunit.

codeunit 50100 MySubscriberCodeunit
{
    trigger OnRun()
    begin
        
    end;
    

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', true, true)] 
    local procedure MyProcedure(var SalesHeader: Record "Sales Header")
    begin
        Message('I am pleased that you called.');
    end;


}

Every time any user posts a sales document this subscriber will be executed.

Executing this subscriber will need to load an instance of this Codeunit into the server memory. After execution the Codeunit instance is trashed.

The resources needed to initiate an instance of this Codeunit and trash it again, and doing that for every sales document being posted are a waste of resources.

If we change the Codeunit and make it a “Single Instance”.

codeunit 50100 MySubscriberCodeunit
{
    SingleInstance = true;
    
    trigger OnRun()
    begin
        
    end;
    

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', true, true)] 
    local procedure MyProcedure(var SalesHeader: Record "Sales Header")
    begin
        Message('I am pleased that you called.');
    end;


}

What happens now is that Codeunit only has one instance for each session. When the first sales document is posted then the an instance of the Codeunit is created and kept in memory on the server as long as the session is alive.

This will save the resources needed to initialize an instance and tear it down again.

Making sure that our subscriber Codeunits are set to single instance is even more important for subscribers to system events that are frequently executed.

Note that a single instance Codeunit used for subscription should not have any global variables, since the global variables are also kept in memory though out the session lifetime.

Make sure that whatever is executed inside a single instance subscriber Codeunit is executed in a local procedure. The variables inside a local procedure are cleared between every execution, also in a single instance Codeunit.

codeunit 50100 MySubscriberCodeunit
{
    SingleInstance = true;

    trigger OnRun()
    begin
        
    end;
    

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', true, true)] 
    local procedure MyProcedure(var SalesHeader: Record "Sales Header")
    begin
        ExecuteBusinessLogic(SalesHeader);

    end;

    local procedure ExecuteBusinessLogic(SalesHeader: Record "Sales Header")
    var
        Customer: Record Customer;
    begin
        Message('I am pleased that you called.');    
    end;

}

If your custom code executes every time that the subscriber is executed then I am fine with having that code in a local procedure inside the single instance Codeunit.

Still, I would suggest putting the code in another Codeunit, and keeping the subscriber Codeunit as small as possible.

This is even more important if the custom code only executes on a given condition.

An example of a Codeunit that you call from the subscriber Codeunit could be like this.

codeunit 50001 MyCodeCalledFromSubscriber
{
    TableNo = "Sales Header";
    
    trigger OnRun()
    begin
        ExecuteBusinessLogic(Rec);
    end;
    local procedure ExecuteBusinessLogic(SalesHeader: Record "Sales Header")
    var
        Customer: Record Customer;
    begin
        Message('I am pleased that you called.');    
    end;
}

And I change my subscriber Codeunit to only execute this code on a given condition.

codeunit 50100 MySubscriberCodeunit
{
    SingleInstance = true;

    trigger OnRun()
    begin
        
    end;
    

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', true, true)] 
    local procedure MyProcedure(var SalesHeader: Record "Sales Header")
    begin
        ExecuteBusinessLogic(SalesHeader);

    end;

    local procedure ExecuteBusinessLogic(SalesHeader: Record "Sales Header")
    var
        MyCodeCalledFromSubscriber: Codeunit MyCodeCalledFromSubscriber;
    begin
        if SalesHeader."Document Type" = SalesHeader."Document Type"::Order then
            MyCodeCalledFromSubscriber.Run(SalesHeader);
    end;

}

This pattern makes sure that the execution is a fast as possible and no unneeded variables are populating the server memory.

14 Replies to “Event subscription and performance”

  1. Interesting scenario. Have you done any example code that visualizes the speed increase?

    Another thing of performance. I have had the feeling that if you have a subscriber to OnAfterInsert on a table that will deactivate the buffered insert. I haven’t investigated that in detail but the “feeling” is that it does.

    1. Blessaður og takk fyrir síðast 😉

      Would you mind explaining what you (& kriki) mean by “deactivating/breaking the buffered insert”?

      1. Sæl Eyrún

        Say we have a sales order with 100 item lines. When the sales order is posted 100 item ledger entries are created. These entries are buffered in the service tier and then sent all in one package to the SQL server for insert. One insert and one log at the SQL side. Adding business logic, or any code to the insert operation changes this functionality and the service tier will not buffer these 100 entries but send each one of them to the SQL side. Resulting in 100 database inserts.

  2. MyCodeCalledFromSubscriber will be loaded in memory each time the calling function ExecuteBusinessLogic is executed. so not really a good way. The if-then should be in MyProcedure for performance gain. But it moves business logic to technical function which is bad design… Seems to be a case where you need to choose between beautiful design and performance.

  3. I’d be trying to put this into a perspective: using mentioned posting sales document example: posting a simplest sales document requires at least 7 records to be written to the database, and a few reads too. Single database write operation would be probably a hundred times slower than creating and trashing an instance of subscriber codeunit.

    Therefore even if you speed up calling event subscriber a hundred of times by making the subscriber codeunit a single instance the overall performance gain would be quite likely negligible.

    This sort of optimisation is worth looking at if the subscriber is expected to be called thousands of times, without any database operation in between calls. An unlikely scenario in real life imho.

  4. @Gunnar: That is probably correct. And if most calls or NOT for orders, you gain performance. But if most calls are for Orders (and in most cases, they are), you still have a problem.

    I concur with Slawek’s reply. DB access is expressed in milliseconds and memory access in microseconds. You need 100’s or even 1000’s of those instance-trashing per database read to feel the worsening of the performance.

  5. Hi Gunnar,

    Do you think it is bad scenario to use SingleInstance on a codeunit to catch events that happens on a loop inside a codeunit where we need to have a kind of global variable in the source codeunit?

    For example, on codeunit 7312 between OnCreateWhseDocumentOnBeforeCreateDocAndLine and OnCreateWhseDocumentOnAfterSaveOldValues in order to manage an hypothetical OldDummyCode?

    I can’t find a better way to do this but I don’t want to have SingleIntance codeunit keept on session’s memory.

    Thanks!

    1. Hi Sergi,

      I have no problems with using a small single instance Codeunit in my solutions.
      But, first I verify that I am not able to use a non-single instance Codeunit with manual event subscription.

  6. We are getting a continue error of page expired, this seems happening by our created codeunit which we use to subscribe events for multiple usage, i.e. posting of production journal, item reclass, sales order posting. We have kept this as single instance, might this be the issue.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.