Main Content

Perform Instrumentation-Free Profiling Using Hardware Execution Tracers

In Create Execution-Time Profile for Generated Code, execution-time profiling uses instrumentation code, which:

  • Modifies the generated code.

  • Adds overheads to the execution-time metrics.

For processor-in-the-loop (PIL) simulations, you can automatically filter the overheads–see Remove Instrumentation Overheads from Execution Time Measurements. If you can set up your target hardware to generate a code execution trace, then you can generate execution-time metrics without instrumentation code. You can:

  • Implement and register a parser for your target hardware.

  • Use the parser to produce execution-time metrics that do not contain overheads.

  • View execution-time metrics by using the Code Profile Analyzer.

Workflow to Define and Use Parser

The format of the execution trace from target hardware profiling events is target specific. Your parser must convert the execution trace to a form that MATLAB® can process.

Use the coder.profile.trace API to:

  • Abstract trace information from the source, for example, target hardware, a simulator, or an external debugger.

  • Provide execution-time information to MATLAB.

Use this workflow:

  1. Implement a parser that inherits from coder.profile.trace.Parser and overrides these protected methods:

    • processCustomFields –– Processes additional information that is required to parse the trace. Registered information is passed to the method by using name-value arguments.

    • parseTrace –– Parses the file that contains the execution trace. While parsing the trace, the code notifies the logger, a coder.profile.trace.Logger object, of profiling events by calling the methods startTask, endTask, startFunction, and endFunction.

    • getTicksPerSecond –– Returns the CPU clock frequency of the target hardware.

    For more information about the methods, see Parser Definition and Usage Example.

  2. Register the parser:

    1. Create a target.TracerService object and specify these properties:

      • Name – Name that you use to call tracer.

      • MATLABClass – Name of parser class, including namespace.

      • Attributes – List of additional information that the parser requires. Each attribute is a target.TracerAttributes object that has a key.

    2. Register your parser by adding the object to an internal database.

      For example:

      tc = target.create('TracerService', ...
                         'Name','myParserName');
      tc.MATLABClass = "fullPathToMyParser";
      tc.Attributes(1) = target.create('TracerAttributes');
      tc.Attributes(1).Key = 'AddressFile';
      target.add(tc);

  3. Use the parser to generate execution-time metrics:

    1. Set up your target hardware to generate a code execution trace. This step is specific to the target hardware and depends on the target hardware configuration in MATLAB and tracer functionality.

    2. Run a PIL simulation, which produces the code execution trace.

    3. Invoke the coder.profile.trace.parse function, which applies the parser to the code execution trace, creating a coder.profile.ExecutionTime object. When you invoke the function, you must specify your parser, the codedescriptor.dmr filepath, the trace file, and other files that your parser requires. For example:

      executionProfile = coder.profile.trace.parse('ExampleParser', ...
                                                   fullfile('SILTopModel_ert_rtw', 'codedescriptor.dmr'), ...
                                                   'trace.csv', ...
                                                   'AddressFile', 'address.csv')

    During a PIL simulation, only one task is active at a time because tasks are executed in isolation. If the profiling event order is not followed during processing of the coder.profile.ExecutionTime object, the software generates warnings.

  4. View the execution-time metrics in the Code Profile Analyzer:

    coder.profile.show(executionProfile);

Parser Definition and Usage Example

This example aims to provide more details about implementing and using a custom parser.

Assume that your tracer generates two CSV files at the end of the PIL simulation:

  • trace.csv — Provides the main execution trace, using the line format event,functionAddr,time:

    • event is the function start or function return.

    • functionAddr is the function address in the code.

    • time is the time at which the event occurs.

  • address.csv — Provides function addresses of named functions in the code, using the line format functionAddr,functionName:

    • functionAddr is the function address in the code.

    • functionName is the name of the function.

For simplicity, this example considers only a few function calls and one simulation step as shown in this table.

Generated FileFile Content
trace.csvstart,0x0040117a,1000
start,0x004001ec,1010
start,0x004001ec,1020
start,0x00402492,1030
start,0x0040247c,1040
end,0x0040247c,1050
end,0x00402492,1060
end,0x004001ec,1070
end,0x004001ec,1080
end,0x0040117a,1090
address.csv0x00402492,step
0x0040247c,CounterTypeA
0x0040117a,xilRun
0x004001ec,xilOutput
0x004001ec,xilProcessMsg

This table shows how you can implement your parser.

coder.profile.trace.Parser ObjectComments
classdef MyParser < coder.profile.trace.Parser

Example implementation of coder.profile.trace.Parser.

    properties (Access=private)

        FcnAddressMap;
    end   

Private properties.

    methods (Access = protected)

Protected method implementations.

        function res = getTicksPerSecond(~)
            res = 200000000;
        end

Required.

res = getTicksPerSecond(this) returns the CPU clock frequency.

        function processCustomFields(this, logger, options)
            arguments
                this
                ~
                options.AddressFile (1,:) char
            end
 
            fid = fopen(options.AddressFile );
            c = onCleanup(@()fclose(fid));
 
            this.FcnAddressMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
            while true
                tline = fgetl(fid);
                if ~ischar(tline)
                    break;
                end
                % split line
                parts = strsplit(tline, ',');
                % get address and function name
                hexAddr = parts{1};
                fcnName = parts{2};
                this.FcnAddressMap(hexAddr) = fcnName;
            end
        end

Required.

processCustomFields(this, logger, varagin) processes the additional required files, passed using name-value arguments.

The method:

  • Opens the additional files and stores the information internally.

  • Reads all the function addresses.

        function parseTrace(this, traceFileName, logger)
            % parse the main execution trace to extract code execution
 
            isTaskRunning = false;
            % start parsing
            fid = fopen(traceFileName);
            c = onCleanup(@()fclose(fid));
            while true
                tline = fgetl(fid);
                if ~ischar(tline)
                    break;
                end
                % check if it is a valid trace line
                parts = strsplit(tline, ',');
                event = parts{1};
                hexAddr = parts{2};
                timer = str2double(parts{3});
                % process sample
                if ~this.FcnAddressMap.isKey(hexAddr)
                    continue;
                end
                fcnName = this.FcnAddressMap(hexAddr);
                if ~isTaskRunning
                    % the CPU is idle, no production code running
                    if logger.isTask(fcnName)
                        assert(strcmp(event, 'start'));
                        isTaskRunning = true;
                        logger.startTask(fcnName, timer);
                    end
                else
                    % the CPU is not idle
                    if strcmp(event, 'start')
                        logger.startFunction(fcnName, timer);
                    else
                        if logger.isTask(fcnName)
                            logger.endTask(fcnName, timer);
                            isTaskRunning = false;
                        else
                            logger.endFunction(fcnName, timer);
                        end
                    end
                end
            end
        end

Required.

parseTrace(this, traceFileName, logger) is the main method that processes the trace file traceFileName and forwards events to the logger.

The logger is a coder.profile.trace.Logger object that provides this interface:

  • res = getTaskList(this) returns a cell array with the list of task names (their function names in the code). The information comes from the code descriptor file.

  • res = isTask(this, taskName) returns true if a passed function is a model task.

  • startTask(this, taskName, timeValue) provides notification of the time timeValue at which the task taskName started.

  • endTask (this, taskName, timeValue) provides notification of the time timeValue at which the task taskName terminated.

  • startFunction (this, functionName, timeValue) provides notification of the time timeValue at which the function functionName started.

  • endFunction (this, functionName, timeValue) provides notification of the time timeValue at which the function functionName terminated.

In this example, the method discards all functions called before and after a task execution as they are not part of the generated code.

    end

end statement for methods.

end

end statement for classdef.

After implementing your parser, register the parser in MATLAB.

tc = target.create('TracerService', 'Name', 'ExampleParser');
tc.MATLABClass = "MyParser";
tc.Attributes(1) = target.create('TracerAttributes');
tc.Attributes(1).Key = 'AddressFile';
target.add(tc);

To create the coder.profile.ExecutionTime object, run the parser.

executionProfile = coder.profile.trace.parse('ExampleParser', ...
                                             fullfile('SILTopModel_ert_rtw', 'codedescriptor.dmr'), ...
                                             'trace.csv', ...
                                             'AddressFile', 'address.csv')

To view execution-time metrics in the Code Profile Analyzer, run:

coder.profile.show(executionProfile);

To open the profiling report, run:

report(executionProfile)

For this example, consider section 3 of the profiling report.

Section 3 of profiling report shows execution-time metrics for step and CounterType A functions.

The report section shows that the step method is executed only once and takes 150 ns. For a clock frequency of 200 MHz, the time taken corresponds to 30 cycles, which is the difference between the time values in the 0x00402492 (step) lines in trace.csv, that is start,0x00402492,1030 and end,0x00402492,1060. CounterTypeA runs for 50 ns or 10 cycles, which is the time difference between start,0x0040247c,1040 and end,0x0040247c,1050.

Related Topics