Анализ времени выполнения скриптов и поиск "узкого" места в функционале

 
Заказчик выдвинул требования значительно ускорить быстродействие выполнения определенного функционала.

Для определения "узких" мест в функционале разработал набор методов (функций), которые позволяют измерять суммарное время на выполнение каждой функции и процентное отношение суммарного времени вызова этой функции к суммарному времени выполнения общего функционала. В моей ситуации каждая функция вызывалась по много раз, поэтому нужно было реализовать накопление времени выполнения по каждой функции или участку кода.

Функционал в моем случае: импорт прайс-листа из Excel по производителю с большим количеством строк.

В результате оптимизации, удалось ускорить импорт прайс-листа приблизительно раза в четыре (был 1 час, стало 15 минут).

Вот набор этих методов

(прикрепил ниже в архиве scr_ScriptExecutionAnalysis.zip).

//-----------------------------------------------------------------------------
// scr_ScriptExecutionAnalysis
//-----------------------------------------------------------------------------

var ScriptExecutionAnalysis = {};

function _StartTiming(Caller, IsMain) {
     if (!Caller) {
          return;
     }
     var Name;
     if (typeof(Caller) == 'function') {
          Name = "[fn]" + Caller.toString().split('(')[0].substring(9);
     } else {
          Name = Caller;
     }
     if (!ScriptExecutionAnalysis[Name]) {
          ScriptExecutionAnalysis[Name] = {};
          ScriptExecutionAnalysis[Name].TotalDuration = 0;
          ScriptExecutionAnalysis[Name].NumberOfCalls = 0;
          ScriptExecutionAnalysis[Name].MinDuration = 0;
          ScriptExecutionAnalysis[Name].MaxDuration = 0;
          ScriptExecutionAnalysis[Name].WasZeroDuration = false;
     }
     if (IsMain) {
          ScriptExecutionAnalysis.IsMainName = Name;
     }
     ScriptExecutionAnalysis[Name].Start = (new Date()).getTime();
}

function _EndTiming(Caller) {
     if (!Caller) {
          return;
     }
     var Name;
     if (typeof(Caller) == 'function') {
          Name = "[fn]" + Caller.toString().split('(')[0].substring(9);
     } else {
          Name = Caller;
     }
     if (!ScriptExecutionAnalysis[Name]) {
          return;
     }
     ScriptExecutionAnalysis[Name].End = (new Date()).getTime();
   
     var Duration = ScriptExecutionAnalysis[Name].End - ScriptExecutionAnalysis[Name].Start;
     if (Duration == 0) { ScriptExecutionAnalysis[Name].WasZeroDuration = true }    
         
     if ((ScriptExecutionAnalysis[Name].MinDuration == 0 &&
          !ScriptExecutionAnalysis[Name].WasZeroDuration) ||
          Duration ScriptExecutionAnalysis[Name].MinDuration) {
          ScriptExecutionAnalysis[Name].MinDuration = Duration;
     }
     if (Duration > ScriptExecutionAnalysis[Name].MaxDuration) {
          ScriptExecutionAnalysis[Name].MaxDuration = Duration;
     }
     ScriptExecutionAnalysis[Name].TotalDuration += Duration;
     ScriptExecutionAnalysis[Name].NumberOfCalls++;    
}

function _CreateScriptExecutionAnalysisReport() {
     var Item;
     var Name;
     var Main;
     if (ScriptExecutionAnalysis.IsMainName) {
          Main = ScriptExecutionAnalysis[ScriptExecutionAnalysis.IsMainName];
     }
     var FString = "[%1] TotalDuration - %2 ms, NumberOfCalls - %3, MinDuration - %4 ms, MaxDuration - %5 ms, AverageDuration - %6 ms";
     for (Name in ScriptExecutionAnalysis)   {
          if (Name == 'IsMainName') {
               continue;
          }
          Item = ScriptExecutionAnalysis[Name];
          Log.Write(1, FormatStr(FString,
               Name, Item.TotalDuration, Item.NumberOfCalls, Item.MinDuration, Item.MaxDuration,
               Math.round(Item.TotalDuration / Item.NumberOfCalls * 100) / 100) +
               (Main ? ", Portion - " + Math.round(Item.TotalDuration / Main.TotalDuration * 1000) / 10 + " %": ""));
     }
     ScriptExecutionAnalysis = {};
}

function FormatStr(Str) {
     var Ln = arguments.length;
     for (var i = 1; i Ln; i++ ) {
          Str = Str.replace(new RegExp('%' + i, 'g'), arguments[i]);
     }
     return Str;
}

Описание методов:

_StartTiming(Caller, IsMain) - используется для указания начала отсчета времени.
Аргумент Caller может быть либо объект функции, в которой вызывается этот метод, а может быть и просто текстовая строка (тэг) для унификации этого замера времени. Унификация используется при построении отчета.
Аргумент IsMain - признак того, что метод вызывается из основной функции и нужен для подсчета суммарного времени работы функционала, а суммарное время расчеты функционала в свою очередь используется для определения процентного отношения вызова функции к общему времени выполнении функционала (в отчете).
_EndTiming(Caller) - используется для указания окончания замера времени.
Аргумент Caller указывается по аналогии как в методе _StartTiming.
Каждому вызову метода _StartTiming(Caller) должен соответствовать вызов метода _EndTiming(Caller) с таким же аргументом Caller. Это нужно для построения отчета и вообще для корректного замера времени.
_CreateScriptExecutionAnalysisReport() - метод выводит в лог Террасофт отчет по выполнению функций.

Пример использования в коде:

function MainFunction() {
     _StartTiming(arguments.callee, true);
     ...
     InnerFunction();
     ...
     _EndTiming(arguments.callee);
     _CreateScriptExecutionAnalysisReport();
}

function InnerFunction() {
     _StartTiming(arguments.callee);
     ...
     _StartTiming('ScriptBlock1');
     ...
     _StartTiming('ScriptBlock2');
     ...
     _EndTiming('ScriptBlock2');
     ...
     _StartTiming('ScriptBlock3');
     ...
     _EndTiming('ScriptBlock3');
     ...
     _EndTiming('ScriptBlock1');
     ...
     _EndTiming(arguments.callee);
}

Пример отчета из "реальной жизни"

прикрепил ниже (Report.txt).
Пару строк из отчета:

[[fn]PriceListImport] TotalDuration - 9907 ms, NumberOfCalls - 1, MinDuration - 9907 ms, MaxDuration - 9907 ms, AverageDuration - 9907 ms, Portion - 100 %
[[fn]InitUpdateQuery] TotalDuration - 652 ms, NumberOfCalls - 46, MinDuration - 0 ms, MaxDuration - 31 ms, AverageDuration - 14.17 ms, Portion - 6.6 %
[ProcessOneRecord] TotalDuration - 4219 ms, NumberOfCalls - 49, MinDuration - 31 ms, MaxDuration - 329 ms, AverageDuration - 86.1 ms, Portion - 42.6 %

Расшифровка отчета:

TotalDuration - суммарное время на вызов этой функции/участка кода;
NumberOfCalls - количество вызовов этой функции / прохождения участка кода;
MinDuration - минимальное время выполнения этой функции/участка кода;
MaxDuration - максимальное время выполнения этой функции/участка кода;
AverageDuration - среднее время выполнения этой функции/участка кода;
Portion - процентное отношение суммарного вызова этой функции/участка кода к суммарному времени вызова основной функции.

В отчете [fn] обозначает, что это функция. Отсутствие [fn] обозначает, что это участок кода, который характеризуется указанным тэгом.

PriceListImport - это в моем примере основная функция (видно даже по значению Portion = 100%)
[fn]InitUpdateQuery - это вызов функции InitUpdateQuery, Portion = 6.6 %. Возможно, стоит на эту функцию обратить внимание с таким процентным соотношением.
ProcessOneRecord - участок кода с тэгом 'ProcessOneRecord'. Portion = 42.6 % - большой вес в общем функционале, стоит хорошенько пересмотреть этот участок кода.

Также, стоит заметить, что определенное время (задержка) тратиться на выполнение самих методов замера времени выполнения функций.

Нравится

Поделиться

1 комментарий

Спасибо)) :twisted:

Показать все комментарии