Od tematu magazynowego do praktyki projektowej
Pasujące strony usługowe i techniczne do artykułu
Gdy Delphi-aplikacja w eksploatacji stopniowo „puchnie“, sporadycznie kończy się błędami Access Violation lub po kilku dniach działania nagle staje się niestabilna, często nie chodzi o pojedynczy błąd, lecz o wzorzec: pamięć jest alokowana, ale niezwolniona poprawnie — albo zostaje zwolniona za wcześnie i potem wciąż używana. W takich sytuacjach FastMM FullDebugMode jest na wagę złota. Nie jako stan stały, lecz jako ukierunkowane narzędzie diagnostyczne, które z „gdzieś w stercie coś jest zepsute“ potrafi wydobyć zrozumiałą przyczynę.
Ale jest haczyk: FullDebugMode generuje dużo danych wyjściowych, obciąża wydajność i łatwo prowadzi do błędnych interpretacji. Raport o wyciekach nie pokazuje automatycznie miejsca, gdzie „jest błąd“. A stacktrace jest tak dobry, jak rozwiązywanie symboli (plik MAP, informacje debugowe, inlining). W tym artykule przejdę przez typowy przypadek brzegowy, wyjaśnię poprawne podejście i pułapki — tak, abyś na końcu nie tylko znalazł wycieki, ale usunął je trwale.
Kiedy FastMM FullDebugMode ma rzeczywiście sens
FastMM w nowoczesnych wersjach Delphi często jest już domyślnym menedżerem pamięci lub i tak jest dołączany w wielu projektach. FullDebugMode to jednak specjalna konfiguracja: oznacza bloki pamięci dodatkowymi wzorcami kontrolnymi, zbiera stacktrace’y alokacji i agresywniej sprawdza korupcję sterty (czyli uszkodzone dane zarządzające w stercie, np. przez przekroczenia bufora).
Stosuję FullDebugMode celowo, gdy występuje jeden z następujących scenariuszy:
- Powtarzalny Leak: zużycie pamięci rośnie w teście na operację (np. na request, na import, na akcję UI).
- Sporadyczne AV-y: szczególnie te, które „raz tu, raz tam“ występują w tym samym obszarze (klasyczne: use-after-free).
- Korupcja sterty: komunikaty takie jak „Invalid pointer operation“, „Access violation in ntdll“ lub awarie przy zamykaniu/finalizacji.
- Wyszukiwanie regresji: po refaktoryzacji, aktualizacji biblioteki lub zmianie kompilatora nagle pojawia się nowa niestabilność.
FullDebugMode nie ma sensu jako „włączmy to we wszystkich buildach“. Narzut jest wysoki, timing się zmienia, i akurat race condition mogą przez to zniknąć lub się przesunąć. Do pracy ciągłej lepszy jest raczej lekki monitoring (np. Working-Set procesu, Private Bytes, liczniki na operację) — FullDebugMode to skalpel, nie czujnik tętna.
Podstawowa zasada: raport wycieków to objaw, stacktrace to ślad
Raport o wyciekach pokazuje najpierw: Te bloki są nadal zaalokowane przy zakończeniu programu. To jest problem tylko wtedy, gdy te bloki powinny były zostać zwolnione. Istnieją uzasadnione „wycieki“: globalne singletons, cache’e, uchwyty systemowe o czasie życia procesu lub biblioteki firm trzecich, które celowo nie finalizują. Te przypadki chcesz znać, ale nie naprawiać ich na ślepo.
Stacktrace w raporcie pokazuje miejsce, w którym blok został zażądany. To często nie jest miejsce, w którym „zapomniałeś wywołać Free“. Częste scenariusze w rozrośniętych systemach:
- Alokacja w warstwie UI lub serwisowej, zwolnienie powinno nastąpić w niższej warstwie (ownership niejasny).
- Alokacja w fabryce, ownership przekazywany jest do wywołującego — ale wywołujący myśli, że to on jest właścicielem.
- Obiekty są przechowywane w kolekcjach (listy, słowniki), ale model ownership nie jest spójny.
- Ścieżka wyjątków pomija sprzątanie, bo brakuje try/finally lub zaczyna się za późno.
Czysty przebieg jest zatem: odtworzyć → izolować → rozwiązać stacktrace → znaleźć błąd własności (ownership) → poprawka z testem regresji. FastMM dostarcza ślady, ale musisz je przełożyć na architekturę i cykle życia.
Poprawne włączenie FastMM FullDebugMode (bez przeoczenia skutków ubocznych)
FullDebugMode jest w praktyce aktywowany przez opcje FastMM i odpowiednią konfigurację FastMM. Decydujące jest mniej „jak dokładnie nazywa się plik include”, a bardziej co konfiguracja powoduje i w jakich warunkach kompilacji ją wykorzystujesz.
Zalecane warunki dla kompilacji debugowej
- Debug DCUs i informacje debugowe: stacktrace’y są użyteczne tylko wtedy, gdy da się je rozwiązać do rzeczywistej jednostki/wiersza/adresu. Upewnij się, że generowane są informacje debugowe i dostępny jest plik MAP.
- Świadomy wybór optymalizacji: Dla czytelności stacktrace’ów zazwyczaj lepszy jest build bez optymalizacji. Inlining i agresywne optymalizacje mogą „zamazywać” ramki stosu.
- Te same warunki wykonania: Używaj możliwie tych samych danych, tej samej konfiguracji, tych samych uprawnień. Wiele wycieków zależy od danych (np. rzadkie formaty, ścieżki wyjątkowe).
- Rozróżnij 64-bit i 32-bit: Zachowanie pamięci, wyrównanie i biblioteki zewnętrzne różnią się. Debuguj na docelowej platformie, na której występuje problem.
Jedna kwestia, którą administratorzy i techniczni liderzy często bagatelizują: FullDebugMode może też zmieniać timing. Jeśli stosujesz wielowątkowość, warunki wyścigu mogą występować inaczej. Dlatego sensowne jest równoległe uruchomienie bez FullDebugMode, które jedynie potwierdza reprodukcję. FullDebugMode to wtedy krok do diagnostyki.
Ostrożnie z „ReportMemoryLeaksOnShutdown”
Delphi może przez ReportMemoryLeaksOnShutdown zgłaszać wycieki przy zamykaniu programu. To praktyczne, ale w złożonych aplikacjach (usługi, host wtyczek, długi czas działania) może mylić: podczas shutdownu wykonywane są sekcje finalizacji, wątki zatrzymują się, cache’y są czyszczone. Wyciek, który w połowie czasu działania jest krytyczny, może do końca zniknąć — lub odwrotnie: pozorny wyciek pojawia się dopiero przy zamykaniu, bo nadal działa praca w tle.
Dla praktycznego poszukiwania wycieków ważniejsze jest zatem: mierzyć wyciek na operację (np. po 100 żądaniach), a nie tylko przy zamknięciu. FastMM może w tym pomóc, ale scenariusz testowy musi to odzwierciedlać.
Typowy przypadek brzegowy: raport wycieków wskazuje „jakikolwiek obiekt”, ale przyczyną jest Ownership
Klasyk w aplikacjach korporacyjnych: proces importu tworzy dla każdego rekordu obiekty pomocnicze (np. StringLists, parsery JSON, listy tymczasowe). W „happy path” są one poprawnie zwalniane. W rzadkich przypadkach (przeskok z powodu walidacji, wyjątek, wczesne zakończenie) jakiś obiekt zostaje. Po 10 000 rekordach staje się to widoczne.
FastMM FullDebugMode pomaga tutaj, ponieważ pokazuje miejsce alokacji. Ale „Fix” to nie „free w miejscu alokacji”. Naprawa to solidny wzorzec własności (ownership):
- Ten, kto tworzy obiekt, nie jest automatycznie jego właścicielem (owner).
- Własność (ownership) musi być jasna w kontrakcie API (parametry/zwrot, dokumentacja, konwencje nazewnictwa).
- Kolekcje muszą być jednoznaczne: owning vs. non-owning. Mieszane formy się mszczą.
- Ścieżki wyjątków wymagają wczesnych bloków try/finally.
Jeśli ze stosu wywołań widzisz tylko „TStringList.Create”, informacja nie jest bezwartościowa – ale mówi ci tylko: tutaj coś powstaje. Pytanie brzmi: gdzie to powinno się zakończyć? I tutaj myślenie architektoniczne pomaga bardziej niż akrobatyka debuggera.
Poprawne czytanie stosów wywołań: co naprawdę z nich wynika
Stos wywołań z FastMM to zwykle lista adresów powrotu, które – przy symbolach debugowania – mapują się na Units, procedury i najlepiej numery linii. Czytając to, trzy rzeczy są kluczowe:
- Top-of-Stack nie zawsze oznacza źródło błędu: najwyższe ramki to często Memory-Manager/RTL. Interesujące staje się miejsce, w którym zaczyna się twój kod.
- Łańcuch wywołań zamiast pojedynczej linii: linia to tylko punkt. Łańcuch pokazuje, która ścieżka doprowadziła do alokacji.
- Wiele identycznych bloków: jeśli FastMM zgłasza kilka leaków o takim samym rozmiarze, często oznacza to powtarzającą się ścieżkę. To dobrze: masz reprodukowalność.
Gdy brakuje numerów linii: plik MAP, Packages, Release-DCUs
Wiele zespołów się tu potyka: FullDebugMode jest włączony, raport o leakach przychodzi, ale zamiast Unit/linie są tylko adresy lub kryptyczne symbole. Typowe przyczyny:
- Nie wygenerowano pliku MAP ani informacji debugowych.
- Aplikacja korzysta z Release-DCUs lub zewnętrznych DLL bez symboli.
- Aplikacja używa Runtime Packages: wtedy części kodu znajdują się w BPLs i rozwiązywanie symboli musi być do tego dopasowane.
- Optymalizacja/inlining uczyniły stos wywołań mniej czytelnym.
W praktyce to oznacza: do polowania na leaki potrzebujesz buildu z włączonymi opcjami diagnostycznymi. To inne cele niż „jak najszybciej”. Liderzy techniczni powinni traktować to jako osobny profil builda, żeby nie każdy członek zespołu zmieniał ad hoc opcje projektu.
Ocenianie ramek: „Interessant“ ist oft eine Zeile weiter oben
Przykład z praktyki (bez konkretnego kodu klienta): Stacktrace pokazuje w pierwszym frame w twoim kodzie rutynę „LoadConfig”. Widzisz tam tworzenie obiektu. Dodajesz Free, Leak znika – i nagle gdzie indziej wybucha Double Free. Dlaczego? Bo „LoadConfig” wstawia obiekt do Cache, a inna ścieżka kodu jest już Owner i sprząta to później.
Prawidłowa lektura byłaby taka: Stacktrace pokazuje ci, gdzie powstaje blok. Fix często leży w definicji: Kto posiada obiekt po Return? Jeśli nie odpowiesz na to pytanie precyzyjnie, tylko zmienisz obraz błędu (Leak → AV).
Uszkodzenie sterty vs. Leak: Dlaczego FullDebugMode często znajduje prawdziwego winowajcę
Wiele „Leaks” w rzeczywistości to problemy wtórne: Buffer-Overrun nadpisuje Heap-Metadaten, Memory-Manager później nie może poprawnie zwolnić pamięci, i w efekcie widzisz pozornie losowe Leaks lub Invalid Pointer Operations. FullDebugMode jest tu skuteczny, ponieważ działa na wzorcach kontroli i przy Free/Reuse wykonuje dodatkowe walidacje.
Ważne jest rozróżnienie:
- Leak: Blok został zaalokowany i nigdy nie został zwolniony. Stabilność pogarsza się w czasie, awaria nie musi wystąpić.
- Use-after-free: Blok jest zwalniany, ale później nadal używany. Prowadzi do sporadycznych AV, które są trudne do odtworzenia.
- Double Free: Blok jest zwalniany dwukrotnie. Może wywołać awarię natychmiast lub dopiero później (gdy blok zostanie ponownie użyty).
- Heap-Korruption: Ktoś zapisuje poza granicami bloku. Objawy są często opóźnione.
FullDebugMode jest szczególnie wartościowy, gdy widzisz objawy z opóźnieniem. Dodatkowa walidacja ujawnia błędy wcześniej – często dokładnie w miejscu, gdzie występuje nieprawidłowy dostęp, a nie dopiero minuty później w dowolnym Free.
Postępowanie w projektach: Powtarzalne polowanie na Leaki zamiast „debugowania we mgle”
Jeśli chcesz polować na wycieki pamięci, potrzebujesz procedury, która jest powtarzalna i którą można udostępniać w zespołach. Preferuję pracę z ustalonym ramem diagnostycznym:
1) Reprodukcja w deterministycznym scenariuszu
Ustal sekwencję testową, która niezawodnie ujawnia Leak: „Uruchom Service, przetwórz 500 wiadomości, zatrzymaj Service” lub „Otwórz okno X, wykonaj akcję Y 200 razy”. Ważne jest, żebyś udokumentował sekwencję wraz z parametrami (zestaw danych, najemca, Feature-Flags), tak aby inni mogli ją odtworzyć.
2) Minimalizacja: ujawnienie Leaka krok po kroku
Jeśli sekwencja trwa 20 minut, podziel ją. Celem jest to, abyś mógł jak najszybciej porównać „przed” i „po”. W dużych aplikacjach to często jest główny pożeracz czasu, a nie samo naprawianie.
3) Włącz FullDebugMode i interpretuj raport
Jetzt erst kommt FastMM FullDebugMode ins Spiel. Sammle die Reports, gruppiere nach Blockgröße/Callstack und schaue nach Wiederholungen. Ein einzelner verbleibender Block kann ein legitimer Cache sein. 10.000 identische Blöcke sind fast immer ein echter Leak.
4) Ownership-Klärung und Fix in der passenden Schicht
Fixe Leaks dort, wo Ownership definiert wird: Factory, API-Vertrag, Collection-Wrapper. „Schnell ein Free einbauen“ direkt neben Create ist oft der falsche Ort, wenn das Objekt weitergereicht wird.
5) Regression: gleiche Sequenz, gleicher Build, gleicher Report
Der Fix ist erst dann gut, wenn die Sequenz wieder läuft und weder Leaks noch neue Speicherfehler auftreten. Gerade bei Use-after-free ist ein „Leak weg“ kein Beweis, sondern nur ein neues Symptom.
Typische Fallstricke in Delphi-Code, die FastMM sichtbar macht
Collections und Ownership (Listen, Dictionaries, Interfaces)
Viele Leaks kommen nicht aus komplizierten Algorithmen, sondern aus alltäglichen Datenstrukturen. Zwei klassische Fehlerbilder:
- Eine Liste enthält Objekte, aber niemand weiß, wer sie freigibt. Lösung: owning Liste verwenden oder konsequent im finally räumen.
- Ein Dictionary hält Objekte als Values; beim Remove wird der Value nicht freigegeben oder beim Clear vergessen.
Zusätzlich tricky sind Interfaces: Referenzzählung (ARC-ähnlich) ist komfortabel, aber Mischbetrieb mit Objekt-Ownership kann bei zyklischen Referenzen oder Events Leaks erzeugen. FullDebugMode zeigt dir dann oft den Allokationspfad, aber die Ursache ist ein Referenzzyklus (A hält B über Interface, B hält A über Callback).
Exceptions und frühe Exits
In gewachsenen Business-Software-Systemen sind Exceptions oft Teil der normalen Steuerung (z. B. Validierung, Abbruch, Retry). Das Problem ist selten die Exception selbst, sondern der Pfad drumherum: Ein Objekt wird vor dem try/finally erzeugt, dann fliegt eine Exception, und der Cleanup wird übersprungen. FullDebugMode liefert dir den Stacktrace der Allokation – und du musst prüfen, ob es einen garantiert ausgeführten Freigabepfad gibt.
Threads und Lebenszeit: „Freigeben im falschen Thread“
Bei VCL/FMX und Services mit Worker-Threads entsteht ein weiterer Randfall: Ein Objekt wird in einem Thread erzeugt, aber im UI-Thread freigegeben (oder umgekehrt), weil man per Queue/Synchronize „nur schnell“ etwas rüberreicht. Das kann funktionieren, kann aber auch zu Use-after-free führen, wenn der Producer weiterarbeitet, während der Consumer bereits freigibt.
FastMM FullDebugMode kann hier helfen, weil er zeitversetzte Fehler früher detektiert. Der eigentliche Fix ist jedoch ein sauberes Lebenszeitmodell: klare Besitzverhältnisse, Übergabe nur über immutable Daten oder eindeutige Ownership-Transfer-Punkte.
Wie du Reports nutzbar machst: Filtern, vergleichen, dokumentieren
In Teams lohnt es sich, Leak-Reports nicht nur „anzuschauen“, sondern wie ein Artefakt zu behandeln. Drei pragmatische Maßnahmen, die sich bewährt haben:
- Baseline-Report: Ein „bekannter Zustand“ (z. B. aktuelle Produktversion) wird einmal mit FullDebugMode durchlaufen und als Referenz abgelegt. Dann erkennst du neue Leaks sofort.
- Vergleich nach Use-Case: Für kritische Workflows (Import, Export, API-Request, UI-Massenoperation) legst du jeweils eine kurze Sequenz fest, die regelmäßig wiederholbar ist.
- Dokumentierte „legitime Leaks“: Wenn ein Cache bewusst nicht finalisiert, dokumentiere das. Sonst jagt in sechs Monaten jemand wieder die gleichen Einträge.
To nie biurokracja, lecz oszczędność czasu: polowanie na wycieki szybko staje się błędnym kołem, ponieważ te same wzorce pojawiają się ponownie w każdym sprincie.
Kiedy wysiłek się opłaca – a kiedy warto postąpić inaczej
FastMM FullDebugMode to narzędzie diagnostyczne z kosztami. Wysiłek opłaca się szczególnie, gdy:
- Aplikacja działa przez długi czas (usługa, klient terminalowy, system zmianowy, procesy 24/7).
- Przetwarzasz rzeczywiste strumienie danych klientów i nie pokrywasz wszystkich ścieżek w testach.
- Stabilność jest ważniejsza niż krótkoterminowa szybkość dostarczania funkcji (typowe dla rozwiązań bliskich procesom).
Jeśli natomiast masz tylko mały pomocnik desktopowy, który kończy działanie po 30 sekundach, polowanie na wycieki często schodzi na drugi plan. Podobnie: jeśli masz jednorazowy problem z nagłym skokiem pamięci (np. duży eksport), często nie chodzi o wyciek, lecz o strategię strumieniowania i szczytowe obciążenie sterty.
Wniosek praktyczny: FullDebugMode to nie przełącznik, lecz proces
FastMM FullDebugMode wprowadza porządek w poszukiwaniu błędów pamięci: ujawnia alokacje, wykrywa uszkodzenia sterty wcześniej i dostarcza stacktrace’y, dzięki którym możesz naprawić przyczynę zamiast objawu. Kluczową dźwignią nie jest jednak narzędzie, lecz przebieg działań: odtwarzalne scenariusze, buildy umożliwiające diagnostykę, jasne zasady odpowiedzialności za obiekty (ownership) i regresja względem bazy odniesienia.
Jeśli zmagasz się z uporczywym wyciekiem lub sporadycznym błędem sterty i chcesz trwale ustabilizować ten temat w większym systemie Delphi, warto przeprowadzić krótki, schludny setup diagnostyczny z klarowną sekwencją i raportami nadającymi się do analizy. Jeśli potrzebujesz wsparcia przy analizie, profilach buildów lub refaktoryzacji architektury: kontakt do Net-Base Software GmbH.
W tym temacie ważne są także Delphi Znajdowanie wycieków pamięci i Czytanie raportu Fastmm Leak. Artykuł porządkuje te aspekty w zrozumiały sposób i pokazuje, na co zwracać uwagę w codziennej pracy.
Następny krok
Jeżeli temat stanie się rzeczywistym projektem, architektura, stan istniejący i eksploatacja powinny być rozpatrywane razem na wczesnym etapie.
Wspieramy nie tylko w pojedynczych zagadnieniach, lecz także wtedy, gdy z fragmentów kodu źródłowego, kwestii związanych z systemami legacy lub koncepcji portalu ma powstać solidny projekt dla przedsiębiorstwa.
- Stan istniejący, obraz docelowy i ryzyka techniczne są oceniane łącznie.
- REST, dostęp do danych, portale i Rollout nie będą przesuwane na później.
- Wcześnie widzą Państwo, która droga jest ekonomicznie i operacyjnie wykonalna.