Mit PHP das Server-Betriebssystem erkennen
Häufig möchte man das Betriebssystem des Besuchers erkennen, um beispielsweise Statistiken zu führen. Noch häufiger wird natürlich der Browser abgefragt, aber das ist hier auch nicht gemeint.
In diesem Fall möchte ich das Betriebssystem erkennen, unter dem mein PHP-Script läuft. „Warum willst du das herausfinden, du weißt doch wohl, wo deine Scripte laufen!“ sagt ihr nun? Naja, meistens ja, aber manchmal auch nicht. Und wenn beispielsweise meine Scripte und Webseiten auf beiden Betriebsystemen laufen sollen, muß ich diese Information haben, um einige Dinge zu tun. Ich habe lang überlegt, und wirklich einfache und gebräuchliche Beispiele fallen mir nicht ein, denn zum Beispiel für den php.ini-Trenner bei der include_path-Variablen gibt es eine globale PHP-Konstante, man kann also einfach schreiben:
$include_path = get_include_path(); $include_path.= PATH_SEPARATOR . realpath(__DIR__ . '../'library'); set_include_path($include_path);
(Um im Folgenden alles zu vereinfachen, betrachte ich nur Windows und Linux)
Um Verzeichnisse zu trennen, kann man einfach „/“ nutzen, das funktioniert auch unter Windows (da ist ja eigentlich „\“ gebräuchlich). Oder man nutzt die Konstante DIRECTORY_SEPARATOR.
Das Problem mit den unterschiedlichen Zeilenumbrüchen löst man mit PHP_EOL.
Ein Beispiel, wo wir aber betriebssystemspezifisch arbeiten müssen ist beispielsweise das Mapping von Netzlaufwerken via SMB. Macht man zwar nicht alle Tage, aber wenn man in der Firma mit vielen Servern und entfernten Verzeichnissen arbeitet, kommt das schonmal vor. Unter Windows muss man dazu „net use“ benutzen, unter Linux macht man das mit dem „smbclient“ (wenn man dynamisch mappen möchte, sonst verwendet man natürlich direkt einen „statischen“ Mountpoint).
Man benötigt außerdem Informationen über das Betriebssystem, falls man Linux- oder Windows-spezifische Funktionen nutzt wie beispielsweise alle PCNTL-Funktionen oder die win32service-Extension. Außerdem sollte man aufpassen sobald man exec() oder system() benutzt, häufig sind diese dann nicht portabel.
PHP bietet mehrere Möglichkeiten, die aber nicht alle zuverlässig funktionieren. Heute auf der Arbeit habe ich diese Zeile in einem Projekt gefunden:
if (isset($_SERVER['SERVER_SOFTWARE']) && is_numeric(stripos($_SERVER['SERVER_SOFTWARE'], 'linux'))) {
Das sieht nicht nur blöd aus, es funktioniert auch nicht immer, denn auf einigen Linux-Systemen gibt es diese Variable, auf anderen wiederum nicht.
Wenn man auf php.net etwas sucht, findet man die PHP-Konstante PHP_OS:
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
PHP_OS enthält das Betriebssystem, auf dem PHP gebuilded wurde (zumindestens wenn man den PHP-Entwicklern glaubt, ein User behauptet hier etwas anderes). Ich weiß nicht, was da zB rauskommt, wenn man PHP für Linux unter cygwin kompiliert. Also auch noch nicht ideal.
Die Funktion meiner Wahl ist derzeit
if (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN') {
Der Befehl „uname“ kennt man vielleicht schon von Linux, aber auch unter Windows funktioniert php_uname() und liefert die Informationen des Betriebssystems zurück. Genutzt wird diese Funktion auch beispielsweise von PEAR OS_Guess.php. Diese Klasse versucht auch noch weitere Informationen zu gewinnen. Genutzt habe ich sie allerdings noch nicht.
Manche Hoster verbieten jedoch diese Funktion. Also immer dran denken:
if (function_exists('php_uname')) {</code><code>
Man muss wohl doch PHP_OS nutzen. Oder man schreibt sich eine Funktion, die zB anhand des DIRECTORY_SEPARATOR die Unterscheidung macht. Oder oder oder…
Warning: php_uname() has been disabled for security reasons in /usr/www/...
Mit PHP 5.3 kommen jetzt auch noch ein paar weitere globale Konstanten hinzu, die es wohl nur unter Windows gibt. Daran könnte man es dann auch festmachen. Dann braucht man aber Minimum 5.3, was man wohl in den nächsten Jahren noch nicht überall auf der Welt erwarten kann. Geht also auch nicht.
So oder so, das ganze Thema ist irgendwie konfus. Welchen Best-Practice habt ihr?
LDAP Authentifizierung und andere Abfragen
Zuhause beim Programmieren oder bei der Erstellung von einfachen Webseiten wird man mit LDAP wahrscheinlich nicht viel zu tun haben, doch wenn man häufiger Intranet-Projekte erstellt oder in irgendeiner Weise den Login an ein OpenLDAP-Verzeichnis oder ActiveDirectory knüpfen will, kommt man an LDAP kaum vorbei.
Grundsätzliche Informationen über LDAP und ActiveDirectory findet man natürlich bei Wikipedia. Ich möchte hier nicht 5 Seiten darüber schreiben, wofür soetwas gut ist und warum man sowas haben sollte, da könnt ihr euch am besten selbst die Informationen sammeln. Stichworte sind wie gesagt LDAP, ActiveDirectory, OpenLDAP und für die hart gesottenen die RFC 2307.
Im Firmenumfeld werden Administrationsseiten (von denen es in größeren Firmen schnell Unmengen gibt) natürlich auch geschützt, sodass nur die berechtigten Personen darauf zugreifen können. Da man nicht für jede Seite ein eigenes Passwort etc. verwenden möchte, nutzt man das vorhandene ActiveDirectory, um Zugriffe auf die Seiten zu vergeben. Außerdem ist es damit einfacher, bei Ausscheiden eines Mitarbeiters zentral an einer Stelle den Account zu löschen.
Nur wie macht man das? Wie gestatte ich einem ActiveDirectory-User den Zugriff, wohingegen ich anderen den Zugriff verweigere?
Es gibt mehrere Lösungen. Zum einen bietet der Webserver da Möglichkeiten. Beim IIS zum Beispiel kann man bei der Konfiguration der Website entweder anonymen Zugriff erlauben (d.h. jeder Besucher kann sie betreten), oder man aktiviert eine Passwortabfrage, wie zB:
Falls man dann die entsprechende Webseite betreten möchte, kommt eine Abfrage:
Damit ist schonmal sichergestellt, dass man nur mit einen gültigen Account Zutritt erlangt. Doch wie bestimme ich nun, dass nur eine Untermenge aller Accounts zugelassen werden soll? Im Falle des IIS macht man das mit Hilfe der NTFS-Berechtigungen. Wenn man auf das DocumentRoot nur denjenigen Leserechte gibt, die auch die Seite betreten können sollen, haben wir genau das, was wir wollen.
Auch der Apache kann natürlich etwas vergleichbares. Dazu benötigt man das Modul mod_auth_ldap. Hier eine einfache Beispielkonfiguration:
<Location /example-repository> # LDAP soll für die Authentifizierung zuständig sein. AuthLDAPAuthoritative on AuthType Basic AuthName "Mein geschütztes Verzeichnis" # Wenn anonyme Zugriffe auf nicht erlaubt sind müssen sie hier # den DN für einen Benutzer angeben, der für den Lesezugriff # verwendet werden kann. AuthLDAPBindDN "CN=browse_user,OU=FunktionaleUser,DC=example,DC=com" # Das Passwort für den „Browse User“ AuthLDAPBindPassword sicheres_passwort # Die LDAP URL für die Verbindung. # Alle Verzeichnisse unterhalb der angegebenen „Bind URL“ werden # durchsucht. Das Feld „login“ wird für die Suche nach dem Benutzernamen verwendet. # Format: scheme://host:port/basedn?attribute?scope?filter AuthLDAPURL "ldap://ldap.example.com:389/DC=example,DC=com?login?sub?(objectClass=*)" # Natürlich ist auch eine gesicherte Verbindung möglich. Beispiel: # "ldaps://ldap.example.com:636/DC=example,DC=com?login?sub?(objectClass=*)" Require valid-user </Location>
Weitere Informationen dazu gibt es natürlich auf der entsprechenden Webseite zu mod_auth_ldap.
Dies sind also Methoden, die die Webserver allein regeln, PHP bekommt davon garnichts mit. Falls die Authentifizierung erfolgreich war (Der Login ist korrekt und dieser User hat Leserechte), kann man in PHP mittels der Variablen $_SERVER[‚AUTH_USER‘] den Benutzernamen herausfinden.
Um noch flexibler zu sein, kann man diese Authentifizierung natürlich auch direkt in PHP erledigen. Dazu bietet php einige ldap_* Funktionen, die es uns ermöglichen, via LDAP das Verzeichnis zu durchsuchen und Informationen daraus auszulesen. Außerdem kann man sich dann ein formschönes Login-Formular basteln, und hat nicht so einen grauen Kasten.
Um sich mit einem LDAP-Server verbinden zu können, benötigt man die Serverdaten, die Protokollversion und natürlich einen Login für den LDAP-Server, das ist der Bind-User. Das sieht dann ungefähr so aus:
function setupLdapConnection() { // get ldap connection to domainX $ldapOptions = array ( 'binddn' => 'cn=ldapsearch,ou=serviceuser,DC=domainX,DC=net', 'bindpw' => 'ldapsearchpwd', 'basedn' => 'DC=domainX,DC=net', 'host' => 'ldap.domainX.net' ); $ldap = ldap_connect($ldapOptions['host']); if ($ldap == false) { throw new Exception('LDAP connnect failed'); } ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0); $res = ldap_bind($ldap, $ldapOptions['binddn'], $ldapOptions['bindpw']); if ($res === false) { throw new Exception('Unable to bind to LDAP server'); } return $ldap; }
Damit haben wir nun eine LDAP-Connection aufgebaut, und dann kann man anfangen zu Suchen, beispielsweise so:
$ldap = setupLdapConnection(); $baseDn = 'DC=domainX,DC=net'; $attributes = array('cn', 'mail', 'objectClass', 'sAMAccountName', 'extensionAttribute13'); $filter = 'cn=User123'; $res = ldap_search($ldap, $baseDn, $filter, $attributes); if ($res === false) { return "LDAP search failed\r\n"; } if (ldap_count_entries($ldap, $res) === 0) { return "LDAP search failed, no entries found\r\n"; } $entry = ldap_get_entries($ldap, $res); if ($entry === false) { return "LDAP get entry failed\r\n"; } return $res;
In $res haben wir dann das Ergebnis-Array mit den gewünschten Informationen. Man kann natürlich auch mit Hilfe der $baseDn nur in einem bestimmten Zweig suchen, oder mit Hilfe des $filter nach anderen Attributen suchen.
Mit diesen Funktionen kann man dann auch zB prüfen, ob ein User Mitglied einer Gruppe ist (Attribut memberOf), oder seine Email-Adresse herausfinden oder oder…
Zur Authentifizierung selbst gibt es natürlich auch schon fertige Klassen. Zu nennen sind da wohl PEAR_Auth und Zend_Auth_Adapter_Ldap. Hier ein schönes kurzes Beispiel mit dem ZF:
application.ini:
ldap.server1.host = ldap.domainX.net ldap.server1.useSsl = false ldap.server1.accountDomainName = domainX.net ldap.server1.accountDomainNameShort = domainX ldap.server1.accountCanonicalForm = 3 ldap.server1.accountFilterFormat = "(&(objectClass=user)(sAMAccountName=%s))" ldap.server1.username = "cn=ldapsearch,ou=serviceuser,DC=domainX,DC=net" ldap.server1.password = ldapsearchpwd ldap.server1.baseDn = "DC=domainX,DC=net" ldap.server1.bindRequiresDn = true
IndexController:
public function loginAction() { $form = new forms_LoginForm(); if ($this->getRequest()->isPost()) { $formData = $this->getRequest()->getPost(); if ($form->isValid($formData)) { $values = $form->getValues(); $loginSuccessful = false; $auth = Zend_Auth::getInstance(); $options = Zend_Registry::get('configIni')->ldap->toArray(); $adapter = new Zend_Auth_Adapter_Ldap($options, $values['loginusername'], $values['loginpassword']); $result = $auth->authenticate($adapter); if ($result->isValid()) { $namespace = new Zend_Session_Namespace(); $namespace->username = $values['loginusername']; } else { $this->_flashMessenger->addMessage('error:login_failed_ldap'); } $this->_redirect(""); } else { $form->populate($formData); } } $this->view->form = $form; }
Ist natürlich alles auf das Wesentliche gekürzt, man hat normalerweise natürlich noch diverse Sicherheitsabfragen oder zB einen try-catch-Block um das authenticate() etc.
Nutzt ihr auch LDAP, und wenn ja wie?
Status, und bevor ich es wieder vergesse
Als erstes möchte ich mich entschuldigen für die letzten Tage, wo ich nichts von mir hab hören lassen. Durch den Urlaub, den ich letzte Woche hatte, habe ich den Blog etwas vernachlässigt.
Außerdem bin ich nach wie vor auf der Suche nach einem stabilen, unbefristeten Job als PHP-Entwickler hier im Münsterland, weshalb ich hier und da Stellengesuche lese, einige wenige Bewerbungen schreibe, und mit meinem derzeitigen Arbeitgeber verhandle, ob es hier eine Zukunft für mich gibt oder nicht (Einstellungsstops während der Krise sind anstrengend für zeitlich befristete Angestellte, alles ist sehr kurzfristig und man kann höchstens wenige Wochen in die Zukunft planen).
Außerdem habe ich während des Urlaubs meine favorisierten Blogs nicht gelesen, weshalb ich auch erst heute gratulieren kann zum einjährigen Bestehen von PHP hates me!. Gratulation, jeden Tag einen Artikel ist eine tolle Leistung!
Um beim Gewinnspiel mitzumachen, möchte ich auch noch schnell meinen Lieblingsartikel verlinken, der natürlich die Blogvorstellung meines Blogs im Rahmen der Aktion „Ein Herz für Blogger“ ist.
Das war’s auch schon wieder für heute.
Sinnvolle SVN Hooks für PHP Projekte
Dieser Artikel ist nur für diejenigen gedacht, die SVN bereits kennen. Wer jetzt die Stirn runzelt, möge sich vorher bei wikipedia oder youtube informieren und mal erste Versuche mit einem SVN-Server sammeln.
Erstmal zur Begrifflichkeit ansich: Hooks sind Interfaces zu kleinen externen Programmen, die zu bestimmten Zeiten während eines Programmablaufs aufgerufen werden können. Sie klinken sich also in den Ablauf ein.
Beim SVN gibt es 3 interessante Hooks, die häufig genutzt werden (insgesamt gibt es 9):
– start-commit
– pre-commit
– post-commit
Im SVN Handbuch kann man genauer nachlesen, wann diese Hooks aufgerufen werden.
Das erste Script, welches wir als pre-commit-Hook aufrufen wollen, ist ein einfaches PHP-Lint, wir wollen also die PHP-Syntax testen. Dieses Beispiel mache ich etwas ausführlicher, alle anderen Hook-Scripte hänge ich einfach an dieses Posting dran.
#!/bin/bash REPOS="$1" TXN="$2" PHP="/usr/local/php5/bin/php" SVNLOOK="/usr/bin/svnlook" AWK="/usr/bin/awk" GREP="/bin/egrep" SED="/bin/sed" CHANGED=`$SVNLOOK changed -t "$TXN" "$REPOS" | $GREP "^[U|A]" | $AWK '{print $2}' | $GREP \\.php$` for FILE in $CHANGED do MESSAGE=`$SVNLOOK cat -t "$TXN" "$REPOS" "$FILE" | $PHP -l` if [ $? -ne 0 ] then echo 1>&2 echo "***********************************" 1>&2 echo "PHP error in: $FILE:" 1>&2 echo `echo "$MESSAGE" | $SED "s| -| $FILE|g"` 1>&2 echo "***********************************" 1>&2 exit 1 fi done
Da ich mein SVN auf einem Linux-Server betreibe, habe ich hier das entsprechende Bash-Script. Falls ihr euren SVN-Server unter Windows betreibt, muß man das Script natürlich anpassen.
Was passiert hier? Es werden mittels svnlook alle geänderten oder neu hinzugefügten Dateien gesucht, aufgelistet und dann noch die Dateien mit einer .php Endung gefiltert. Für jede Dieser Dateien wird dann wiederum via svnlook der Quelltext geholt und mittels der Pipe an „php -l“ übergeben. Im Falle eines Fehlers gibt es eine Fehlermeldung, die dann im SVN-Client ausgegeben wird. Der Commit wird also scheitern (da dies ja ein pre-commit-Hook ist).
Ein weiteres Hook-Script, welches pre-commit ausgeführt wird, ist zum Beispiel der PEAR PHP-CodeSniffer. Dieses kleine Script kann PHP-Code auf Coding-Standards überprüfen, also ob beispielsweise PHPdoc vorhanden ist, oder ob die geschweiften Klammern an den richtigen Stellen stehen. Ich persönlich habe dieses Script allerdings nicht als Hook eingebunden, da ich auch ab und zu fremden Code ins SVN packe, der natürlich nicht meinen Code-Standards entspricht. PHP-CodeSniffer führe ich lokal ab und zu aus, und dann auch nur auf meine Verzeichnisse. Beim CodeSniffer wird ein entsprechendes Hook-Script gleich mitgeliefert.
Noch ein einfaches kleines Hook-Script wäre das hier:
/* test to see if svn commit comment length is greater than or equal to 10 chars */ $log = exec("svnlook log -t ". $argv[2] ." ". $argv[1]); if(strlen($log) > 9){ exit(0); }else{ exit(1); }
---------
Weitere Hook-Scripte:
- PHP CodeSniffer: liegt dem Paket bei unter /scripts/phpcs-svn-pre-commit
- PHPUnit: dazu gibt es im Internet keine Quellen (ich habe jedenfalls keine gefunden), wie man das am besten machen kann. Ich habe es früher (vor 2 Jahren) so gelöst: Download Vorgehensweise ist einfach: Nach dem commit einmal das svn komplett auschecken, dann die beiden unit-tests starten, und im Fehlerfall Emails versenden
- Mailer-Script: subversion liegt bereits eins bei, hier eine Weiterentwicklung: http://opensource.perlig.de/svnmailer/
- Jabber Benachrichtung: http://trac.c3d2.de/subversion-hooks/browser (über einen ICQ-Gateway kann man dann auch ICQ-Nachrichten senden)
- Trac Issue Tracking/Wiki: http://trac.edgewall.org/browser/trunk/contrib/trac-post-commit-hook
- Bei Tigris liegen auch noch einige, aber auch viel unnützes Zeug: http://subversion.tigris.org/tools_contrib.html#hook_scripts
Falls ihr noch andere Hook-Scripte habt, nur her damit!
EDIT: Stefan empfiehlt noch ein tolles Hook-Script, mit dem man automatisch nach einem Commit die geänderten Dateien auf einen FTP, SFTP oder Filesystem synchronisieren kann: http://svn2web.sourceforge.net
PHP Profiling mit XDebug und KCachegrind
So, nun will ich das Thema nachholen, was ich am letzten Donnerstag hab anklingen lassen. Am Wochenende ist dann jedoch das Gewinnspiel dazwischengekommen, sodass ich nun etwas zum Thema PHP-Profiler schreibe.
Profiler? Sind das nicht die FBI-Psychiater, die anhand von Tatorten und den Opfern etwas über den Täter aussagen können, wie er lebt und denkt? Naja, vielleicht…
Profiler gibt es auch für die meisten Programmiersprachen. Profiler schauen „unter die Haube“ und untersuchen die kleinsten Einheiten und Funktionen einen PHP-Programms. Sie messen die Zeiten für die Kommandos, zählen die Anzahl der Aufrufe, merken sich welche Funktion andere Funktionen aufruft und kann daraus sehr ausführliche und interessante Tabellen und Graphen generieren. Diese nutzt man dann am häufigsten, um (zeitliche) Flaschenhälse bei der Programmierung zu finden.
Aber wie erhält man diese Zeiten? Wenn man nur einen kleinen Teil seines Programms beobachten möchte und die Zeit messen möchte, die es braucht, macht man das wahrscheinlich so:
$start = microtime(true); // Hier einige Befehle, die gemessen werden sollen $end = microtime(true); $diff = $end - $start; echo "Benötigte Zeit: " . $diff;
Das ist meistens völlig ausreichend für den Anfang, um einen kurzen Überblick zu finden. Wenn das Programm aber sehr groß ist, und man nicht tausende dieser Messungen einbauen möchte, kann man gleich das ganze Script profilen lassen.
Wir machen das ganze mal praktisch an einem kleinen Beispiel:
class User { private $username; private $newsletter; public function __construct() { $this->username = $this->getRandomString(8); $this->newsletter = rand(0, 1); } public function getUsername() { usleep(500000); return $this->username; } public function getNewsletter() { return $this->newsletter; } private function getRandomString($stringLength) { //srand ((double)microtime() * 1000000); return substr(md5(rand()), 0, $stringLength); } } for ($i = 0; $i < 5; $i++) { $user = new User(); if (rand(0, 1)) { echo $user->getUsername()."\n"; } else { echo $user->getNewsletter()."\n"; } }
Nun muß ich noch XDebug in der php.ini aktivieren. Dazu aktiviert man einfach die PHP-Extension XDebug wie folgt in der php.ini:
[XDebug]
; Only Zend OR (!) XDebug
zend_extension_ts=“C:\xampp\php\ext\php_xdebug.dll“
xdebug.remote_enable=true
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000
xdebug.remote_handler=dbgp
xdebug.profiler_enable=1
xdebug.profiler_output_dir=“C:\xampp\tmp“
Natürlich sollte man vorher die php_xdebug.dll downloaden und in den entsprechenden Ordner legen. Nachdem man die php.ini so geändert hat, wird für jedes PHP-Script ein sogenannter CacheGrind-Dump im Output-Dir abgelegt. Diese Datei kann, je nach Komplexität und Umfang des Script, auch mehrere hundert MB groß werden.
Nun führe ich das Script ganz normal aus, im Hintergrund wird dann die CacheGrind Datei erzeugt.
Die generierte cachegrind.out.2916 sieht so aus, und ist (noch) nicht wirklich brauchbar:
Öffnen und tabellarisch bzw. grafisch darstellen kann man diese Textdatei dann zum Beispiel mit dem Windows Programm WinCacheGrind:
Hier erkennt man zwar schon etwas, aber viel besser ist unter Linux KCachegrind, das sieht dann so aus:
Man schaue sich nun die Anzahl der Aufrufe sowie die Zeitwerte an, und kann unzweifelhaft feststellen, dass in unserem einfachen Beispiel die usleep-Funktion die meiste Zeit gebraucht hat. Aber auch getRandomString() ist nicht zu vernachlässigen, vielleicht könnte man da noch etwas optimieren. Bei großeren Programmen mit SQL-Abfragen und komplexeren Algorithmen und Abläufen wird das ganze natürlich noch viel interessanter, probiert es einfach mal bei euren großen Projekten aus!
Man kann sowohl nach den absoluten als auch den prozentualen Zeitwerten sortieren, sieht Callees und Caller, kann sich einen Call Graph anzeigen lassen usw, ein wirklich tolles Programm.
Ich würde auf jeden Fall für ernsthafte Profiler das KCachegrind empfehlen. Falls man gerade kein laufendes Linux-System zur Hand hat, ist es höchste Zeit, mittels VirtualBox und Ubuntu schnell eins aufzusetzen. Beides ist natürlich kostenlos, und nach einer Stunde ist es lauffähig. Man braucht auch keine umständlichen Dual-Boot-Sachen und zerschießt sich wohlmöglich sein System, VirtualBox kann „ein Betriebssystem in einem Fenster“ laufen lassen. Einfach mal ausprobieren und informieren.
Mittels Profiling kann man sehr einfach herausfinden, wie oft welche Funktion aufgerufen wird. Man erkennt ziemlich schnell, wieviele Datenbank-Queries gemacht werden und von wo diese kommen. Man sieht, wieviele Objekte erstellt werden und wo dies passiert (und wie lang das dauert).
Die gefundenen Flaschenhälse kann man dann beseitigen. Entweder durch effizientere SQL-Abfragen, bessere Algorithmen, Caching oder oder. Das ist von Fall zu Fall verschieden.
Mich würde interessieren, ob und wie ihr profiled, und was ihr dadurch für gewöhnlich an Fehlern/Flaschenhälsen findet und wie ihr sie behebt.