Das Beste ist der Feind des Guten

Bild 6

In diesem Artikel haben wir uns einmal entschlossen, unser internes SelfTester-Tool zu verbessern, mit dem wir die Qualität des PVS-Studio-Analysators testen. Die Verbesserung war einfach und schien nützlich zu sein, brachte uns aber in einige Schwierigkeiten. Später stellte sich heraus, dass wir die Idee besser aufgeben sollten.

Selftester


Wir entwickeln und fördern den statischen Code-Analysator PVS-Studio für C, C ++, C # und Java. Um die Qualität unseres Analysators zu testen, verwenden wir interne Tools, die allgemein als SelfTester bezeichnet werden. Wir haben für jede unterstützte Sprache eine separate SelfTester-Version erstellt. Es ist auf die Besonderheiten des Testens zurückzuführen und einfach bequemer. Daher haben wir derzeit drei interne SelfTester-Tools in unserem Unternehmen für C \ C ++, C # bzw. Java. Außerdem erzähle ich Ihnen etwas über die Windows-Version von SelfTester für C \ C ++ Visual Studio-Projekte und nenne sie einfach SelfTester. Dieser Tester war der erste in der Reihe ähnlicher interner Tools. Er ist der fortschrittlichste und komplexeste von allen.

Wie funktioniert SelfTester? Die Idee ist einfach: Nehmen Sie einen Pool von Testprojekten (wir verwenden echte Open Source-Projekte) und analysieren Sie sie mit PVS-Studio. Als Ergebnis wird für jedes Projekt ein Analysatorprotokoll erstellt. Dieses Protokoll wird mit dem Referenzprotokoll desselben Projekts verglichen. Beim Vergleichen von Protokollen erstellt SelfTester eine Zusammenfassung der Protokolle, die auf bequeme, entwicklerfreundliche Weise verglichen werden.

Nach dem Studium der Zusammenfassung kommt ein Entwickler zu Änderungen des Verhaltens des Analysators in Abhängigkeit von Anzahl und Art der Warnungen, Arbeitsgeschwindigkeit, internen Analysatorfehlern usw. All diese Informationen sind sehr wichtig: Sie können sich darüber im Klaren sein, wie der Analysator mit seiner Arbeit umgeht.

Basierend auf der Zusammenfassung des Protokollvergleichs führt ein Entwickler Änderungen im Analysatorkern ein (z. B. beim Erstellen einer neuen Diagnoseregel) und steuert sofort das Ergebnis seiner Änderungen. Wenn ein Entwickler keine Probleme mehr mit einem regulären Protokollvergleich hat, erstellt er eine aktuelle Warnprotokollreferenz für ein Projekt. Ansonsten geht die Arbeit weiter.

Die Aufgabe von SelfTester besteht also darin, mit einem Pool von Testprojekten zu arbeiten (es gibt übrigens mehr als 120 davon für C / C ++). Projekte für den Pool werden in Form von Visual Studio-Lösungen ausgewählt. Dies geschieht, um zusätzlich die Arbeit des Analysators an verschiedenen Visual Studio-Versionen zu überprüfen, die den Analysator unterstützen (zu diesem Zeitpunkt von Visual Studio 2010 bis Visual Studio 2019).

Hinweis: Außerdem werde ich die Konzepte Lösung und Projekt trennen und ein Projekt als Teil einer Lösung betrachten.

Die Benutzeroberfläche von SelfTester sieht folgendermaßen aus:

Bild 3

Links befindet sich eine Liste der Lösungen, rechts die Ergebnisse einer Überprüfung für jede Visual Studio-Version.

Graue Bezeichnungen "Nicht unterstützt" zeigen an, dass eine Lösung eine ausgewählte Visual Studio-Version nicht unterstützt oder für diese Version nicht konvertiert wurde. Einige Lösungen verfügen über eine Konfiguration in einem Pool, die eine bestimmte Visual Studio-Version zur Überprüfung angibt. Wenn keine Version angegeben ist, wird eine Lösung für alle nachfolgenden Visual Studio-Versionen aktualisiert. Ein Beispiel für eine solche Lösung finden Sie im Screenshot "smart_ptr_check.sln" (eine Überprüfung wird für alle Visual Studio-Versionen durchgeführt).

Ein grünes Etikett "OK" zeigt an, dass bei einer regelmäßigen Überprüfung keine Unterschiede zum Referenzprotokoll festgestellt wurden. Ein rotes Etikett "Diff" zeigt Unterschiede an. Diese Etiketten müssen besonders beachtet werden. Nach zweimaligem Klicken auf das gewünschte Etikett wird die ausgewählte Lösung in einer verwandten Visual Studio-Version geöffnet. Dort wird auch ein Fenster mit einem Warnprotokoll geöffnet. Mit den Steuertasten unten können Sie die Analyse der ausgewählten oder aller Lösungen erneut ausführen, das ausgewählte Protokoll (oder alle auf einmal) referenzieren usw.

Die Ergebnisse von SelfTester werden immer im HTML-Bericht (Diffs-Bericht) dupliziert.

Zusätzlich zur grafischen Benutzeroberfläche verfügt SelfTester über automatisierte Modi für Nacht-Build-Läufe. Das übliche Verwendungsmuster, das Entwickler wiederholen, wird jedoch von einem Entwickler während des Arbeitstages ausgeführt. Daher ist eine der wichtigsten Eigenschaften von SelfTester die Arbeitsgeschwindigkeit.

Warum Geschwindigkeit wichtig ist:

  1. Die Leistung jedes Schritts ist für Nachttestläufe sehr wichtig. Je schneller die Tests bestehen, desto besser. Derzeit beträgt die durchschnittliche Leistungszeit von SelfTester mehr als 2 Stunden.
  2. Wenn SelfTester tagsüber ausgeführt wird, muss ein Entwickler weniger auf das Ergebnis warten, was die Produktivität seiner Mitarbeiter erhöht.

Diesmal war es die Beschleunigung der Leistung, die zum Grund für Verbesserungen wurde.

Multithreading in SelfTester


SelfTester wurde ursprünglich als Multithread-Anwendung mit der Möglichkeit entwickelt, mehrere Lösungen gleichzeitig zu testen. Die einzige Einschränkung bestand darin, dass Sie nicht gleichzeitig dieselbe Lösung für verschiedene Visual Studio-Versionen überprüfen konnten, da viele Lösungen vor dem Testen auf bestimmte Versionen von Visual Studio aktualisiert werden müssen. Währenddessen werden Änderungen direkt in Dateien der .vcxproj- Projekte übernommen, was zu Fehlern beim parallelen Ausführen führt.

Um die Arbeit effizienter zu gestalten, verwendet SelfTester einen intelligenten Taskplaner, um einen streng begrenzten Wert für parallele Threads festzulegen und zu verwalten.

Der Planer wird auf zwei Ebenen verwendet. Die erste ist die Lösungsebene , mit der die .sln- Lösung mit dem Dienstprogramm PVS-Studio_Cmd.exe getestet wird . Der gleiche Scheduler, jedoch mit einer anderen Einstellung für den Parallelitätsgrad , wird in PVS-Studio_Cmd.exe (auf der Ebene der Quelldateien) verwendet.

Der Parallelitätsgrad ist ein Parameter, der angibt, wie viele parallele Threads gleichzeitig ausgeführt werden müssen. Für den Parallelitätsgrad der Lösungen bzw. der Dateiebene wurden vier und acht Standardwerte ausgewählt. Daher muss die Anzahl der parallelen Threads in dieser Implementierung 32 betragen (4 gleichzeitig getestete Lösungen und 8 Dateien). Diese Einstellung erscheint uns für die Arbeit des Analysators auf einem Acht-Kern-Prozessor optimal.

Ein Entwickler kann andere Werte des Parallelitätsgrads selbst entsprechend seiner Computerleistung oder aktuellen Aufgaben festlegen. Wenn ein Entwickler diesen Parameter nicht angibt, wird standardmäßig die Anzahl der logischen Systemprozessoren ausgewählt.

Hinweis: Nehmen wir weiter an, dass wir uns mit dem Standardgrad der Parallelität befassen.

Der Scheduler LimitedConcurrencyLevelTaskScheduler wird von System.Threading.Tasks.TaskScheduler geerbt und verfeinert, um beim Arbeiten über ThreadPool die maximale Parallelitätsstufe bereitzustellen . Vererbungshierarchie:

LimitedConcurrencyLevelTaskScheduler : PausableTaskScheduler { .... } PausableTaskScheduler: TaskScheduler { .... } 

Mit PausableTaskScheduler können Sie die Aufgabenleistung unterbrechen. Darüber hinaus bietet LimitedConcurrencyLevelTaskScheduler eine intellektuelle Kontrolle über die Aufgabenwarteschlange und die Planung ihrer Leistung unter Berücksichtigung des Parallelitätsgrads, des Umfangs der geplanten Aufgaben und anderer Faktoren. Ein Scheduler wird verwendet, wenn LimitedConcurrencyLevelTaskScheduler- Tasks ausgeführt werden.

Gründe für Verfeinerungen


Das oben beschriebene Verfahren hat einen Nachteil: Es ist nicht optimal, wenn es sich um Lösungen unterschiedlicher Größe handelt. Die Größe der Lösungen im Testpool ist sehr unterschiedlich: von 8 KB bis 4 GB - die Größe eines Ordners mit einer Lösung und von 1 bis zu mehreren Tausend Quellcodedateien in jeder.

Der Scheduler stellt Lösungen einfach nacheinander ohne intelligente Komponente in die Warteschlange. Ich möchte Sie daran erinnern, dass standardmäßig nicht mehr als vier Lösungen gleichzeitig getestet werden können. Wenn derzeit vier große Lösungen getestet werden (die Anzahl der Dateien in jeder ist mehr als acht), wird davon ausgegangen, dass wir effektiv arbeiten, da wir so viele Threads wie möglich verwenden (32).

Stellen wir uns jedoch eine ziemlich häufige Situation vor, in der mehrere kleine Lösungen getestet werden. Beispielsweise ist eine Lösung groß und enthält 50 Dateien (die maximale Anzahl von Threads wird verwendet), während andere drei Lösungen jeweils drei, vier, fünf Dateien enthalten. In diesem Fall verwenden wir nur 20 Threads (8 + 3 + 4 + 5). Die Prozessorzeit wird nicht ausreichend genutzt und die Gesamtleistung reduziert.

Hinweis : Tatsächlich ist der Engpass normalerweise das Festplattensubsystem und nicht der Prozessor.

Verbesserungen


Die Verbesserung, die in diesem Fall offensichtlich ist, ist das Ranking der Liste der getesteten Lösungen. Wir müssen die festgelegte Anzahl gleichzeitig ausgeführter Threads (32) optimal nutzen, indem wir Testprojekte mit der richtigen Anzahl von Dateien übergeben.

Betrachten wir noch einmal unser Beispiel zum Testen von vier Lösungen mit jeweils der folgenden Anzahl von Dateien: 50, 3, 4 und 5. Die Aufgabe, die eine Lösung mit drei Dateien überprüft, funktioniert wahrscheinlich am schnellsten. Es ist am besten, stattdessen eine Lösung mit acht oder mehr Dateien hinzuzufügen (um das Maximum aus den verfügbaren Threads für diese Lösung zu verwenden). Auf diese Weise werden 25 Threads gleichzeitig verwendet (8 + 8 + 4 + 5). Nicht schlecht. Sieben Threads sind jedoch noch nicht beteiligt. Und hier kommt die Idee einer weiteren Verfeinerung, die darin besteht, die Beschränkung auf vier Threads für Testlösungen aufzuheben. Weil wir jetzt nicht nur eine, sondern mehrere Lösungen mit 32 Threads hinzufügen können. Stellen wir uns vor, wir haben zwei weitere Lösungen mit jeweils drei und vier Dateien. Durch Hinzufügen dieser Aufgaben wird die "Lücke" nicht verwendeter Threads vollständig geschlossen, und es werden 32 (8 + 8 + 4 + 5 + 3 + 4 ) davon vorhanden sein.

Hoffentlich ist die Idee klar. Tatsächlich erforderte die Implementierung dieser Verbesserungen auch keinen großen Aufwand. Alles war an einem Tag erledigt.

Wir mussten die Aufgabenklasse überarbeiten: Erben von System.Threading.Tasks.Task und Zuweisung des Feldes "weight". Wir verwenden einen einfachen Algorithmus, um das Gewicht einer Lösung festzulegen: Wenn die Anzahl der Dateien weniger als acht beträgt, entspricht das Gewicht dieser Anzahl (z. B. 5). Wenn die Zahl größer oder gleich acht ist, ist das Gewicht gleich acht.

Wir mussten auch den Scheduler ausarbeiten: Bringen Sie ihm bei, Lösungen mit dem erforderlichen Gewicht auszuwählen, um einen Maximalwert von 32 Threads zu erreichen. Wir mussten auch mehr als vier Threads für das gleichzeitige Testen von Lösungen zulassen.

Schließlich brauchten wir einen ersten Schritt, um alle Lösungen im Pool zu analysieren (Auswertung mit der MSBuild-API), um die Lösungen auszuwerten und das Gewicht festzulegen (Anzahl der Dateien mit Quellcode abrufen).

Ergebnis


Ich denke, nach einer so langen Einführung haben Sie bereits vermutet, dass nichts daraus geworden ist.

Bild 12

Es ist jedoch gut, dass die Verbesserungen einfach und schnell waren.

Hier kommt der Teil des Artikels, in dem ich Ihnen erzählen werde, was "uns in viele Schwierigkeiten gebracht hat" und was damit zu tun hat.

Nebenwirkungen


Ein negatives Ergebnis ist also auch ein Ergebnis. Es stellte sich heraus, dass die Anzahl der großen Lösungen im Pool die Anzahl der kleinen (weniger als acht Dateien) bei weitem übersteigt . In diesem Fall haben diese Verbesserungen keine nennenswerten Auswirkungen, da sie fast unsichtbar sind: Das Testen kleiner Projekte nimmt im Vergleich zur Zeit, die für große Projekte benötigt wird, winzig viel Zeit in Anspruch.

Wir haben uns jedoch entschlossen, die neue Verfeinerung als "nicht störend" und potenziell nützlich zu belassen. Darüber hinaus wird der Pool an Testlösungen ständig aufgefüllt, sodass sich die Situation in Zukunft möglicherweise ändern wird.

Und dann ...

Bild 5

Einer der Entwickler beschwerte sich über den Absturz des SelfTesters. Nun, das Leben passiert. Um zu verhindern, dass dieser Fehler verloren geht, haben wir einen internen Vorfall (Ticket) mit dem Namen "Ausnahme bei der Arbeit mit SelfTester" erstellt. Der Fehler ist bei der Auswertung des Projekts aufgetreten. Obwohl eine große Anzahl von Fenstern mit Fehlern das Problem wieder in der Fehlerbehandlungsroutine anzeigte. Dies wurde jedoch schnell beseitigt, und in der nächsten Woche stürzte nichts ab. Plötzlich beschwerte sich ein anderer Benutzer über SelfTester. Wieder der Fehler einer Projektevaluierung:

Bild 8

Diesmal enthielt der Stapel viele nützliche Informationen - der Fehler lag im XML-Format vor. Es ist wahrscheinlich, dass beim Behandeln der Datei des Proto_IRC.vcxproj- Projekts (seiner XML-Darstellung) etwas mit der Datei selbst passiert ist, weshalb XmlTextReader dies nicht verarbeiten konnte.

Da wir in relativ kurzer Zeit zwei Fehler hatten, haben wir uns das Problem genauer angesehen. Darüber hinaus wird SelfTester, wie oben erwähnt, von Entwicklern sehr aktiv genutzt.

Zunächst haben wir den letzten Absturz analysiert. Leider fanden wir nichts Verdächtiges. Nur für den Fall, dass wir Entwickler (SelfTester-Benutzer) gebeten haben, ein Auge auf mögliche Fehler zu werfen und diese zu melden.

Wichtiger Punkt: Der fehlerhafte Code wurde in SelfTester wiederverwendet. Es wurde ursprünglich verwendet, um Projekte im Analysator selbst ( PVS-Studio_Cmd.exe ) auszuwerten . Deshalb ist die Aufmerksamkeit auf das Problem gewachsen. Es gab jedoch keine derartigen Abstürze im Analysator.

In der Zwischenzeit wurde das Ticket über Probleme mit SelfTester durch neue Fehler ergänzt:

Bild 9

Wieder XmlException . Offensichtlich gibt es irgendwo konkurrierende Threads, die mit dem Lesen und Schreiben von Projektdateien arbeiten. SelfTester arbeitet in folgenden Fällen mit Projekten:

  1. Projektevaluierung im Zuge der vorläufigen Berechnung der Lösungsgewichte: ein neuer Schritt, der zunächst Verdacht erregte;
  2. Aktualisieren von Projekten auf die erforderlichen Visual Studio-Versionen: Wird direkt vor dem Testen durchgeführt (Projekte stören nicht) und darf den Arbeitsprozess nicht beeinträchtigen.
  3. Projektevaluierung während des Testens: ein etablierter thread-sicherer Mechanismus, der von PVS-Studio_Cmd.exe wiederverwendet wird ;
  4. Wiederherstellen von Projektdateien (Ersetzen geänderter .vcxproj- Dateien durch anfängliche Referenzdateien) beim Beenden von SelfTester, da Projektdateien während der Arbeit auf die erforderlichen Visual Studio-Versionen aktualisiert werden können. Dies ist ein letzter Schritt, der keine Auswirkungen auf andere Mechanismen hat.

Der Verdacht fiel auf den neuen Code, der zur Optimierung hinzugefügt wurde (Gewichtsberechnung). Die Codeuntersuchung ergab jedoch, dass der Tester immer korrekt bis zum Ende der Vorbewertung wartet, wenn ein Benutzer die Analyse direkt nach dem Start von SelfTester ausführt. Dieser Ort sah sicher aus.

Auch hier konnten wir die Ursache des Problems nicht identifizieren.

Schmerz


Den ganzen nächsten Monat stürzte SelfTester immer wieder ab. Das Ticket füllte sich weiterhin mit Daten, aber es war nicht klar, was mit diesen Daten zu tun war. Die meisten Abstürze waren mit derselben XmlException. Gelegentlich gab es noch etwas anderes, aber denselben wiederverwendeten Code aus PVS-Studio_Cmd.exe .

Bild 1

Traditionell stellen interne Tools keine sehr hohen Anforderungen, daher haben wir die Fehler von SelfTester immer wieder nach einem Restprinzip herausgesucht. Von Zeit zu Zeit wurden verschiedene Personen involviert (während des gesamten Vorfalls arbeiteten sechs Personen an dem Problem, darunter zwei Praktikanten). Wir mussten uns jedoch von dieser Aufgabe ablenken lassen.

Unser erster Fehler. Zu diesem Zeitpunkt hätten wir dieses Problem ein für alle Mal lösen können. Wie? Es war klar, dass der Fehler durch eine neue Optimierung verursacht wurde. Immerhin hat vorher alles gut funktioniert, und der wiederverwendete Code kann eindeutig nicht so schlecht sein. Darüber hinaus hatte diese Optimierung keinen Nutzen gebracht. Was musste also getan werden? Entfernen Sie diese Optimierung. Wie Sie wahrscheinlich verstehen, wurde es nicht getan. Wir haben weiter an dem Problem gearbeitet, das wir selbst geschaffen haben. Wir suchten weiter nach der Antwort: "WIE ???" Wie stürzt es ab? Es schien richtig geschrieben zu sein.

Unser zweiter Fehler. Andere Leute haben sich an der Lösung des Problems beteiligt . Es ist ein sehr, sehr großer Fehler. Das Problem wurde nicht nur nicht gelöst, sondern es wurden auch zusätzliche Ressourcen verschwendet. Ja, neue Leute brachten neue Ideen mit, aber es hat viel Arbeitszeit gekostet, diese Ideen (umsonst) umzusetzen. Irgendwann hatten unsere Praktikanten Testprogramme geschrieben, die die Bewertung ein und desselben Projekts in verschiedenen Threads mit paralleler Änderung eines Projekts in einem anderen Projekt emulierten. Es hat nicht geholfen. Wir haben nur herausgefunden, dass die MSBuild-API im Inneren threadsicher ist, was wir bereits kennen. Wir haben auch das automatische Speichern von Mini-Dumps hinzugefügt, wenn die XmlException- Ausnahme auftritt. Wir hatten jemanden, der das alles debuggte. Armer Kerl! Es gab Diskussionen, wir haben andere unnötige Dinge getan.

Zum Schluss noch der dritte Fehler. Wissen Sie, wie viel Zeit von dem Moment an vergangen ist, als das SelfTester-Problem aufgetreten ist, bis es gelöst wurde? Nun, du kannst dich selbst zählen. Das Ticket wurde am 17.09.2008 erstellt und am 20.02.2019 geschlossen. Es gab mehr als 40 Kommentare! Leute, das ist viel Zeit! Wir haben uns erlaubt , fünf Monate mit DIESEM beschäftigt zu sein. Gleichzeitig waren wir damit beschäftigt, Visual Studio 2019 zu unterstützen, die Java-Sprachunterstützung hinzuzufügen, den MISRA C / C ++ - Standard einzuführen, den C # -Analysator zu verbessern, aktiv an Konferenzen teilzunehmen, eine Reihe von Artikeln zu schreiben usw. Alle diese Aktivitäten erhielten aufgrund eines dummen Fehlers in SelfTester weniger Zeit für Entwickler.

Leute, lernt aus unseren Fehlern und macht das nie so. Wir werden auch nicht.

Das war's, ich bin fertig.

Bild 15

Okay, es war ein Witz, ich werde dir sagen, was das Problem mit SelfTester war :)

Bingo!


Glücklicherweise gab es unter uns eine Person mit klaren Augen (mein Kollege Sergey Vasiliev), die das Problem nur aus einem ganz anderen Blickwinkel betrachtete (und auch - er hatte ein bisschen Glück). Was ist, wenn es im SelfTester in Ordnung ist, aber etwas von außen die Projekte zum Absturz bringt? Normalerweise hatten wir nichts mit SelfTester gestartet, in einigen Fällen haben wir die Ausführungsumgebung streng kontrolliert. In diesem Fall könnte dieses "Etwas" SelfTester selbst sein, aber eine andere Instanz.

Beim Beenden von SelfTester funktioniert der Thread, der Projektdateien aus Referenzen wiederherstellt, noch eine Weile. Zu diesem Zeitpunkt wird der Tester möglicherweise erneut gestartet. Der Schutz gegen die gleichzeitige Ausführung mehrerer SelfTester-Instanzen wurde später hinzugefügt und sieht nun wie folgt aus:

Bild 16

Aber zu diesem Zeitpunkt hatten wir es nicht.

Nüsse, aber wahr - während fast sechs Monaten der Qual hat niemand darauf geachtet. Das Wiederherstellen von Projekten aus Referenzen ist ein relativ schneller Hintergrundvorgang, der jedoch leider nicht schnell genug ist, um den Neustart von SelfTester nicht zu beeinträchtigen. Und was passiert, wenn wir es starten? Das ist richtig, die Gewichte von Lösungen zu berechnen. Ein Prozess schreibt .vcxproj- Dateien neu, während ein anderer versucht, sie zu lesen. Sagen Sie Hallo zu XmlException .

Sergey fand dies alles heraus, als er dem Tester die Möglichkeit hinzufügte, zu einem anderen Satz von Referenzprotokollen zu wechseln. Dies wurde erforderlich, nachdem dem Analysator eine Reihe von MISRA-Regeln hinzugefügt wurden. Sie können direkt in der Benutzeroberfläche wechseln, während der Benutzer dieses Fenster sieht:

Bild 14

Danach wird SelfTester neu gestartet . Und früher haben Benutzer das Problem anscheinend selbst emuliert und den Tester erneut ausgeführt.

Schuldzuweisungen und Schlussfolgerungen


Natürlich haben wir die zuvor erstellte Optimierung entfernt (dh deaktiviert). Darüber hinaus war es viel einfacher, als eine Art Synchronisation zwischen den Neustarts des Testers selbst durchzuführen. Und alles begann nach wie vor perfekt zu funktionieren. Als zusätzliche Maßnahme haben wir den oben genannten Schutz gegen den gleichzeitigen Start des Testers hinzugefügt.

Ich habe oben bereits über unsere Hauptfehler bei der Suche nach dem Problem geschrieben, also genug von Selbstkennzeichnung. Wir sind Menschen, also könnten wir uns irren. Es ist wichtig, aus eigenen Fehlern zu lernen und Schlussfolgerungen zu ziehen. Die Schlussfolgerungen aus diesem Fall sind recht einfach:

  • Wir sollten die Komplexität der Aufgaben überwachen und abschätzen.
  • Manchmal müssen wir irgendwann aufhören;
  • Versuchen Sie, das Problem genauer zu betrachten. Mit der Zeit kann man einen Tunnelblick auf den Fall bekommen, während dies eine neue Perspektive erfordert.
  • Haben Sie keine Angst, alten oder unnötigen Code zu löschen.

Das war's, diesmal bin ich definitiv fertig. Vielen Dank für das Lesen bis zum Ende. Ich wünsche Ihnen fehlerfreien Code!

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


All Articles