classdef ExecutionTimeScheduleController < handle
    %EXECUTIONTIMESCHEDULECONTROLLER is the main interface to show the
    %scheduling from a simulation with execution-time profiling

%   Copyright 2020-2021 The MathWorks, Inc.
    
    properties(GetAccess = protected, SetAccess = private)
        % store information about which functions are tasks
        Tasks = [];
        TaskNames = [];
        NumTasks = 0;
        
        % store information about sections
        NumSections = 0;
        MatchSectionIdToName = [];
        UniqueSectionNames = [];
        % since the profiling engine adds virtual sections to model
        % preemptions, remove them
        RealNodes = [];
        
        % Reference to raw data container
        TimerData;
        TimeScaleFactor = 1.0;

        StartSimTime = -1;
        StopSimTime = -1;
    end
    
    properties (Constant = true, Hidden = true)
        % num max point to plot without simTime boundaries
         CntMaxNumPointsToProcess = 500000;
    end
    
    properties (GetAccess = protected, SetAccess=public, Hidden = true)
        % num max point to plot without simTime boundaries
         MaxNumPointsToProcess = coder.profile.ExecutionTimeScheduleController.CntMaxNumPointsToProcess;
    end
    
    properties(Access = private)
        % top model the simulation was started from (the SDI run is
        % associated to this model)
        ModelName;
        % number of asyncqueues
        NumAQs = 0;
    end
    
    methods
        function obj = ExecutionTimeScheduleController(modelName, timerData, tps)
            obj.TimerData = timerData;
            if tps == 0
                obj.TimeScaleFactor = 1.0;
            else
                obj.TimeScaleFactor = tps;
            end
            obj.ModelName = modelName;
            obj.initialize(timerData);
        end
        
        function refresh(this)
            % organize plots in SDI, open SDI and flush all data
            Simulink.sdi.loadSDIEvent;
            Simulink.sdi.view
            % 8 is the maximum number of rows in SDI
            numSubPlots = max(min(this.NumAQs, 8), 1);
            Simulink.sdi.setSubPlotLayout(numSubPlots, 1);
            Simulink.sdi.loadSDIEvent;
        end
        
        function plot(this, showTasksOnly, startSimTime, stopSimTime)
            if showTasksOnly
                % if only tasks are shown, discard function names
                namesToUse = cell(1,0);
                nameIds = zeros(1, this.NumSections);
                for i=1:this.NumSections
                    if this.Tasks(i)
                        name = this.UniqueSectionNames{this.MatchSectionIdToName(i)};
                        idx = find(strcmp(name, namesToUse));
                        if isempty(idx)
                            namesToUse{end+1} = name; %#ok<AGROW>
                            nameIds(i) = length(namesToUse);
                        else
                            nameIds(i) = idx;
                        end
                    end
                end
            else
                namesToUse = this.UniqueSectionNames;
                nameIds = this.MatchSectionIdToName;
            end
            
            % mark any existing SDI run for this model as completed
            % this will make the schedule method create always a new run
            Simulink.HMI.synchronouslyFlushWorkerQueue();
            Simulink.HMI.DatabaseStreaming.removeModelFromActiveSimList(this.ModelName);
            % update internal state
            this.StartSimTime = startSimTime;
            this.StopSimTime = stopSimTime;
            % set auto timespan in SDI
            autoTimeSpan = Simulink.sdi.getAutoTimespan();
            resetAutoTimeSpan = onCleanup(@()Simulink.sdi.setAutoTimespan(autoTimeSpan));
            Simulink.sdi.setAutoTimespan(true);
            % uncheck other signals
            Simulink.sdi.clearAllSubPlots();
            % create AQs
            [hQueues, hClients] = this.createAsyncQueues(namesToUse);
            this.NumAQs = length(hQueues);
            % close AQs no matter error happens
            deleteQueues =  onCleanup(@()this.deleteQueues(hQueues));
            % queue configuration
            Simulink.AsyncQueue.Queue.configureQueuesAndLaunchThreads(hQueues);
            % plot scheduling to SDI
            [startAbsTime, stopAbsTime] = this.plotScheduling(nameIds, startSimTime, stopSimTime);
            % queue configuration
            Simulink.sdi.internal.WebInterface.startStreaming(startAbsTime, stopAbsTime, num2cell(hClients), '');
            % update singal interpolation
            this.updateInterpolationMethods();
        end
    end
    
    methods(Abstract, Access = protected)
        % create asyncqueues
        num = createAsyncQueues(this, namesToUse);
        % plot the scheduling SDI
        [startTime, stopTime] = plotScheduling(this, nameIds, startSimTime, stopSimTime);
        % return the interpolation to use for each queue
        interMethods = getInterpolationMethod(this);
    end
    
    methods(Access = private)
        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
        % SDI methods
        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
        function updateInterpolationMethods(this)
            % update SDI singals to use the requested interpolation
            interMethods = getInterpolationMethod(this);
            runId = Simulink.sdi.getCurrentRunID(this.ModelName);
            if isempty(runId) || runId == uint32(0)
                return;
            end
            run = Simulink.sdi.getRun(runId);
            if isempty(run)
                return;
            end
            if length(run.getAllSignals) ~= length(interMethods)
                return;
            end
            for i = 1:length(interMethods)
                idx = run.getSignalIDByIndex(i);
                Simulink.sdi.Instance.engine.setSignalInterpMethod(idx, interMethods{i});
            end
        end
        
        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
        % Initialize methods
        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
        function initialize(this, timerData)
            this.NumSections = timerData.getNumSectionNodes;
            this.TaskNames = cell(1, this.NumSections);
            this.UniqueSectionNames = cell(1,0);
            this.Tasks = false(1, this.NumSections);
            this.MatchSectionIdToName = zeros(1, this.NumSections);
            numUniqueNames = 0;
            for i=1:this.NumSections
                section = timerData.getVisibleSectionProfile(i);
                this.Tasks(i) = section.getTraceInfo.isTask;
                if this.Tasks(i)
                    this.TaskNames{i} = section.Name;
                    fcnCallName = section.Name;
                    fcnCallName = regexprep(fcnCallName, '\s\[(.+)\]$', '');
                    fcnCallName = strrep(fcnCallName, ' ', '-');
                else
                    this.TaskNames{i} = '';
                    fcnCallName = section.getTraceInfo.getCodeSectionName();
                end
                idx = find(strcmp(fcnCallName, this.UniqueSectionNames));
                if isempty(idx)
                    numUniqueNames = numUniqueNames + 1;
                    this.MatchSectionIdToName(i) = numUniqueNames;
                    this.UniqueSectionNames{end+1} = fcnCallName;
                else
                    this.MatchSectionIdToName(i) = idx;
                end
            end
            this.NumTasks = sum(this.Tasks==true);
            
            this.RealNodes = zeros(1, timerData.getNumNodes - 1);
            for i=1:(timerData.getNumNodes - 1)
                id = timerData.getSectionProfile(i).Number;
                if id > 0
                    this.RealNodes(i) = id;
                end
            end
        end
    end
    
    methods(Access = protected)
        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
        % SDI methods
        %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
        function [client, hQueue] = createAQueue (this, sigName, dt, subPlotIdx)
            % create and configure an asyncquque
            sigSource = Simulink.AsyncQueue.SignalSource;
            sigSource.Path = this.ModelName;
            sigSource.Name = sigName;
            sigSource.Index = subPlotIdx;
            signal = Simulink.AsyncQueue.Signal.create(dt, int32(1), false, false);
            uuid = matlab.lang.internal.uuid;
            signal.addMetaData('__METADATA__', uuid);
            signal.setSource(sigSource);
            
            hQueue = Simulink.AsyncQueue.Queue(signal);
            
            % create database observer
            client = Simulink.AsyncQueue.SignalClient;
            client.ObserverType = 'database_observer';
            hQueue.bindClient(client);
            
            % create web observer
            client = Simulink.AsyncQueue.SignalClient;
            client.ObserverType = 'webclient_observer';
            client.ObserverParams = Simulink.HMI.AsyncQueueObserverAPI. ...
                getDefaultObserverParams('webclient_observer');
            client.ObserverParams.LineSettings.Axes = uint32(subPlotIdx);
            hQueue.bindClient(client);
            
            % set domain
            eng = Simulink.sdi.Instance.engine;
            eng.sigRepository.setSignalDomainType(signal.ID, 'xil_profile')
        end
        
        function deleteQueues(~, queues)
            % terminate asyncqueues
            if ~isempty(queues)
                for i = 1:length(queues)
                    delete(queues(i));
                end
            end
        end
    end
end
