Integrierte Laufzeitsteuerung für Softwareanwendungen

Die Veröffentlichung enthält eine Softwareimplementierung integrierter Tools zum Sammeln und Sammeln von Metrikinformationen zur Laufzeit von Anwendungen, die in C / C ++ / C # geschrieben wurden.

Der Kern des beschriebenen Ansatzes basiert auf der Aufnahme von „Kontrollpunkten“ in den Programmcode der Anwendung zum Extrahieren von Daten zur Ausführungszeit von Strukturkomponenten: Methoden, Funktionen und {} Blöcke. Die extrahierten Metrikinformationen werden in einer internen Datenbank gesammelt, deren Inhalt am Ende der Anwendung in ein in einer Datei gespeichertes Textberichtsformular konvertiert wird. Die Angemessenheit der Verwendung der Mittel zur integrierten Steuerung der Ausführungszeit beruht auf der Notwendigkeit, Problembereiche des Codes zu identifizieren, die Ursachen für die vorübergehende Verschlechterung der Anwendung zu analysieren: vollständig oder teilweise oder auf bestimmten Sätzen von Quelldaten zu manifestieren.

Die angegebenen C ++ / C # -Quellcodebeispiele zeigen mögliche Implementierungen des beschriebenen Ansatzes.

Einführung


Die Entwicklung einer Softwareanwendung bei jeder Iteration (z. B. der Veröffentlichung der nächsten Version) ihrer evolutionären Entwicklung umfasst die folgenden grundlegenden Schritte:

  • Entwicklung und Erprobung von Funktionen;
  • Optimierung der verbrauchten RAM-Ressourcen;
  • Stabilisierung von Laufzeitmetriken.

Diese Schritte erfordern eine erhebliche Anzahl von Entwicklern, die nicht nur kreativ sind (wie die Entwicklung und Implementierung effektiver Algorithmen, die Erstellung einer flexiblen Softwarearchitektur usw.), sondern auch Routinearbeiten. Die letztere Kategorie umfasst Aktivitäten zur Stabilisierung von Zeitmetriken für die Anwendungsausführung. In vielen Fällen ist dies ein ziemlich schmerzhafter Vorgang, wenn Entwickler mit einer Verschlechterung konfrontiert sind. Dies ist eine Folge der Erweiterung der Funktionalität des Softwareprodukts, der Neuerstellung der Softwarearchitektur und des Auftretens neuer Threads in der Anwendung. Gleichzeitig erfordern die Ursachen der Verschlechterung bestimmte Anstrengungen, um sie zu erkennen, was nicht nur durch die hohe Fleißigkeit und Verantwortung der Entwickler (notwendige Bedingung), sondern auch durch die Zusammensetzung der für diese Zwecke verwendeten Werkzeuge (ausreichende Bedingung) erreicht wird.

Einer der effektiven Ansätze zur Lösung des Problems der Analyse von Anwendungszeitmetriken ist die Verwendung spezialisierter Softwareprodukte, beispielsweise GNU gprof . Durch die Analyse von Berichten, die mit solchen Tools erstellt wurden, können Sie „Engpässe“ (Klassenmethoden und -funktionen) identifizieren, die einen erheblichen Zeitaufwand für die Ausführung der gesamten Anwendung ausmachen. Gleichzeitig wird die Gültigkeit der Zeit, die für die Ausführung von Methoden und Verfahren aufgewendet wird, sicherlich von den Entwicklern qualifiziert.

Es ist auch zu beachten, dass die Softwareprodukte dieser Klasse in der Regel eine metrische Analyse der Ausführungszeit des Programmcodes auf den Ebenen der Methoden von Klassen und Funktionen durchführen, wobei die niedrigeren (vom Standpunkt der Problemanalyse jedoch signifikanten) Ebenen ignoriert werden: {...}, z. während, bis, wenn - sonst, Try-Catch- Blöcke, innerhalb derer keine weniger bedeutenden Ausgaben für die Ausführungszeit anfallen.

Als nächstes wird der Hauptinhalt einer der möglichen Lösungen für die Implementierung integrierter Laufzeitsteuerungswerkzeuge betrachtet, die darauf abzielen, detaillierte Informationen über Zeitmetriken von gesteuerten Softwareblöcken mit anschließender Erstellung von Berichten für Entwickler zu extrahieren und zu sammeln.

Methoden zum Abrufen von Laufzeitdaten


Die Funktionalität jeder Softwareanwendung kann als abstrakte Maschine mit einer endlichen Menge eindeutiger Zustände {St} und Übergängen {Tr} zwischen ihnen interpretiert werden.

Im Rahmen dieses Ansatzes sollte jeder Ausführungsfluss in der Anwendung als eine geordnete Folge ihrer Zustände und Übergänge zwischen ihnen interpretiert werden. In diesem Fall wird die Schätzung der Ausführungszeitkosten durchgeführt, indem die Zeitmetriken über den gesamten Satz übergebener Zustände summiert werden, wobei die Kosten für Übergänge von einem Zustand in einen anderen ignoriert werden - als vernachlässigbare Werte.

Das Extrahieren und Sammeln von Daten zur Anwendungsausführungszeit an den angegebenen Kontrollpunkten ist die Hauptaufgabe, die von den nachstehend beschriebenen integrierten Steuerungswerkzeugen gelöst wird.

Für jeden im Quellcode durch Platzieren deklarierten Haltepunkt
PROFILE_ENTRY C ++ - Makro, die Anzahl seiner Durchläufe während der Anwendungsausführung sowie die Zeitmetrik - die Gesamtzeit, in der sich die Anwendung im Zustand befand, ab dem Zeitpunkt, an dem der Prüfpunkt an die nächste Ebene der Programmhierarchie übergeben wurde (einschließlich Block, Klassenmethode, Funktion usw.). wie in der folgenden Abbildung dargestellt.

Die Steuerung der Kontrollpunkte (Erstregistrierung und Berechnung ihrer Zeitmetriken ) erfolgt durch das Objekt 'timeManager' , das in einer einzelnen Instanz erstellt wird. Jedes Ereignis beim Übergeben des Prüfpunkts wird durch das Objekt 'timeManager' festgelegt und während des ersten Durchlaufs von ihm als Observables als 'registerEntry' registriert .

Bei jedem Durchlaufen des Kontrollpunkts wird ein timerObject- Objekt erstellt , das den Zeitpunkt seiner Erstellung festlegt . Die Ausführungszeit wird am Prüfpunkt festgelegt, wenn die Anwendung die aktuelle Ebene der Softwarehierarchie verlässt. In diesem Moment wird das timerObject des Objekts automatisch zerstört, was mit der Berechnung seiner "Lebensdauer" T einhergeht. Infolgedessen erhöht der timeManager die Häufigkeit , mit der der Prüfpunkt passiert, und die Zeit, die T. in ihm verbringt . Für alle festgelegten Kontrollpunkte sammelt timeManager Daten mit der anschließenden Veröffentlichung eines Berichts, wenn die Anwendung beendet wird.



Unten finden Sie den C ++ - Quellcode, der die integrierten Tools zur Steuerung der Ausführungszeit der Anwendung implementiert.

//     #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); } 

Die Struktur der Demoanwendung ist unten dargestellt und zeigt die Verwendung der integrierten Laufzeitsteuerungstools als Beispiel sowie eine Tabelle der erzielten Ergebnisse (Einzelheiten siehe Anhang 1. Quellcode der Demoanwendung ).





Abschnitt Nachtrag 2. Der Quellcode der Mittel der eingebauten Steuerung der Ausführungszeit der C # -Anwendung zeigt eine ähnliche Implementierung der Mittel der eingebauten Steuerung in C #.

Der Autor verwendet Paare von TimeWatcher.StartWatch () und TimeWatcher.StopWatch () , um die Ausführungszeit mühsamer (aus rechnerischer Sicht) Methoden und Verfahren im Rahmen des von EREMEX entwickelten Software-Produkts Delta Design - einem computergestützten Konstruktionssystem für elektronische Geräte - zu analysieren .

Unten finden Sie ein Beispiel für einen kurzen Bericht über Zeitmetriken einer der Funktionen des genannten Produkts.


Kurze Schlussfolgerungen


Die beschriebenen Tools können verwendet werden, um Daten zur Ausführungszeit von Anwendungen in verschiedenen Teilen des Programmcodes zu erfassen. Insbesondere ermöglichen sie Folgendes:

  • Sammeln und Sammeln von Daten zu Zeitmetriken von Ausführungsthreads in der Anwendung;
  • Schätzungen der Ausführungszeit von Programmcode durchführen, die auf elementare Sprachkonstrukte genau sind;
  • Verwalten Sie das Volumen der extrahierten Daten, indem Sie die integrierten Steuerungswerkzeuge in den entsprechenden Abschnitten der Anwendungsausführungsabläufe ein- und ausschalten
  • Entwicklung und Anwendung von Regressionstests, die die Stabilität (und die Verschlechterung) von Anwendungszeitmetriken überwachen.

Zusammenfassend ist anzumerken, dass außerhalb des Umfangs dieser Veröffentlichung Fragen zur Verwendung der beschriebenen integrierten Steuerungswerkzeuge im Zusammenhang mit Multithreading- Anwendungen auftraten und keine Analyse der Genauigkeit der erhaltenen Daten anhand von Zeitmetriken in irgendeiner Form vorgelegt wurde. Letzteres ist darauf zurückzuführen, dass in der Praxis bei der Ermittlung der Ursachen für die vorübergehende Verschlechterung einer Anwendung in erster Linie die Daten zur relativen Verteilung der Ausführungszeitkosten zwischen den Softwarekomponenten der Anwendung relevant sind . In diesem Zusammenhang treten Fragen nach der Genauigkeit der erhaltenen Daten in den Hintergrund.

Anhang 1. Quellcode für die Demo-Anwendung


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

Ergänzung 2. Quellcode der integrierten C # -Anwendungen zur Laufzeitsteuerung


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


All Articles