Від теми журналу до практики проєкту
Відповідні сторінки послуг і технічні сторінки до публікації
Якщо Delphi-додаток у роботі поступово «роздувається», періодично завершується помилками Access Violations або після кількох днів роботи раптово стає нестабільним, за цим часто стоїть не одиничний баг, а шаблон: пам’ять виділяється, але не звільняється коректно — або звільняється занадто рано й потім ще використовується. Саме тут FastMM FullDebugMode виявляється неоціненним. Не як постійний режим, а як цілеспрямований діагностичний інструмент, який перетворює «десь у heap щось зламалося» на відтворювану причину.
Підступ у тому, що FullDebugMode генерує великий обсяг виводу, зменшує продуктивність і легко призводить до неправильних інтерпретацій. Звіт про витоки не обов’язково показує місце, де фактично «помилка» трапилася. А стек-трейс корисний лише настільки, наскільки коректно розв’язані символи (MAP-Datei, відладкова інформація, inlining). У цій статті я проходжу типовий прикордонний випадок, пояснюю чистий підхід і підводні камені — щоб наприкінці ти знаходив витоки й усував їх стійко.
Wann FastMM FullDebugMode wirklich sinnvoll ist
FastMM у сучасних версіях Delphi часто вже є дефолтним менеджером пам’яті або підключається у багатьох проєктах. FullDebugMode — це особлива конфігурація: він маркує блоки пам’яті додатковими контрольними патернами, збирає стек-трейси алокацій і агресивніше перевіряє на Heap-Korruption (тобто пошкоджені службові дані в heap, напр., через Buffer-Overruns).
Я застосовую FullDebugMode цілеспрямовано, коли спостерігається одна з цих ситуацій:
- Відтворюваний витік пам’яті: споживання пам’яті зростає під час тестового запуску на одну операцію (наприклад на кожен Request, під час імпорту, під час UI-дії).
- Спорадичні AVs: особливо ті, що «іноді тут, іноді там» у тій самій ділянці (класичний випадок: Use-after-free).
- Heap-Korruption: повідомлення на кшталт «Invalid pointer operation», «Access violation in ntdll» або аварії при завершенні/фіналізації.
- Пошук регресій: після рефакторингу, оновлення бібліотеки або зміни компілятора раптово з’явилася нова нестабільність.
FullDebugMode недоцільно включати просто «у всіх білдах». Накладні витрати великі, таймінги змінюються, і саме через це race-condition можуть зникнути або зміститися. Для постійного моніторингу більше підходить легкий набір метрик (наприклад, Prozess-Working-Set, Private Bytes, лічильники на операцію) — FullDebugMode це скальпель, а не пульсометр.
Grundprinzip: Leak-Report ist Symptom, Stacktrace ist Spur
Звіт про витоки спочатку показує: ці блоки на кінець роботи програми все ще алоковані. Це автоматично є проблемою лише тоді, коли ці блоки мали бути звільнені. Є легітимні «втрати»: глобальні Singletons, Caches, OS-Handles з життєвим циклом процесу або сторонні (third-party) бібліотеки, які навмисно не роблять фіналізації. Ці випадки потрібно знати, але не «ліпити патчі» навмання.
Стек-трейс у звіті показує місце, де блок був виділений. Часто це не те місце, де ти «забув викликати Free». Типова реальність у дорослій (історично розрослій) системі:
- Алокування в UI- або сервісному шарі, звільнення має відбуватися в нижчому шарі (модель Ownership неясна).
- Алокування у фабриці (Factory), Ownership передається виклику (Caller) — але Caller вважає, що воно «owned».
- Об’єкти зберігаються в колекціях (Lists, Dictionaries), але модель Ownership неконсистентна.
- Шлях винятку (Exception-Pfad) пропускає Cleanup, бо відсутній try/finally або він розташований занадто пізно.
Тому коректний порядок дій такий: відтворити → ізолювати → розібрати Stacktrace → знайти помилку Ownership → виправлення з регресійним тестом. FastMM надає сліди, але ти маєш інтерпретувати їх у контексті архітектури й життєвих циклів.
FastMM FullDebugMode sauber aktivieren (ohne Nebenwirkungen zu übersehen)
FullDebugMode у практиці активується через опції FastMM і відповідну конфігурацію FastMM. Важливіше не стільки «як саме називається файл include», скільки що робить ця конфігурація і при яких умовах збірки ти її використовуєш.
Empfohlene Rahmenbedingungen für den Debug-Build
- Debug DCUs та Debug-інформація: стек-трейси корисні лише якщо їх можна розв’язати до реальної одиниці/рядка/адреси. Переконайся, що генеруються відомості для відладки і що доступний MAP-файл.
- Свідомо обирай оптимізації: для читабельності стек-трейсів зазвичай краще використовувати неоптимізовану збірку. Вбудовування (inlining) та агресивні оптимізації можуть «розмити» стек-фрейми.
- Рівні умови виконання: використовуй по можливості ті самі дані, ту саму конфігурацію, ті самі права. Багато витоків залежать від даних (наприклад рідкісні формати, особливі шляхи).
- Розділяй 64-bit і 32-bit: поведінка пам’яті, вирівнювання та сторонні бібліотеки відрізняються. Налагоджуй на цільовій платформі, де виникає проблема.
Один момент, який адміністратори та технічні лідери часто недооцінюють: FullDebugMode може також таймінг змінювати. Якщо задіяне багатопоточність, це може по-іншому впливати на прояви умов гонки. Тому має сенс паралельно мати запуск без FullDebugMode, який лише підтверджує відтворення. FullDebugMode тоді є кроком для діагностики.
Vorsicht bei „ReportMemoryLeaksOnShutdown“
Delphi може через ReportMemoryLeaksOnShutdown повідомляти про витоки при завершенні програми. Це зручно, але в складних застосунках (Services, Plug-in Host, lange Laufzeiten) може вводити в оману: під час завершення виконуються секції фіналізації, зупиняються потоки, звільняються кеші. Витік, який критичний у середині виконання, може зникнути до кінця — або навпаки: видимий витік виникає лише під час завершення, бо триває фонова робота.
Тому для практичної перевірки витоків важливіше: вимірювати витік на операцію (наприклад після 100 Requests), а не лише при завершенні. FastMM може в цьому допомогти, але тестова установка має це відтворювати.
Der typische Randfall: Leak-Report zeigt „irgendein Objekt“, aber die Ursache ist Ownership
Класика корпоративних застосунків: процес імпорту створює для кожного запису допоміжні об’єкти (наприклад, StringLists, JSON-Parser, тимчасові списки). В «щасливому» шляху вони коректно звільняються. У рідкісних випадках (пропуск через валідацію, Exception, ранній Exit) об’єкт залишається. Після 10 000 записів це стає помітним.
FastMM FullDebugMode тут допомагає, бо показує місце алокації. Але «фікс» не в тому, щоб просто free поставити в тому місці. Фікс — це надійний патерн власності (ownership):
- Той, хто створює об’єкт, автоматично не є його власником.
- Власність має бути чітко визначена в контракті API (параметри/повернення, документація, угоди щодо найменувань).
- Колекції мають бути однозначними: володіючі (owning) та неволодіючі (non-owning). Змішані форми караються.
- Шляхи з винятками потребують ранніх try/finally-блоків.
Якщо зі стек-трейсу ти бачиш лише „TStringList.Create“, інформація не марна — але вона каже тільки: тут щось створюється. Питання в іншому: де це має закінчитися? І тут архітектурне мислення допомагає більше, ніж акробатика відладника.
Стек-трейси правильно читати: що ти справді з них можеш вивести
Стек-трейс від FastMM зазвичай — список адрес повернення, які — за наявності debug-символів — відображаються на Units, процедури і, у ідеалі, номери рядків. Коли ти це читаєш, вирішальними є три речі:
- Вершина стеку не завжди відповідає за помилку: верхні фрейми часто належать менеджеру пам’яті/RTL. Цікаво стає там, де починається твій код.
- Ланцюжок викликів замість одного рядка: рядок — це лише точка. Ланцюжок показує шлях, який привів до алокації.
- Кілька ідентичних блоків: якщо FastMM повідомляє про кілька витоків однакового розміру, це часто означає повторюваний шлях. Це добре: у тебе є відтворюваність.
Якщо відсутні номери рядків: MAP-Datei, Packages, Release-DCUs
Багато команд спотикаються в цій точці: FullDebugMode увімкнено, звіт про витоки приходить, але замість Unit/Zeile є лише адреси або криптичні символи. Типові причини:
- Відсутня MAP-Datei або не були згенеровані відладочні дані.
- Ти працюєш з Release-DCUs або сторонніми DLL без символів.
- Додаток використовує Runtime Packages: тоді частини коду лежать в BPLs, і розв’язування символів має цьому відповідати.
- Оптимізація/Inlining зробили стек-трейс менш читабельним.
На практиці це означає: для полювання на витоки потрібен білд, який свідомо «пристосований до діагностики». Це інша мета, ніж «максимально швидко». Технічні Leads повинні розглядати це як окремий Build-Profil, щоб не кожен член команди ad hoc міняв опції проєкту.
Оцінка фреймів: „Цікаво“ часто — рядок вище
Приклад із реального життя (без конкретного коду клієнта): Stacktrace показує тобі першим фреймом у твоєму коді рутину „LoadConfig“. Ти бачиш там створення об’єкта. Ти додаєш виклик Free, витік зникає — і раптом у іншому місці відбувається падіння з Double Free. Чому? Тому що „LoadConfig“ кладе об’єкт у Cache, а інший шлях виконання вже є Owner і пізніше прибирає його.
Правильне прочитання було б таким: Stacktrace показує тобі, де виникає блок. Фікс часто лежить у визначенні: Хто володіє об’єктом після Return? Якщо ти не відповіси на це питання чітко, ти лише змінюєш картину помилки (витік → AV).
Пошкодження купи vs. витік пам’яті: чому FullDebugMode часто знаходить справжнього винуватця
Багато «витоків» насправді є наслідковими проблемами: переповнення буфера перезаписує метадані купи, менеджер пам’яті потім не може коректно звільнити пам’ять, і в підсумку ти бачиш нібито випадкові витоки або операції з недійсними вказівниками. FullDebugMode корисний тут, бо працює з контрольними зразками й при Free/Reuse виконує додаткові валідації.
Важливо зробити розрізнення:
- Витік пам’яті: блок було алоковано і ніколи не звільнено. Стабільність страждає з часом, крах не є обов’язковим.
- Використання після звільнення: блок звільнений, але пізніше ще використовується. Призводить до спорадичних AVs, які важко відтворити.
- Подвійне звільнення: блок звільняється двічі. Може впасти одразу або пізніше (коли блок було повторно використано).
- Пошкодження купи: хтось записує за межі блоку. Симптоми часто з’являються з затримкою.
FullDebugMode особливо цінний, коли ти бачиш симптоми з відтермінуванням. Додаткова валідація робить помилки видимими раніше — часто саме в місці, де відбувається некоректний доступ, а не хвилинами пізніше в будь-якому виклику Free.
Підхід у проєктах: відтворюване полювання на витоки замість «відлагодження в тумані»
Якщо ти хочеш полювати на витоки пам’яті, тобі потрібен процес, який можна відтворити і передати в командах. Я люблю працювати з фіксованою діагностичною рамкою:
1) Відтворення в детермінованому сценарії
Визнач тестову послідовність, яка надійно демонструє витік: „Стартуй Service, оброби 500 повідомлень, зупини Service“ або „Відкрий форму X, виконай дію Y 200 разів“. Важливо документувати послідовність з параметрами (заповнення даних, тенант, Feature-Flags), щоб інші могли її повторити.
2) Мінімізувати: зробити витік видимим по кроках
Якщо послідовність займає 20 хвилин, розбий її. Мета: ти хочеш якомога швидше порівнювати «до» і «після». У великих застосунках це часто і є основним витратником часу, а не саме виправлення.
3) FullDebugMode einschalten und Report interpretieren
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.
Це не бюрократія, а економія часу: полювання за витоками пам’яті інакше швидко перетворюється на безкінечний цикл, оскільки ті самі шаблони повторюються в кожному спринті.
Коли зусилля виправдані — і коли варто діяти інакше
FastMM FullDebugMode — діагностичний інструмент, що вимагає витрат. Зусилля особливо виправдані, якщо:
- додаток працює тривалий час (служба, клієнт термінального сервера, система змін, процеси 24/7).
- ти обробляєш реальні потоки даних клієнтів і не покриваєш у тестах усі шляхи.
- стабільність важливіша за швидке додавання нових функцій (типово для програмних рішень, наближених до бізнес-процесу).
Якщо ж у тебе невеликий десктопний хелпер, який завершується через 30 секунд, виявлення витоків часто відходить на другий план. Так само: якщо це одноразова проблема пікового використання пам’яті (наприклад, великий експорт), це часто не витік, а питання стратегії потокової обробки та пікових навантажень у купі.
Практичний висновок: FullDebugMode — це не перемикач, а процес
FastMM FullDebugMode надає структуру пошуку помилок пам’яті: він робить виділення пам’яті видимими, виявляє пошкодження купи на ранній стадії та надає стек-трейси, за якими можна виправляти причину, а не симптом. Вирішальний важіль — не сам інструмент, а процес: відтворювані сценарії, збірки, придатні для діагностики, чіткі правила володіння ресурсами та регресійне тестування щодо базової версії.
Якщо ти застряг на впертій витоку або спорадичній помилці купи й хочеш забезпечити тривалу стабільність цієї проблеми в більшому Delphi-системі, варто зробити коротке, акуратно організоване діагностичне налаштування з чіткою послідовністю та звітами, придатними для аналізу. Якщо потрібна підтримка з аналізу, профілів збірки або рефакторингу архітектури — зв’яжіться з Net-Base Software GmbH.
Для цієї теми також важливі Delphi Виявлення витоків пам’яті та Читання Fastmm Leak Report. Стаття систематизує ці аспекти зрозуміло і показує, на що звертати увагу в щоденній практиці.
Наступний крок
Якщо тема перетворюється на реальний проєкт, архітектуру, наявні системи та експлуатацію слід розглядати разом на ранньому етапі.
Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.
- Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
- REST, доступ до даних, портали та Rollout не відсуваються на пізніший етап.
- Ви заздалегідь бачите, який шлях є економічно та операційно життєздатним.