für Python-Dienste
War es dein Code, oder war es Warten?
Jeder andere Agent kann dir sagen, dass eine Funktion langsamer geworden ist. Dieser sagt dir, welche Hälfte — der Code, der gerechnet hat, oder die Zeit, die er auf ein Lock, einen Pool-Platz oder eine C-Erweiterung gewartet hat, die das GIL hielt. Die brauchen entgegengesetzte Lösungen, und jede andere Zahl pro Aufruf bleibt korrekt, während das Zweite passiert — weshalb nichts sonst es fängt.
Was es misst
- CPU pro Aufruf
- time.thread_time() ist pro Thread und kostet etwa 100 ns zum Lesen, und ein synchroner Span besitzt seinen Thread für seine gesamte Dauer — das ist also eine exakte Zahl und kein Anteil einer prozessweiten Uhr, aufgeteilt zwischen allem anderen, was gerade lief.
- Warten pro Aufruf
- Der Rest der Eigenzeit einer Funktion: ein Lock, das über mehr Arbeit gehalten wird als früher, ein Connection Pool ohne freien Platz, eine C-Erweiterung, die das GIL genommen und nicht zurückgegeben hat. Für jede andere Messung hier ist es unsichtbar, weil der Code nicht langsamer ist — er steht in der Schlange.
- Jede psycopg-Abfrage
- Version 2 und 3, am Cursor gepatcht statt an der Verbindung, sodass SQLAlchemy auf psycopg ebenfalls gemessen wird. Zurückgegebene Zeilen, Spalten, Antwortgröße und das normalisierte Statement.
- Welche Funktion welche Abfrage abgesetzt hat
- Eine Abfrage wird der Funktion über ihr gutgeschrieben, ein N+1 landet also auf der Funktion mit der Schleife darin, statt sich über eine Abfrage zu verteilen, die für sich genommen immer in Ordnung aussah.
- Deine HTTP-Routen, so wie sie geschrieben sind
- Flask über die gematchte url_rule, Django über die Route, wie sie in urls.py steht, und alles ASGI oder WSGI über das, was du umschließt. Der Verlauf sagt also GET /users/<int:user_id>/orders statt einer Operation pro Nutzer-ID.
Was es findet
Jedes davon wird gegen das Release davor verglichen, der Befund benennt also die Änderung und nicht den Tag.
Dein eigener Code wird langsamer, in zwei Teile zerlegt
Der Befund, der sagt, zu welcher Lösung man greifen muss. Die Rechenzeit ist gestiegen und die Wartezeit nicht: schau in die Funktion. Die Wartezeit ist gestiegen und die CPU hat sich nicht bewegt: schau auf den Pool, das Lock, oder womit der Prozess jetzt einen Thread teilt.
Ein N+1, das im letzten Release nicht da war
Eine Funktion, die eine Abfrage pro Anfrage machte, macht jetzt zwanzig. Jede ist schnell und korrekt indiziert, keine Zeitmessung bewegt sich also genug, um aufzufallen — nur die Anzahl.
Eine Abfrage, die angefangen hat, die ganze Tabelle zu lesen
Zeilen pro Aufruf sind von 30 auf 30.000 gegangen. Auf einer warmen Datenbank mit entwicklungsgroßen Daten bewegt das die Uhr kaum, und es legt die Produktion lahm, wenn ein Jahr an Zeilen dahintersteht.
Etwas, das jetzt in einer Schleife aufgerufen wird
Pro Aufruf unverändert — gleiche Geschwindigkeit, gleiche Daten — und hunderte Male aufgerufen, wo es früher einmal aufgerufen wurde.
Eine Abfrage, die still nichts zurückgibt
Erfolgreich, nichts geworfen, nicht langsam, und leer zurückgekommen, wo früher Zeilen kamen. Die meisten Arten, wie ein solcher Dienst kaputtgeht, machen die Zahlen kleiner statt größer.
Fehler, gruppiert danach, wo sie herkamen
Nach Code-Ort statt nach Meldung, ein Bug ist also ein Eintrag, in wie viele verschiedene Zeichenketten er sich auch formatiert.
Was in deinem Verlauf landet
Kein Dashboard zum Lesen. Eine Karte pro Befund, mit den Zahlen, dem Release, das sie verändert hat, und den Belegen, die dein Coding-Agent braucht, um die Lösung zu schreiben.
Das sind die zwei Regressionen, die apps/demo-python absichtlich ausliefert, gegen den Datensatz, den sein Seed-Skript anlegt — etwa 1.000 Nutzer und 30.000 Bestellungen. Du kannst es laufen lassen und ihnen beim Ankommen zusehen.
orders.get_user_ordersEin Refactoring hat die WHERE-Klausel fallen lassen, jeder Aufruf holt also die ganze Tabelle und filtert sie in Python. Die Uhr hat sich kaum bewegt; die Zeilenzahl ist das ganze Signal.
seit Release v2, gegen v1
orders.enrich_ordersAus einer gebündelten Abfrage wurde eine pro Bestellung. Jede Abfrage ist für sich schnell und korrekt indiziert, und genau das lässt so etwas Review und Staging überleben.
seit Release v2, gegen v1
Wie es installiert wird
Gib es dem Coding-Agenten, den du ohnehin offen hast. Er findet heraus, ob dieses Projekt Flask, Django, FastAPI oder ein nacktes WSGI-Callable ist, setzt die Middleware an die richtige Stelle und markiert die Service-Schicht — und behebt dann in derselben Unterhaltung, was gefunden wird.
claude mcp add sixty \
-e SIXTY_API_KEY=sixty_sk_… \
-e SIXTY_ENDPOINT=https://ingest.sixty.sh \
-- npx -y @sixty-sh/mcp
# or by hand:
pip install sixty-sh
# wsgi.py, asgi.py or main.py — before any connection is opened
import sixty
sixty.init()
# then one of these, whichever this project is:
# Flask instrument_flask(app)
# Django SixtyMiddleware, first in MIDDLEWARE
# ASGI SixtyASGIMiddleware
# WSGI SixtyMiddleware around the callable
# and the code between the view and the database
@sixty.trace
def get_user_orders(user_id):
...Der Release-Bezeichner wird von allein von Vercel, Render, Railway, Fly, Heroku oder GitHub Actions abgeholt, und SIXTY_RELEASE setzt ihn überall sonst. Der erste Vergleich kommt nach deinem nächsten Deployment, weil es nichts gibt, womit man ein Release vergleichen könnte.
Was es nicht tut
- Ein Span, der über ein await hinweg besteht, teilt seinen Thread mit allem anderen, was die Loop ausgeführt hat, er meldet also keine CPU statt einer aufgeblähten. Ein asyncio-Dienst bekommt CPU pro synchroner Funktion und pro Abfrage, aber nicht pro Anfrage — die ehrliche Antwort statt einer plausiblen.
- Abfragepläne werden nicht erfasst. Postgres ist da und das EXPLAIN würde funktionieren; dieser Agent setzt noch keines ab, „es benutzt seinen Index nicht mehr“ kommt also auf Node und Ruby an und hier nicht.
- psycopg ist der einzige instrumentierte Treiber. SQLAlchemy auf psycopg wird gemessen, weil der Cursor darunter es wird — asyncpg und die MySQL-Treiber werden überhaupt nicht gemessen.
- Unter gunicorn oder uwsgi meldet jeder Worker eigenständig und der Collector führt sie zusammen. Das ist korrekt, und man sollte es wissen, wenn man eine Zahl liest, die einen Prozess beschreibt.
- Hier markiert nichts deine Funktionen für dich. Node bekommt das aus einer Build-Transformation und Python hat keinen entsprechenden Hook, der Dekorator oder instrument_module ist also ein Schritt, den du gehst — und ihn auszulassen lässt den Verlauf mit Routen und Abfragen und nichts dazwischen zurück.
Falls du wegen einer dieser Sachen hier bist
Finde heraus, was deine letzte Änderung angerichtet hat.
Probier es aus!Du baust etwas anderes? Sieh dir Node-Dienste, Go-Dienste oder Ruby- und Rails-Apps an — diese Seiten sind anders, und was wir sehen können, auch.