Net-Base Magazyn

29.10.2026

VCL/FMX: Timer-Drift vermeiden – präzise Timestamps statt Intervallzählen und UI-Last berücksichtigen

VCL/FMX-Timer sind keine Präzisionstimer. Wer Interval zählt, baut Drift ein – besonders unter UI-Last. So stellst du auf monotone Timestamps um, entscheidest Catch-up vs. Clamp und debuggst die echten Ursachen.

29.10.2026

Od tematu magazynowego do praktyki projektowej

Pasujące strony usługowe i techniczne do artykułu

Klasyczny przypadek rzadko występuje w teście laboratoryjnym, a częściej trzeciego dnia u klienta: status ma się aktualizować „co 10 sekund”, timeout ma zadziałać „po 2 minutach”, pasek postępu ma się zwiększać „na minutę”. W fazie rozwoju wszystko wydaje się stabilne. Potem pojawia się rzeczywiste obciążenie UI: duże grid jest sortowane, raport jest renderowany, generowane jest PDF, równocześnie uruchamiane są wywołania Synchronize z wątków roboczych. I nagle to już nie 10 sekund, lecz 12, 15 albo 30. Kto w tym momencie nadal „liczy interwały”, trudno mu to będzie sensownie wytłumaczyć — i otrzyma dryfujące logi, nieprawdopodobne wartości czasu oraz trudne do odtworzenia błędy. Właśnie tutaj praktycznie zaczyna działać Unikanie dryfu timerów: nie przez „magiczne timery precyzyjne”, lecz przez precyzyjne znaczniki czasu, monotoniczną bazę czasu i świadomą decyzję, jak aplikacja traktuje opóźnienia.

Ścieżka decyzyjna jest prosta, ale istotna: czy pozostaniesz przy wygodnym modelu „każde OnTimer to tick” (szybko do wdrożenia, ale systematycznie podatnym na dryf), czy przebudujesz logikę czasu tak, aby opierała się na rzeczywiście upływającym czasie (minimalnie więcej pracy, ale odporność przy obciążeniu UI)? Artykuł porównuje opcje, pokazuje główny konflikt celów (poprawność vs. prostota) i prowadzi do pierwszego kroku, który da się dorobić także w rozwiniętych aplikacjach VCL/FMX.

Unikanie dryfu timerów: Skąd naprawdę bierze się dryf timerów: Timery UI nie są generatorem taktów

Jeżeli VCL-TTimer ustawiony jest na 1000 ms, nie oznacza to: „kod uruchamia się co sekundę”. Oznacza raczej: „gdy wątek UI ma czas, zdarzenie zostanie przetworzone w pętli komunikatów mniej więcej co sekundę”. W VCL jest to jawnie część modelu: TTimer jest powiązany z przetwarzaniem wiadomości i dlatego nie jest przeznaczony do precyzyjnego odmierzania czasu; pod obciążeniem zdarzenia są wywoływane później.[Źródło] Wniosek jest surowy, ale uwalniający: timer nie jest „zbyt niedokładny”, lecz nie wolno go mylić z zegarem.

W FMX implementacja różni się w zależności od platformy, ale w praktyce trafia się w ten sam wzorzec: główny wątek jest wąskim gardłem dla renderingu, wejścia i pętli zdarzeń. Jeśli go zablokujesz lub przeciążysz Synchronize/Queue, wszystko, co tam jest przetwarzane, przesunie się — łącznie z callbackami timerów. Widać to szczególnie przy przerywających się animacjach UI lub nierównomiernych wywołaniach timerów, gdy nakłada się wiele bloków Synchronize; sednem nie jest mistyka, lecz przeciążenie głównego wątku.[Źródło]

Punkt decyzyjny: Co fachowo oznacza „co X sekund”?

Zanim zmienisz technikę, doprecyzuj wymaganie. W oprogramowaniu biznesowym pod „co X sekund” często kryją się bardzo różne oczekiwania:

  • Minimalny odstęp: Coś nie może występować częściej niż co X sekund (np. retry-backoff, polling).
  • Stabilna średnia częstotliwość: W ujęciu średnim ma się to zgadzać (np. „na godzinę 3600 próbek” lub licznik czasu działania, który nie zbacza).
  • Odświeżanie wizualne: UI ma się regularnie aktualizować, ale dokładność jest drugorzędna (postęp, animacje, wskaźniki).
  • Czas zegarowy: Coś ma się wydarzyć w określonym punkcie godzinowym (np. 02:00), niezależnie od dryfu czasu działania – tu obowiązuje czas systemowy, a nie zegar monotoniczny.

Najważniejszy konflikt celów pojawia się, gdy oczekujesz częstotliwości bez dryfu, a jednocześnie używasz UI-Timera wyłącznie jako wyzwalacza. Wówczas liczenie interwałów jest niemal zawsze błędne. Do wymuszenia minimalnej przerwy często wystarcza kontrola oparta na znacznikach czasu: „czy od ostatniego razu rzeczywiście upłynęło X?”

Opcje w porównaniu: trzy sposoby radzenia sobie z dryfem timera

Realistycznie napotkasz trzy podejścia. Nie są one same w sobie „dobre” lub „złe”, lecz pasują do różnych celów.

Opcja Zasada Zalety Typowa wada Dobrze pasuje do
Intervall zählen OnTimer => Zeit += Interval Minimalny nakład, szybko widoczny „efekt” Dryf jest wprowadzany, gdy callbacki są opóźnione Krótkotrwałe efekty UI bez znaczenia merytorycznego
Timestamp-basiert im UI-Timer OnTimer => Δt = now – last Bez dryfu w sensie odwzorowania czasu rzeczywistego, odporny pod obciążeniem Musisz aktywnie zadecydować między nadrabianiem zaległości (catch-up) a ograniczaniem (clamp) Timeouty, Backoff, status, postęp, wiarygodność w logach
Scheduler/Worker außerhalb UI Logik tickt im Hintergrund, UI rendert nur Obciążenie UI w niewielkim stopniu wpływa na logikę czasową Wielowątkowość, zamykanie, synchronizacja stają się konieczne Próbkowanie, komunikacja z urządzeniami, wymagająca periodyczność

Jeśli dziś obserwujesz dryf czasu, opcja 2 zwykle jest najlepszym pierwszym krokiem: możesz pozostać przy UI-Timerze, ale skorygować logikę czasową. Opcja 3 ma sens, gdy periodyczność jest merytorycznie „twarda” lub na każdy tick przypada rzeczywista praca.

Najczęstszy błąd logiczny: „Jedes OnTimer ist ein Tick”

Dryf często nie pochodzi „od systemu operacyjnego”, lecz z twojego modelowania: traktujesz opóźnione zdarzenie jak punktualne. To generuje dwa następstwa:

  • Powolne przesunięcie czasu: Twój „czas wewnętrzny” pozostaje w tyle za rzeczywistym czasem pracy lub biegnie przed nim, w zależności od wzorca.
  • Efekty grupowania (batching): Gdy wątek UI znów będzie wolny, zdarzenia timera przyjdą w krótkim odstępie — i twój kod wykona akcje seryjnie (np. kilka polli, kilka zapisów do DB).

VCL-TTimer jest tu dobrym przykładem: jest podłączony do pętli komunikatów wątku GUI; gdy ten jest zajęty, komunikaty timera są przetwarzane później.[Quelle] Konsekwencja dla architektury: zdarzenie timera to „punkt kontrolny”, a nie takt.

Czyste podejście: monotone Timestamps und Δt-basierte Zeitlogik

Robustna przebudowa składa się z dwóch decyzji, które powinieneś wyraźnie rozdzielić: (1) źródło czasu, (2) strategia aktualizacji.

Źródło czasu: czas wykonania potrzebuje monotoniczności, nie czasu ściennego

Dla czasów wykonywania i różnic czasowych potrzebujesz monotonicznego źródła czasu — zegara, który nie cofa się i nie jest podatny na synchronizację czasu ani ręczne zmiany. Microsoft opisuje API wysokiej rozdzielczości dla znaczników czasu i przypisuje je do zastosowań; QueryPerformanceCounter (QPC) jest typowym wyborem do pomiarów o wysokiej rozdzielczości.[Źródło] Praktyczny wniosek: dla Δt QPC (lub Delphi-abstrakcja taka jak TStopwatch) jest często bardziej odpowiednia niż Now. Dla grubszego zakresu potrzeb GetTickCount64 jest często wystarczające.

Ograniczenie tego źródła jest istotne: nie sprawi, że twój timer stanie się „punktualny”. Zapewnia natomiast, że poprawnie zmierzysz opóźnienie i potraktujesz je fachowo.

Strategia aktualizacji: mierzyć Δt, nie „wierzyć interwałowi”

Zmiana jest koncepcyjnie niewielka, ale skuteczna: przy każdym callbackie odczytujesz „now”, obliczasz Δt od „last” i aktualizujesz stan na podstawie tej rzeczywistej różnicy czasu. Dzięki temu timeouty, postępy i szybkości pozostają sensowne pod obciążeniem, nawet jeśli samo zdarzenie przychodzi nieregularnie.

Konflikt celów: Catch-up vs. Clamp (i dlaczego to decyzja merytoryczna)

Z Δt pojawia się automatycznie kolejne pytanie: co robisz, gdy Δt jest duże, bo UI się zawiesiło? Dwie strategie są powszechne, obie mają uzasadnione przypadki użycia.

Catch-up: nadrabianie pominiętych kroków

Dzielisz Δt na kilka „Fixed Steps” i wykonujesz logikę w pętli, aż zaległości zostaną nadrobione. To atrakcyjne dla stałych częstotliwości (symulacja, próbkowanie). Ale wada: w wątku UI Catch-up może wywołać lawinę obciążeń. Jedno zawieszenie powoduje wtedy wiele iteracji, które zablokują wątek UI — kolejne zawieszenie jest niemal gwarantowane.

Clamp: Δt begrenzen, Reaktivität priorisieren

Ograniczasz Δt do maksimum (typowo 100–200 ms dla logiki bliskiej UI) i akceptujesz, że „utraconego czasu” nie da się w pełni zasymulować. Dla wielu procesów biznesowych jest to wręcz pożądane: użytkownicy nie chcą późniejszego „nadganiania”, lecz stabilnej użyteczności, wiarygodnych wyświetleń i timeoutów, które nie rosną bez kontroli.

Jeśli poważnie traktujesz próbkowanie/sterowanie, zwykle dochodzi się do kombinacji: logika czasu i Catch-up w workerze, UI tylko jako widok z ograniczonymi aktualizacjami.

Skonstruowany przykład: dryft powstaje z „poprawnie działającego“ kodu

UI-Last realistisch einpreisen: VCL/FMX reagieren ähnlich, nur die Symptome variieren

W VCL i FMX główny wątek jest wąskim gardłem: obsługa wejść, układu, renderingu, pętla wiadomości/zdarzeń, a często także wywołania timerów działają tam sekwencyjnie. To jest powód, dla którego problem z „trochą więcej logiki” w timerze często się nasila: zwiększasz obciążenie dokładnie tam, gdzie twój timer już konkuruje o zasoby.

W VCL zachowanie TTimer jest powiązane z pętlą komunikatów i dlatego przy obciążeniu działa z opóźnieniem.[Quelle] W FMX często dodatkowo dochodzi do nasilającego efektu, ponieważ synchroniczne wywołania obciążające UI lub wysoka częstotliwość aktualizacji UI mogą zakłócać rendering; opisane efekty zacinania są typowe, gdy zbyt dużo pracy trafia do Main Thread.[Quelle]

Praktyczny wniosek dla leadów technicznych i administratorów (oraz dla każdego, kto później będzie musiał wyjaśnić działanie): jeśli aplikacja wykonuje krytyczną czasowo logikę w wątku UI, to „pod obciążeniem” nie jest scenariuszem brzegowym, lecz normalnym trybem pracy. Dryf timera nie jest wtedy niespodzianką, lecz oczekiwanym zachowaniem systemu.

Der erste Schritt, der fast immer lohnt: Intervallzählen eliminieren

Nie musisz od razu wdrażać kompletnej architektury planowania. Najszybszy, niskiego ryzyka krok: wszędzie tam, gdzie timing ma znaczenie, zamień „Tick zählt” na „Zeit wird gemessen”.

  1. Zeitquelle festlegen: Für Laufzeitmessungen monotone Quelle wählen (QPC/TStopwatch oder GetTickCount64).
  2. Letzten Zeitpunkt speichern: Pro Logikbereich ein lastTimestamp halten (w kontrolerze/serwisie, nie jako szczegół komponentu UI).
  3. Δt berechnen: Bei jedem Trigger Δt = now − lastTimestamp berechnen, dann lastTimestamp = now setzen.
  4. Schwellen prüfen statt pro Tick handeln: Aktionen an „Δt-Summe ≥ Schwelle“ koppeln (z.B. 10 s), nicht an die Anzahl Callbacks.
  5. Clamp/Catch-up bewusst wählen: UI-nah meist clamp, fachliche Periodik eher catch-up (dann besser im Worker).
  6. Messwerte loggen: Δt-Min/Max, Handler-Dauer und „ausgelassene“ Schritte erfassen, um Verhalten später erklären zu können.

Sztuczka z „Δt-Summe” (akumulatorem) zapobiega temu, by drobne nieregularności od razu przesunęły periodyczność. Zbierasz Δt i wykonujesz akcję, gdy uzbiera się wystarczająco czasu. W ten sposób z nieregularnego triggera robi się odporna na dryft częstość — bez udawania, że wywołania zwrotne UI są punktualne.

Pułapki: jak dryft wraca mimo podejścia opartego na znacznikach czasu

Systemowy zegar jako podstawa czasu wykonywania

Now jest przeznaczony do czasu zegarowego (logi, terminy, czas „dla ludzi”). Do pomiarów czasów wykonywania jest ryzykowny, bo czas systemowy może skakać. Porządne podejście: czas monotoniczny dla Δt, dodatkowo UTC/czas zegarowy do audytu/wyświetlania, ale rozdzielone w logice i modelu danych.

Nadrabianie zaległości w wątku UI

Jeżeli nadrabiasz zaległości w timerze w wątku UI, tworzysz błędne koło: zacięcie generuje zaległość, zaległość generuje więcej pracy w UI, więcej pracy generuje kolejne zacięcie. Jeżeli nadrabianie jest konieczne, to znak, że trzeba rozdzielić odpowiedzialności: Worker liczy, UI pokazuje.

I/O lub DB w handlerze timera

Handler timera, który wykonuje synchroniczne żądania HTTP, duże transakcje DB lub skanuje pliki, to praktycznie gwarantowane zablokowanie UI. Nawet przy doskonałej logice Δt użytkownicy zobaczą klastery operacji. Lepsza struktura: timer inicjuje zadanie, Worker je wykonuje, UI otrzymuje krótkie aktualizacje.

Debugowanie zniekształca pomiary czasu

Punkty przerwania w wątku głównym to katastrofa dla pomiarów czasu. Do analizy dryftu używaj lekkich pomiarów: mierzyć czas trwania handlera, agregować Δt (np. Min/Max) i logować tylko przy odchyleniach. W ten sposób uzyskasz obraz bez „spowalniania aplikacji po to, by zmierzyć spowolnienie”.

Checklist debuggingowa: ujawnić dryft zamiast zgadywać

  • Rejestrować Δt na tick: Nie tylko wartości średnie, lecz Min/Max i odstające wartości.
  • Mierzyć czas trwania handlera: Jak długo sam callback timera blokuje?
  • Liczyć częstotliwość aktualizacji UI: Ile operacji Synchronize/Queue/Invalidate pojawia się na sekundę?
  • Zdefiniować „budżet”: Np. „Timer-Handler < 2 ms” jako wartość docelowa; powyżej tego to kwestia wydajności, nie timera.
  • Generować odtwarzalne obciążenie: Duże ilości danych w gridzie, celowe repainty, symulowana latencja I/O (bez breakpointów).

Dzięki temu możesz wyraźnie rozróżnić: Czy timer jest opóźniony, bo pipeline UI jest zablokowany? Czy to handler sam blokuje? A może jedno i drugie? Dopiero wtedy warto przechodzić do kwestii wątków, alternatywnych mechanizmów timera lub optymalizacji renderowania.

Kiedy naprawdę opłaca się krok „Timer poza UI”?

Timestampowy timer w UI rozwiązuje już wiele problemów. Większa przebudowa (Worker/Scheduler) jest opłacalna, gdy zachodzi któryś z poniższych punktów:

  • Ścisła periodyczność: z punktu widzenia domeny trzeba utrzymać określoną częstość (sampling, komunikacja z urządzeniami, sterowanie).
  • Praca na tick nie jest trywialna: I/O, DB, parsowanie, kryptografia, większe obliczenia.
  • Interfejsy krytyczne dla obsługi: terminale, kasy, pulpity operatorskie — UI musi reagować także pod obciążeniem.
  • Powtarzalność: timing musi być testowalny i możliwy do wsparcia (monitoring, korelacja logów).

Porządna docelowa architektura to zwykle: rdzeń w tle z monotoniczną bazą czasu i własną kontrolą obciążenia; UI jest odświeżane z ograniczoną częstotliwością (np. 5–10 aktualizacji/s wystarcza dla wielu widoków) i dzięki temu pozostaje stabilne. To nie tylko redukuje dryft, ale też ogranicza dyskusje w supportcie: „Dlaczego timeout nagle wynosił 3 minuty?” — na to można odpowiedzieć danymi pomiarowymi.

Źródła czytać poprawnie: co one zabezpieczają — i jaką lukę musisz sam wypełnić

Dokumentacja Embarcadero dotycząca Vcl.ExtCtrls.TTimer dostarcza kluczowych podstaw: Timer jest powiązany z przetwarzaniem komunikatów GUI i dlatego pod obciążeniem nie jest precyzyjny.[Źródło] Dokumentacja nie podaje jednak gotowej strategii dla Catch-up/Clamp ani odsprzęgania. To praca architektoniczna, wynikająca z twoich wymagań funkcjonalnych.

Ujęcie Microsoftu dotyczące wysokorozdzielczych znaczników czasu wyjaśnia, dlaczego powinieneś traktować czas jako wartość pomiarową: API takie jak QueryPerformanceCounter zostały zaprojektowane do obliczania wiarygodnych różnic czasowych; mierzysz czas zamiast polegać na regularności zdarzeń.[Źródło] Luka pozostaje: wciąż potrzebujesz wyzwalacza (Timer, Wait, Loop). Właśnie dlatego sensowna jest kombinacja: Timer jako budzik, monotoniczne znaczniki czasu jako prawda.

Obraz końcowy: co natychmiast poprawi się po przebudowie

Po przebudowie rzadko uzyskujesz „precyzję mikrosekundową”. Zysk to przewidywalność. Time-outy nie przedłużają się po cichu, backoff zachowuje się konsekwentnie, postęp i odliczanie pozostają wiarygodne, a logi znów tworzą zrozumiałą oś czasu. Przede wszystkim zyskujesz nowe narzędzie diagnostyczne: gdy tylko Δt i czas działania handlera zostaną zarejestrowane, prawdziwe przyczyny (masowe przerysowania, zbyt wiele wywołań Synchronize, I/O w UI) znajdziesz dużo szybciej.

Jeśli chcesz zacząć dziś: przeszukaj kod w poszukiwaniu miejsc, które wyprowadzają czas z liczby zdarzeń timera („+Interval”, licznik na tick, odliczanie na tick). Przestaw tę logikę na monotoniczne znaczniki czasu i Δt. Potem możesz celowo zdecydować, które części powinny trafić do wątku roboczego, a które mogą pozostać blisko UI.

Źródła i materiały uzupełniające

Główne stwierdzenia merytoryczne zostały redakcyjnie umieszczone w kontekście poniższych źródeł zewnętrznych.

  1. Vcl.ExtCtrls.TTimer – RAD Studio API Documentation (docwiki.embarcadero.com)
    VCL TTimer jest powiązany z przetwarzaniem komunikatów w wątku GUI i dlatego nie jest timerem precyzyjnym; pod obciążeniem zdarzenia są wyzwalane z opóźnieniem.
  2. Acquiring high-resolution time stamps – Win32 apps | Microsoft Learn (learn.microsoft.com)
    Dla precyzyjnych różnic czasowych w ramach Windows przewidziane są monotoniczne, wysokorozdzielcze znaczniki czasu, takie jak QueryPerformanceCounter; wybór API do znaczników czasu zależy od wymagań co do precyzji.
  3. Why can a `TSkAnimatedImage` stutter when FMX work is wrapped in `TThread.Synchronize`? – Stack Overflow for Agents (agents.stackoverflow.com)
    W FMX dodatkowa praca w wątku głównym (np. przez Synchronize) może powodować zacinanie/opóźnienia, ponieważ potok UI jest przetwarzany w wątku głównym.
  • performance – Why is a Timer on Android more accurate when a finger is touching the screen? – Stack Overflow (stackoverflow.com)
    Dokładność timera w kontekście Android/FMX może być wpływana przez aktywność pętli zdarzeń; timery są praktycznie zależne od przetwarzania w wątku głównym.
  • Dla tego tematu ważne są także Delphi Ttimer i Fmx Timer Drift. Artykuł porządkuje te aspekty w przystępny sposób i pokazuje, na co zwracać uwagę w praktyce.

    Omów projekt lub przedsięwzięcie modernizacyjne z Net-Base.

    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.

    Udostępnij wpis

    Udostępnij ten wpis bezpośrednio

    LinkedIn, X, XING, Facebook, WhatsApp i e-mail są natychmiast dostępne. Dla Instagrama przygotowujemy bezpośrednio link i krótki tekst.

    E-mail

    Instagram otwiera się w nowej karcie. Link i krótki tekst są wcześniej kopiowane do schowka.

    Napisz komentarz

    Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert