Contrôles d'exécution des applications logicielles intégrés

La publication présente une implémentation logicielle d'outils intégrés pour collecter et accumuler des informations métriques sur l'exécution des applications écrites en C / C ++ / C #.

L'essence de l'approche décrite est basée sur l'inclusion de «points de contrôle» dans le code de programme de l'application pour extraire des données sur le temps d'exécution des composants structurels: méthodes, fonctions et blocs {}. Les informations métriques extraites sont accumulées dans une base de données interne, dont le contenu à la fin de l'application est converti en un formulaire de rapport texte enregistré dans un fichier. La pertinence d'utiliser les moyens de contrôle intégré du temps d'exécution est due à la nécessité d'identifier les zones problématiques du code, d'analyser les causes de la dégradation temporaire de l'application: complète ou partielle, ou apparaissant sur certains ensembles de données sources.

Les exemples de code source C ++ / C # donnés illustrent les implémentations possibles de l'approche décrite.

Présentation


Le développement d'une application logicielle à chaque itération (par exemple, la sortie de la prochaine version) de son développement évolutif comprend les étapes de base suivantes:

  • dĂ©veloppement et test de fonctionnalitĂ©s;
  • optimisation des ressources consommĂ©es de RAM;
  • stabilisation des mĂ©triques d'exĂ©cution.

Ces étapes nécessitent une quantité importante de développeurs non seulement créatifs (tels que le développement et la mise en œuvre d'algorithmes efficaces, la construction d'une architecture logicielle flexible, etc.), mais également un travail de routine. Cette dernière catégorie comprend les activités visant à stabiliser les mesures de temps pour l'exécution des applications. Dans de nombreux cas, il s'agit d'une procédure assez pénible lorsque les développeurs sont confrontés à une dégradation, qui est une conséquence de l'extension des fonctionnalités du produit logiciel, de la reconstruction de l'architecture logicielle et de l'émergence de nouveaux threads dans l'application. Dans le même temps, les sources de dégradation nécessitent certains efforts pour les détecter, ce qui est obtenu non seulement par la grande industriosité et la responsabilité des développeurs (condition nécessaire), mais aussi par la composition des outils utilisés à ces fins (condition suffisante).

L'une des approches efficaces pour résoudre le problème de l'analyse des métriques de temps d'application est l'utilisation de produits logiciels spécialisés, par exemple GNU gprof . L'analyse des rapports générés par de tels outils vous permet d'identifier les «goulots d'étranglement» (méthodes et fonctions de classe), qui représentent une quantité importante de temps passé à exécuter l'application dans son ensemble. Dans le même temps, la validité du temps consacré à l'exécution des méthodes et procédures est certes nuancée par les développeurs.

Il convient également de noter que les produits logiciels de cette classe, en règle générale, effectuent une analyse métrique du temps d'exécution du code de programme aux niveaux des méthodes des classes et des fonctions, en ignorant les niveaux inférieurs (mais néanmoins significatifs du point de vue de l'analyse du problème): {...}, pour, tandis que do-till, if - else, blocs try-catch , à l'intérieur desquels des dépenses non moins importantes de temps d'exécution se produisent.

Ensuite, le contenu principal de l'une des solutions possibles pour la mise en œuvre d'outils de contrôle d'exécution intégrés visant à extraire et à accumuler des informations détaillées sur les métriques de temps des blocs logiciels contrôlés avec la génération ultérieure de rapports pour les développeurs est examiné.

Méthodes de récupération des données d'exécution


La fonctionnalité de toute application logicielle peut être interprétée comme une machine abstraite avec un ensemble fini d' états uniques {St} et de transitions {Tr} entre eux.

Dans le cadre de cette approche, tout flux d'exécution dans l'application doit être interprété comme une séquence ordonnée de ses états et des transitions entre eux. Dans ce cas, l'estimation des coûts de temps d'exécution est effectuée en additionnant les métriques de temps sur l'ensemble des états passés sans tenir compte des coûts des transitions d'un état à un autre - en tant que valeurs négligeables.

L'extraction et l'accumulation de données sur le temps d'exécution de l'application aux points de contrôle spécifiés est la tâche principale résolue par les outils de contrôle intégrés décrits ci-dessous.

Pour chaque point d'arrêt déclaré dans le code source en plaçant
Macro PROFILE_ENTRY C ++, le nombre de passes lors de l'exécution de l'application est enregistré, ainsi que la métrique de temps - la durée totale pendant laquelle l'application était dans l'état à partir du moment où le point de contrôle est passé au niveau suivant de la hiérarchie du programme (y compris un bloc, une méthode de classe, une fonction, etc.) comme illustré dans le schéma ci-dessous.

Le contrôle des points de contrôle (enregistrement initial et calcul de leurs métriques de temps) est effectué par l'objet 'timeManager' , qui est créé en une seule instance. Chaque événement de passage du point de contrôle est fixé par l'objet 'timeManager', et lors de la première passe il est enregistré par lui comme observable comme 'registerEntry' .

Au moment de chaque passage du point de contrôle, un timerObject est créé, fixant l'heure de sa création. Le temps d'exécution est fixé au point de contrôle lorsque l'application quitte le niveau actuel de la hiérarchie logicielle. À ce moment, le timerObject de l'objet est automatiquement détruit, ce qui s'accompagne du calcul de sa «durée de vie» T. Par conséquent, le timeManager augmente le nombre de fois que le point de contrôle passe et le temps qu'il y passe par T. Pour tous les points de contrôle définis, timeManager accumule des données avec la publication ultérieure d'un rapport à la fin de l'application.



Vous trouverez ci-dessous le code C ++ source qui implémente les outils intégrés pour contrôler le temps d'exécution de l'application.

//     #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 structure de l'application de démonstration est illustrée ci-dessous, illustrant l'utilisation des outils de contrôle d'exécution intégrés à titre d'exemple, ainsi qu'un tableau des résultats obtenus (pour plus de détails, voir l' annexe 1. Code source de l'application de démonstration ).





Section Addendum 2. Le code source des moyens du contrôle intégré du temps d'exécution de l'application C # présente une implémentation similaire des moyens du contrôle intégré en C #.

L'auteur utilise des paires de TimeWatcher.StartWatch () et TimeWatcher.StopWatch () pour profiler le temps d'exécution de méthodes et procédures laborieuses (d'un point de vue informatique) dans le cadre du produit logiciel Delta Design développé par EREMEX - un système de conception assistée par ordinateur pour les équipements électroniques.

Vous trouverez ci-dessous un exemple de bref rapport sur les mesures de temps de l'une des fonctionnalités du produit mentionné.


Brèves conclusions


Les outils décrits peuvent être utilisés pour collecter des données sur le temps d'exécution d'une application dans différentes parties de son code de programme, en particulier, ils permettent:

  • collecter et accumuler des donnĂ©es sur les mĂ©triques de temps des threads d'exĂ©cution dans l'application;
  • effectuer des estimations du temps d'exĂ©cution du code de programme exactes aux constructions de langage Ă©lĂ©mentaire;
  • gĂ©rer le volume des donnĂ©es extraites en activant et dĂ©sactivant les outils de contrĂ´le intĂ©grĂ©s sur les sections correspondantes des flux d'exĂ©cution d'application;
  • dĂ©velopper et appliquer des tests de rĂ©gression qui surveillent la stabilitĂ© (et dĂ©tectent la dĂ©gradation) des mĂ©triques de temps d'application.

En conclusion, il convient de noter qu'en dehors du champ d'application de cette publication, il y avait des questions concernant l'application des outils de contrôle intégrés décrits dans le contexte des applications de multithreading et aucune analyse de l'exactitude des données obtenues par métrique de temps n'a été présentée sous aucune forme. Ce dernier est dû au fait qu'en pratique, lors de l'identification des causes de dégradation temporaire d'une application, les données sur la répartition relative des coûts de temps d'exécution entre les composants logiciels de l'application sont principalement pertinentes . À cet égard, les questions de l'exactitude des données obtenues s'effacent au second plan.

Annexe 1. Code source de l'application de démonstration


 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) } 

Supplément 2. Code source des applications C # de contrôle d'exécution intégrées


 /// <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/fr468403/


All Articles