Controles de tiempo de ejecución de aplicaciones de software incorporados

La publicación presenta una implementación de software de herramientas integradas para recopilar y acumular información métrica sobre el tiempo de ejecución de las aplicaciones escritas en C / C ++ / C #.

La esencia del enfoque descrito se basa en la inclusión de "puntos de control" en el código del programa de la aplicación para extraer datos sobre el tiempo de ejecución de los componentes estructurales: métodos, funciones y bloques {}. La información métrica extraída se acumula en una base de datos interna, cuyo contenido al final de la aplicación se convierte en un formulario de informe de texto almacenado en un archivo. La conveniencia de utilizar los medios de control integrados del tiempo de ejecución se debe a la necesidad de identificar áreas problemáticas del código, analizar las causas de la degradación temporal de la aplicación: total o parcial, o manifestarse en ciertos conjuntos de datos de origen.

Los ejemplos de código fuente de C ++ / C # dados demuestran posibles implementaciones del enfoque descrito.

Introduccion


El desarrollo de una aplicación de software en cada iteración (por ejemplo, el lanzamiento de la próxima versión) de su desarrollo evolutivo incluye los siguientes pasos básicos:

  • desarrollo y prueba de funcionalidad;
  • optimización de recursos consumidos de RAM;
  • estabilización de métricas de tiempo de ejecución.

Estos pasos requieren una cantidad significativa de desarrolladores no solo creativos (como el desarrollo e implementación de algoritmos efectivos, la construcción de una arquitectura de software flexible, etc.), sino también un trabajo de rutina. La última categoría incluye actividades destinadas a estabilizar las métricas de tiempo para la ejecución de la aplicación. En muchos casos, este es un procedimiento bastante doloroso, cuando los desarrolladores se enfrentan a la degradación, que es una consecuencia de la expansión de la funcionalidad de un producto de software, la reconstrucción de la arquitectura del software y la aparición de nuevos hilos en la aplicación. Al mismo tiempo, las fuentes de degradación requieren ciertos esfuerzos para detectarlas, lo que se logra no solo por la alta laboriosidad y responsabilidad de los desarrolladores (condición necesaria), sino también por la composición de las herramientas utilizadas para estos fines (condición suficiente).

Uno de los enfoques efectivos para resolver el problema de analizar las métricas de tiempo de aplicación es el uso de productos de software especializados, por ejemplo GNU gprof . El análisis de los informes generados por tales herramientas le permite identificar "cuellos de botella" (métodos y funciones de clase), que representan una cantidad significativa de tiempo dedicado a ejecutar la aplicación como un todo. Al mismo tiempo, los desarrolladores califican la validez del tiempo dedicado a la ejecución de métodos y procedimientos.

También se debe tener en cuenta que los productos de software de esta clase, como regla, realizan análisis métricos del tiempo de ejecución del código del programa en los niveles de métodos de clases y funciones, ignorando los niveles más bajos (pero no obstante significativos desde el punto de vista del análisis del problema): {...}, para, mientras que, do-until, if - else, try-catch blocks, dentro del cual ocurren gastos no menos significativos de tiempo de ejecución.

A continuación, se considera el contenido principal de una de las posibles soluciones para la implementación del control incorporado de las herramientas de tiempo de ejecución destinadas a extraer y acumular información detallada sobre las métricas de tiempo de los bloques de software controlados con la posterior generación de informes para desarrolladores.

Métodos para recuperar datos de tiempo de ejecución


La funcionalidad de cualquier aplicación de software puede interpretarse como una máquina abstracta con un conjunto finito de estados únicos {St} y transiciones {Tr} entre ellos.

En el marco de este enfoque, cualquier flujo de ejecución en la aplicación debe interpretarse como una secuencia ordenada de sus estados y transiciones entre ellos. En este caso, la estimación de los costos del tiempo de ejecución se realiza sumando las métricas de tiempo sobre todo el conjunto de estados pasados, ignorando los costos de las transiciones de un estado a otro, como valores insignificantes.

La extracción y acumulación de datos sobre el tiempo de ejecución de la aplicación en los puntos de control especificados es la tarea principal resuelta por las herramientas de control incorporadas que se describen a continuación.

Para cada punto de interrupción declarado en el código fuente colocando
PROFILE_ENTRY Macro C ++, se registra el número de pases durante la ejecución de la aplicación, así como la métrica de tiempo: el tiempo total que la aplicación estuvo en el estado desde el momento en que el punto de control pasó al siguiente nivel de la jerarquía del programa (incluido un bloque, método de clase, función, etc.) como se ilustra en el diagrama a continuación.

El control de los puntos de control (registro inicial y cálculo de sus métricas de tiempo) se realiza mediante el objeto 'timeManager' , que se crea en una sola instancia. Cada evento de pasar el punto de control es fijado por el objeto 'timeManager', y durante la primera pasada se registra como observable como 'registerEntry' .

En el momento de cada paso del punto de control, se crea un timerObject , que fija el tiempo de su creación. El tiempo de ejecución se fija en el punto de control cuando la aplicación sale del nivel actual de la jerarquía de software. En este momento, el objeto de temporizador del objeto se destruye automáticamente, lo que se acompaña del cálculo de su "vida útil". Para todos los puntos de control establecidos, timeManager acumula datos con la publicación posterior de un informe cuando finaliza la aplicación.



A continuación se muestra el código fuente de C ++ que implementa las herramientas integradas para controlar el tiempo de ejecución de la aplicación.

//     #include <vector> #include <map> #include <algorithm> #include <stdio.h> #include <time.h> typedef unsigned long LONGEST_INT; // ,    //     //     //  ( )   // ()   //    #define PROFILER_ENABLED // CREATE_PROFILER  timeManager , //     // 'main()' #ifdef PROFILER_ENABLED #define CREATE_PROFILER timeManager tManager; #else #define CREATE_PROFILER //   CREATE_PROFILER. #endif //INITIALIZE_PROFILER    timeManager  //       //   'main()' #ifdef PROFILER_ENABLED #define INITIALIZE_PROFILER bool timeManager::object = false;\ std::vector<registerEntry> timeManager::entries; #else #define INITIALIZE_PROFILER //   INITIALIZE_PROFILER. #endif //DELAY(_SECONDS)   '_SECONDS' . //    ,  //     //  #ifdef PROFILER_ENABLED #define DELAY(_SECONDS) {clock_t clk_wait=clock()+((double)_ARG)*CLOCKS_PER_SEC;\ while(clock()<clk_wait) {}} #else #define DELAY(_SECONDS) //    DELAY. #endif //     , //     UNIX  WINDOWS //      #ifdef PROFILER_ENABLED #define MERGE2(x,y) x##y #define MERGE1(_X,_Y) MERGE2(_X,_Y) #if WIN32 #define UNIQUENAME prefix,postfix) MERGE1(prefix,postfix) #else #define UNIQUENAME(prefix,postfix) MERGE2(prefix,postfix) #endif #define GEN_SRC(_ARG1,_ARG2) static int UNIQUENAME(ind,_ARG2)=-1;\ if(UNIQUENAME(ind,_ARG2)<0)\ UNIQUENAME(ind,_ARG2)=timeManager::add_entry(_ARG1);\ timeManager::incr_counter(UNIQUENAME(ind,_ARG2));\ timerObject UNIQUENAME(tm,_ARG2)(UNIQUENAME(ind,_ARG2)); //PROFILE_ENTRY      #ifdef PROFILER_ENABLED #if WIN32 #define PROFILE_ENTRY(_TITLE) GEN_SRC(_TITLE,__COUNTER__) #else #define PROFILE_ENTRY(_TITLE) GEN_SRC(_TITLE,__LINE__) #endif #else #define PROFILE_ENTRY(_TITLE) //    PROFILE_ENTRY. #endif //        //    //    ,   timeManager struct registerEntry { //     (  ) std::string entry_name; //     //     LONGEST_INT covers_counter; //      //     (ticks) LONGEST_INT elapsed_time; // registerEntry(const char * title):entry_name(title), covers_counter(0), elapsed_time(0) {} }; //     class timerObject { //   ,     int index; //    clock_t start_time; public: //       timerObject(int ind):index(ind),start_time(clock()) {} //    “ ”  //       //   ~timerObject(void) { timeManager::incr_timer(index,(LONGEST_INT)(clock()-start_time)); } }; //     class timeManager { private: //     static std::vector<registerEntry> entries; // ,     //    static bool object; public: //     //  ,    //     static int add_entry(const char * title) { entries.push_back(registerEntry(title)); return (((int)entries.size())-1); } //       //      static void incr_counter(int profile_entry_id) { entries[profile_entry_id].covers_counter++; } //  'value'     //      static void incr_timer(int profile_entry_id, LONGEST_INT value) { entries[profile_entry_id].elapsed_time += val; } //       //   static void report(void); //  timeManager(void) { if(!object) object = true; else { printf("\n<<>>:    'timeManager' .\n"); throw; } } //        //   virtual ~timeManager(void) {report();} }; //      bool cmp_entries(registerEntry & first, registerEntry & second) { if(first.entry_name.compare(second.entry_name)>0) return false; return true; } //      //    void timeManager::report(void) { const std::string bar(72,'*'); //        const char * REPORT_FILE = "C:\\tmProfile.txt"; FILE * fp = fopen(REPORT_FILE,"w"); if(!fp) { printf("\n<<>>:       (%s)",REPORT_FILE); return; } fprintf(fp,"\n#%s",bar.c_str()); fprintf(fp,"\n#\n#      "); fprintf(fp,"\n#\n#%s",bar.c_str()); fprintf(fp,"\n#\n# %-35s %-15s %-20s", " ",""," ()"); fprintf(fp,"\n# %-35s %-15s %-20s", "------------------","-------------","---------------\n#"); //         std::sort(entries.begin(),entries.end(),cmp_entries); for(unsigned jj = 0; jj< entries.size(); jj++) { fprintf(fp,"\n# %-35s %-16d", entries[jj].entry_name.c_str(), entries[jj].covers_counter); if(entries[jj].covers_counter == 0) fprintf(fp,"%-20d",0); else fprintf(fp,"%-20.0f", static_cast<double>(entries[jj].elapsed_time)/ static_cast<double>(CLOCKS_PER_SEC)); } if(entries.size() == 0) fprintf(fp,"\n# No covered profile entries found\n"); fprintf(fp,"\n#\n#%s\n",bar.c_str()); fclose(fp); } 

La estructura de la aplicación de demostración se ilustra a continuación, ilustrando el uso de las herramientas de control de tiempo de ejecución incorporadas como ejemplo, así como una tabla de los resultados obtenidos (para más detalles, consulte el Apéndice 1. Código fuente de la aplicación de demostración ).





Anexo de sección 2. El código fuente de los medios del control incorporado del tiempo de ejecución de la aplicación C # presenta una implementación similar de los medios del control incorporado en C #.

El autor utiliza pares de TimeWatcher.StartWatch () y TimeWatcher.StopWatch () al perfilar el tiempo de ejecución de métodos y procedimientos laboriosos (desde un punto de vista computacional) como parte del producto de software Delta Design desarrollado por EREMEX , un sistema de diseño asistido por computadora para equipos electrónicos.

A continuación se muestra un ejemplo de un breve informe sobre las métricas de tiempo de una de las funciones del producto mencionado.


Breves conclusiones


Las herramientas descritas se pueden utilizar para recopilar datos sobre el tiempo de ejecución de la aplicación en varias partes de su código de programa, en particular, permiten:

  • recopilar y acumular datos sobre métricas de tiempo de subprocesos de ejecución en la aplicación;
  • realizar estimaciones del tiempo de ejecución del código del programa con precisión para construcciones de lenguaje elemental;
  • gestionar el volumen de datos extraídos activando y desactivando las herramientas de control integradas en las secciones correspondientes de los flujos de ejecución de la aplicación;
  • desarrollar y aplicar pruebas de regresión que controlen la estabilidad (y detecten la degradación) de las métricas de tiempo de aplicación.

En conclusión, debe tenerse en cuenta que, fuera del alcance de esta publicación, hubo preguntas sobre la aplicación de las herramientas de control incorporadas descritas en el contexto de aplicaciones de subprocesamiento múltiple y no se presentó ningún análisis de la precisión de los datos obtenidos por métricas de tiempo. Esto último se debe al hecho de que, en la práctica, al identificar las causas de la degradación temporal de una aplicación, los datos sobre la distribución relativa de los costos de tiempo de ejecución entre los componentes de software de una aplicación son principalmente relevantes . En este sentido, las preguntas sobre la precisión de los datos obtenidos se están desvaneciendo.

Apéndice 1. Código fuente para la aplicación de demostración


 INITIALIZE_PROFILER int main(int argc, char * argv[]) { //create profile manager CREATE_PROFILER PROFILE_ENTRY("1 Main context") f11(); for(unsigned jj = 0;jj<4;jj++) f12(); f13 (); f14 (); f15 (); f16 (); f17(); return 0; } void f11(void)///////////////////////////////////////// { PROFILE_ENTRY ("2 f11()........................") for (unsigned jj = 0; jj<5; jj++) { PROFILE_ENTRY ("2 f11()::for(...){...} iterat-ing") DELAY(1) } //profile entry for repeating int nn(3); while(nn > 0) { PROFILE_ENTRY("2 f11()::while(...){...} iterat-ing") DELAY(1) nn--; } } void f12(void)///////////////////////////////////////// { PROFILE_ENTRY("3 f12()........................") goto ending; { PROFILE_ENTRY("3 f12()::ignored code part") DELAY(1) } ending: PROFILE_ENTRY("3 f12()::ending code part") DELAY(2) } void f13(void) ///////////////////////////////////////// { PROFILE_ENTRY("4 f13()........................") srand((unsigned) time(NULL)/2); for(unsigned jj = 0; jj < 200; jj++) { if(rand()%2 == 0) { PROFILE_ENTRY("4 f13()::even branch") DELAY(0.01) } else { PROFILE_ENTRY("4 f13()::od branch") DELAY(0.02) } } } void f14(void)///////////////////////////////////////// { static int depth = 10; { PROFILE_ENTRY("5 f14() recursion") depth--; DELAY(0.5) if(depth == 0) return; } f14(); } void f15(void)///////////////////////////////////////// { PROFILE_ENTRY("7 f15()........................") for(unsigned jj = 0; jj < 10; jj++) { demo_class obj; obj.method1(); obj.method2(); obj.method3(); } } void f16(void)///////////////////////////////////////// { PROFILE_ENTRY("8 f16()........................") try { for(int jj = 10; jj >= 0; jj--) { PROFILE_ENTRY("81 f16() try clause") DELAY(1) int rr = 200/jj; } } catch(...) { PROFILE_ENTRY("81 f16() catch clause") DELAY(2) return; } } void f17(void)///////////////////////////////////////// { PROFILE_ENTRY("9 f17()........................") f21(); f22(); f23(); f24(); f25(); } void f22(void)///////////////////////////////////////// { PROFILE_ENTRY("91 f22()........................") DELAY(1) f221(); f222(); f223(); } void f23(void) {PROFILE_ENTRY("91 f23()") DELAY(1) } void f24(void) {PROFILE_ENTRY("91 f24()") DELAY(1) } void f25(void) {PROFILE_ENTRY("91 f25()") DELAY(1) } void f221(void) {PROFILE_ENTRY("91 f221()") DELAY(3) } void f222(void) {PROFILE_ENTRY("91 f222()") DELAY(4) } void f223(void) {PROFILE_ENTRY("91 f223()") DELAY(5) } 

Suplemento 2. Código fuente de aplicaciones C # de control de tiempo de ejecución incorporadas


 /// <summary> ///             /// </summary> public class TimeWatcher { /// <summary> ///            /// </summary> internal class TimeEntry { //     public Stopwatch timeWatch; //       public long elapsedTime; //  public TimeEntry() { timeWatch = new Stopwatch(); elapsedTime = 0; } } //       //    private static bool enableTimeWatcher = false; //            private static Dictionary<string, TimeEntry> entryDictionary = new Dictionary<string, TimeEntry>(); //         public static void StartWatch(string postfix = "") { if (!enableTimeWatcher) return; string entryName = GetCurrentMethod(); if (postfix != "") { entryName += postfix; } //    ,      //         if (!entryDictionary.ContainsKey(entryName)) { entryDictionary.Add(entryName, new TimeEntry()); entryDictionary[entryName].timeWatch.Start(); } else { if (entryDictionary[entryName].timeWatch.IsRunning) { throw new System.InvalidOperationException(":    '" + entryName + "'  ."); } else entryDictionary[entryName].timeWatch.Restart(); } } //        public static void StopWatch(string postfix = "") { if (!enableTimeWatcher) return; string entryName = GetCurrentMethod(); if (postfix != "") { entryName += postfix; } //    ,      if (!entryDictionary.ContainsKey(entryName)) { throw new System.InvalidOperationException(":     -    '" + entryName + "'."); } if (!entryDictionary[entryName].timeWatch.IsRunning) { throw new System.InvalidOperationException ":        '" + entryName + "'."); } entryDictionary[entryName].timeWatch.Stop(); entryDictionary[entryName].elapsedTime += entryDictionary[entryName].timeWatch.ElapsedMilliseconds; } //        //     public static void TimeWatchReport() { const string bar = "============================================="; if (!enableTimeWatcher) return; Console.WriteLine(""); Console.WriteLine(bar); Console.WriteLine("     (): "); Console.WriteLine(""); int maxLen = 0; foreach (var timeEntry in entryDictionary) { if(timeEntry.Key.Length > maxLen) maxLen = timeEntry.Key.Length; } maxLen++; string strFormat = "{0," + maxLen + "} ... {1,-10}"; foreach (var timeEntry in entryDictionary) { Console.WriteLine(strFormat, timeEntry.Key, timeEntry.Value.elapsedTime); } Console.WriteLine(bar); Console.WriteLine(""); entryDictionary.Clear(); enableTimeWatcher = false; } //          //      /tmw    //   public static void InitTimeWatch() { if (Environment.GetCommandLineArgs().Any(v => v == "/tmw")) { if (entryDictionary.Count > 0) { TimeWatchReport(); } entryDictionary.Clear(); enableTimeWatcher = true; } } //        private static string GetCurrentMethod() { StackTrace st = new StackTrace(); StackFrame sf = st.GetFrame(2); return sf.GetMethod().Name; } } 

Source: https://habr.com/ru/post/468403/


All Articles