Net-Base Dergi

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

Dergi konusundan proje pratiğine

İçeriğe Uygun Hizmet ve Teknik Sayfalar

Der Klassiker passiert selten im Labortest, sondern am dritten Tag beim Kunden: Ein Status soll „alle 10 Sekunden“ aktualisieren, ein Timeout soll „nach 2 Minuten“ greifen, eine Fortschrittsanzeige soll „pro Minute“ hochlaufen. In der Entwicklung wirkt alles stabil. Dann kommt reale UI-Last dazu: ein großes Grid wird sortiert, ein Report wird gerendert, ein PDF wird erzeugt, nebenbei laufen Synchronize-Aufrufe aus Worker-Threads. Und plötzlich sind es nicht mehr 10 Sekunden, sondern 12, 15 oder 30. Wer an dieser Stelle noch „Interval zählt“, kann das kaum sauber erklären – und bekommt driftende Logs, unplausible Zeitangaben und schwer reproduzierbare Fehler. Genau hier setzt Timer-Drift vermeiden praktisch an: nicht durch „magische Präzisionstimer“, sondern durch präzise Timestamps, eine monotone Zeitbasis und eine bewusste Entscheidung, wie die Anwendung Verspätungen behandelt.

Der Entscheidungspfad ist simpel, aber wichtig: Bleibst du beim bequemen Modell „jedes OnTimer ist ein Tick“ (schnell implementiert, aber systematisch driftanfällig), oder baust du die Zeitlogik so um, dass sie auf der tatsächlich vergangenen Zeit beruht (minimal mehr Aufwand, dafür robust unter UI-Last)? Der Beitrag vergleicht Optionen, zeigt den Haupt-Zielkonflikt (Korrektheit vs. Einfachheit) und führt zu einem ersten Schritt, der sich auch in gewachsenen VCL/FMX-Anwendungen nachrüsten lässt.

Timer-Drift vermeiden: Wo Timer-Drift wirklich herkommt: UI-Timer sind keine Taktgeber

Wenn ein VCL-TTimer auf 1000 ms steht, heißt das nicht: „der Code läuft jede Sekunde“. Es heißt eher: „wenn der UI-Thread Zeit hat, wird ungefähr im Sekundentakt ein Event in der Message-Pump abgearbeitet“. In der VCL ist das explizit Teil des Modells: TTimer ist an die Nachrichtenverarbeitung gebunden und damit nicht für präzises Timing gedacht; unter Last werden Events später ausgelöst.[Quelle] Die Folgerung ist hart, aber befreiend: Der Timer ist nicht „zu ungenau“, sondern du darfst ihn nicht als Uhr missverstehen.

In FMX ist die Implementierung je Plattform unterschiedlich, aber in der Praxis landet man im selben Muster: Der Main Thread ist Engstelle für Rendering, Input und Eventloop. Wenn du ihn blockierst oder mit Synchronize/Queue überfrachtest, verschiebt sich alles, was dort abgearbeitet wird – einschließlich Timer-Callbacks. Man sieht das besonders deutlich bei stotternden UI-Animationen oder ungleichmäßigen Timer-Aufrufen, sobald viele Synchronize-Blöcke auflaufen; der Kern ist nicht Mystik, sondern Main-Thread-Überlastung.[Quelle]

Entscheidungspunkt: Was bedeutet „alle X Sekunden“ fachlich wirklich?

Bevor du Technik austauschst, kläre die Anforderung. In Business-Software verstecken sich hinter „alle X Sekunden“ oft sehr unterschiedliche Erwartungen:

  • Mindestabstand: Etwas darf nicht häufiger als alle X Sekunden passieren (z.B. Retry-Backoff, Polling).
  • Driftfreie Rate: Im Mittel soll es stimmen (z.B. „pro Stunde 3600 Samples“ oder eine Laufzeitanzeige, die nicht wegläuft).
  • Optischer Refresh: Die UI soll regelmäßig aktualisieren, aber Exaktheit ist zweitrangig (Progress, Animationen, Anzeigen).
  • Wandzeit: Etwas soll zu einem Uhrzeit-Zeitpunkt passieren (z.B. 02:00), unabhängig von Laufzeitdrift – hier gilt die Systemzeit, nicht eine monotone Uhr.

Der wichtigste Zielkonflikt entsteht, wenn du driftfreie Rate erwartest, aber ein UI-Timer nur als Trigger benutzt wird. Dann ist Intervallzählen fast immer falsch. Für Mindestabstand hingegen reicht oft schon ein timestampbasierter Check: „ist seit dem letzten Mal wirklich X vergangen?“

Optionen im Vergleich: Drei Wege, mit Timer-Drift umzugehen

Realistisch begegnen dir drei Ansätze. Sie sind nicht „gut“ oder „schlecht“ per se, sondern passen zu unterschiedlichen Zielen.

Option Prinzip Stärken Typische Schwäche Passt gut für
Intervall zählen OnTimer => Zeit += Interval Minimaler Aufwand, schnell „irgendwas“ sichtbar Drift wird eingebaut, sobald Callbacks verspätet sind Kurzlebige UI-Effekte ohne fachliche Relevanz
Timestamp-basiert im UI-Timer OnTimer => Δt = now – last Driftfrei im Sinne von „Realzeit abbilden“, robust unter Last Du musst Catch-up vs. Clamp aktiv entscheiden Timeouts, Backoff, Status, Progress, Plausibilität im Log
Scheduler/Worker außerhalb UI Logik tickt im Hintergrund, UI rendert nur UI-Last beeinflusst die Zeitlogik kaum Threading, Shutdown, Synchronisation werden Pflicht Sampling, Gerätekommunikation, schwerere Periodik

Wenn du heute driftende Zeit siehst, ist Option 2 meist der beste erste Schritt: Du kannst beim UI-Timer bleiben, aber die Zeitlogik korrigieren. Option 3 lohnt sich, wenn Periodik fachlich „hart“ ist oder pro Tick echte Arbeit anfällt.

Der häufigste Logikfehler: „Jedes OnTimer ist ein Tick“

Der Drift kommt oft nicht „vom Betriebssystem“, sondern aus deiner Modellierung: Du behandelst ein verspätetes Event wie ein pünktliches. Das erzeugt zwei Folgeschäden:

  • Schleichende Zeitverschiebung: Deine „interne Zeit“ hängt der realen Laufzeit hinterher oder läuft vor, je nach Muster.
  • Batching-Effekte: Wenn der UI-Thread wieder frei ist, kommen Timer-Events in kurzer Folge – und dein Code feuert Aktionen gehäuft ab (z.B. mehrere Polls, mehrere DB-Writes).

VCL-TTimer ist dafür ein Lehrbeispiel: Er hängt an der Message-Pump des GUI-Threads; wenn dieser beschäftigt ist, werden Timer-Nachrichten später verarbeitet.[Quelle] Die Konsequenz für Architektur: Das Timer-Event ist ein „Checkpunkt“, kein Taktgeber.

Sauberer Ansatz: monotone Timestamps und Δt-basierte Zeitlogik

Der robuste Umbau besteht aus zwei Entscheidungen, die du klar trennen solltest: (1) Zeitquelle, (2) Update-Strategie.

Zeitquelle: Laufzeit braucht Monotonie, nicht Wandzeit

Für Laufzeiten und Zeitdifferenzen willst du eine monotone Zeitquelle, also eine Uhr, die nicht rückwärts springt und nicht durch Zeitsynchronisation oder manuelle Änderungen beeinflusst wird. Microsoft beschreibt dafür hochauflösende Zeitstempel-APIs und ordnet sie nach Einsatzzweck ein; QueryPerformanceCounter (QPC) ist die typische Wahl für hochauflösende Messungen.[Quelle] Praktische Folgerung: Für Δt ist QPC (oder eine Delphi-Abstraktion wie TStopwatch) häufig sinnvoller als Now. Für gröbere Bedürfnisse ist GetTickCount64 oft ausreichend.

Die Grenze dieser Quelle ist wichtig: Sie macht deinen Timer nicht „pünktlich“. Sie sorgt dafür, dass du die Verspätung korrekt misst und fachlich sauber behandelst.

Update-Strategie: Δt messen, nicht „Interval glauben“

Die Umstellung ist konzeptionell klein, aber wirkungsvoll: Bei jedem Callback liest du „now“, berechnest Δt seit „last“ und aktualisierst deinen Zustand anhand dieser echten Zeitdifferenz. Damit bleiben Timeouts, Fortschritte und Raten unter Last plausibel, auch wenn das Event selbst unregelmäßig kommt.

Der Zielkonflikt: Catch-up vs. Clamp (und warum das eine Fachentscheidung ist)

Mit Δt kommt die nächste Frage automatisch: Was machst du, wenn Δt groß ist, weil das UI hing? Zwei Strategien sind üblich, beide haben legitime Anwendungsfälle.

Catch-up: verpasste Schritte nacharbeiten

Du teilst Δt in mehrere „Fixed Steps“ und arbeitest die Logik in einer Schleife ab, bis der Rückstand aufgeholt ist. Das ist attraktiv für feste Raten (Simulation, Sampling). Der Haken: Im UI-Thread kann Catch-up eine Lastlawine auslösen. Ein einziger Hänger führt dann zu vielen Iterationen, die wiederum den UI-Thread blockieren – der nächste Hänger ist vorprogrammiert.

Clamp: Δt begrenzen, Reaktivität priorisieren

Du begrenzt Δt auf ein Maximum (typisch 100–200 ms für UI-nahe Logik) und akzeptierst, dass du „verlorene Zeit“ nicht vollständig nachsimulierst. Für viele Business-Abläufe ist das sogar wünschenswert: Nutzer wollen kein nachträgliches „Aufholen“, sondern stabile Bedienbarkeit, plausible Anzeigen und Timeouts, die nicht unkontrolliert wachsen.

Wenn du Sampling/Steuerung wirklich ernst meinst, landet man meist bei einer Kombination: Zeitlogik und Catch-up im Worker, UI nur als Anzeige mit gedrosselten Updates.

Konstruiertes Beispiel: Drift entsteht durch „korrekt wirkenden“ Code

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

In VCL und FMX ist der Main Thread der Nadelöhr-Prozess: Eingaben, Layout, Rendering, Message-/Eventloop, und oft auch Timer-Callbacks laufen dort seriell. Das ist der Grund, warum das Problem mit „ein bisschen mehr Logik“ im Timer oft schlimmer wird: Du vergrößerst die Arbeit genau dort, wo dein Timer schon um Ressourcen konkurriert.

In der VCL ist das Verhalten von TTimer an die Message-Pump gebunden und daher unter Last verspätet.[Quelle] In FMX kommt als Verstärker häufig hinzu, dass UI-lastige Synchronize-Aufrufe oder eine hohe Frequenz an UI-Updates das Rendering stören können; die beschriebenen Stutter-Effekte sind typisch, wenn zu viel Arbeit im Main Thread landet.[Quelle]

Praktische Konsequenz für technische Leads und Admins (und für jeden, der später den Betrieb erklären muss): Wenn eine Anwendung zeitkritische Logik im UI-Thread abwickelt, ist „unter Last“ kein Randfall, sondern Normalbetrieb. Timer-Drift ist dann keine Überraschung, sondern ein zu erwartendes Systemverhalten.

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

Du musst nicht sofort eine komplette Scheduling-Architektur einziehen. Der schnellste, risikoarme Schritt: Überall dort, wo Timing fachlich relevant ist, ersetze „Tick zählt“ durch „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 (im Controller/Service, nicht als UI-Component-Detail).
  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.

Der Kniff mit „Δt-Summe“ (Akkumulator) verhindert, dass kleine Unregelmäßigkeiten sofort die Periodik verschieben. Du sammelst Δt auf und führst die Aktion aus, sobald genug Zeit „angespart“ wurde. Damit wird aus einem unregelmäßigen Trigger eine driftfeste Rate – ohne so zu tun, als wären UI-Callbacks pünktlich.

Fallstricke: Wie Drift trotz Timestamp-Ansatz zurückkommt

Systemuhr als Laufzeitbasis

Now ist für Wandzeit gedacht (Logs, Termine, menschliche Zeit). Für Laufzeiten ist es riskant, weil die Systemzeit springen kann. Sauber ist: monotone Zeit für Δt, zusätzlich UTC/Wandzeit für Audit/Anzeige, aber getrennt in der Logik und im Datenmodell.

Catch-up im UI-Thread

Wenn du Catch-up im UI-Timer machst, baust du dir einen Teufelskreis: Hänger erzeugt Rückstand, Rückstand erzeugt mehr Arbeit im UI, mehr Arbeit erzeugt den nächsten Hänger. Wenn Catch-up notwendig ist, ist das ein Signal für Entkopplung: Worker rechnet, UI zeigt.

I/O oder DB im Timer-Handler

Ein Timer-Handler, der synchron HTTP macht, große DB-Transaktionen fährt oder Dateien scannt, ist praktisch eine garantierte UI-Blockade. Selbst mit perfekter Δt-Logik werden Nutzer Batches sehen. Die bessere Struktur: Timer stößt an, Worker erledigt, UI bekommt kurze Updates.

Debugging verfälscht Timing

Breakpoints im Main Thread sind Timing-GAU. Für Driftanalyse nutze leichtgewichtige Messungen: Handler-Dauer messen, Δt aggregieren (z.B. Min/Max), und nur bei Ausreißern loggen. Damit bekommst du ein Bild, ohne die Anwendung zu „verlangsamen, um die Verlangsamung zu messen“.

Debugging-Checkliste: Drift sichtbar machen statt zu raten

  • Δt pro Tick erfassen: Nicht nur Mittelwerte, sondern Min/Max und Ausreißer.
  • Handler-Dauer messen: Wie lange blockiert das Timer-Callback selbst?
  • UI-Update-Frequenz zählen: Wie viele Synchronize/Queue-/Invalidate-Operationen pro Sekunde laufen an?
  • „Budget“ definieren: Z.B. „Timer-Handler < 2 ms“ als Zielwert; darüber ist es ein Performance-Thema, nicht ein Timer-Thema.
  • Last reproduzierbar erzeugen: Große Datenmengen im Grid, bewusstes Repaint, simulierte I/O-Latenz (ohne Breakpoints).

Damit kannst du sauber unterscheiden: Kommt der Timer verspätet, weil die UI-Pipeline blockiert? Oder blockiert dein Handler selbst? Oder beides? Erst dann lohnt es, an Threads, Timer-Alternativen oder Render-Optimierung zu drehen.

Wann lohnt sich der Schritt „Timer raus aus der UI“ wirklich?

Ein timestampbasierter UI-Timer löst viele Probleme bereits. Der größere Umbau (Worker/Scheduler) lohnt sich, wenn einer dieser Punkte zutrifft:

  • Harte Periodik: fachlich muss eine Rate eingehalten werden (Sampling, Gerätekommunikation, Steuerung).
  • Arbeit pro Tick ist nicht trivial: I/O, DB, Parsing, Kryptografie, größere Berechnungen.
  • Bedienkritische Oberflächen: Terminals, Kassen, Leitstände – UI muss auch unter Last reagieren.
  • Reproduzierbarkeit: Timing muss test- und supportbar sein (Monitoring, Log-Korrelation).

Die saubere Zielarchitektur ist meist: ein Hintergrundkern mit monotoner Zeitbasis und eigener Lastkontrolle; die UI wird gedrosselt aktualisiert (z.B. 5–10 Updates/s reichen für viele Anzeigen) und bleibt damit stabil. Das reduziert Drift nicht nur, es reduziert Supportdiskussionen: „Warum war der Timeout plötzlich 3 Minuten?“ lässt sich dann mit Messwerten beantworten.

Quellen richtig lesen: Was sie absichern – und welche Lücke du selbst schließen musst

Die Embarcadero-Doku zu Vcl.ExtCtrls.TTimer liefert die zentrale Grundlage: Der Timer hängt an der GUI-Nachrichtenverarbeitung und ist daher unter Last nicht präzise.[Quelle] Die Doku gibt dir aber keine fertige Strategie für Catch-up/Clamp oder Entkopplung. Das ist Architekturarbeit, die sich aus deinen fachlichen Anforderungen ableitet.

Microsofts Einordnung zu hochauflösenden Zeitstempeln erklärt, warum du Zeit als Messwert behandeln solltest: APIs wie QueryPerformanceCounter sind dafür gedacht, zuverlässige Zeitdifferenzen zu berechnen; du misst Zeit, statt dich auf Event-Regelmäßigkeit zu verlassen.[Quelle] Die Lücke bleibt: Du brauchst trotzdem einen Trigger (Timer, Wait, Loop). Genau deshalb ist die Kombination sinnvoll: Timer als Wecker, monotone Timestamps als Wahrheit.

Das Schlussbild: Was nach der Umstellung sofort besser wird

Nach dem Umbau ist das Ergebnis selten „Mikrosekundenpräzision“. Der Gewinn ist Vorhersagbarkeit. Timeouts laufen nicht stillschweigend länger, Backoff verhält sich konsistent, Fortschritt und Countdown bleiben plausibel, und Logs ergeben wieder eine erklärbare Timeline. Vor allem bekommst du ein neues Diagnosewerkzeug: Sobald Δt und Handler-Dauer erfasst sind, findest du die echten Ursachen (Repaint-Stürme, zu viele Synchronize-Aufrufe, I/O im UI) viel schneller.

Wenn du heute anfangen willst: Suche im Code nach Stellen, die aus der Anzahl Timer-Events Zeit ableiten („+Interval“, Zähler pro Tick, Countdown pro Tick). Stelle diese Logik auf monotone Timestamps und Δt um. Danach kannst du gezielt entscheiden, welche Teile in einen Worker gehören und welche bewusst UI-nah bleiben dürfen.

Quellen und weiterfuehrende Informationen

Die fachlichen Kernaussagen wurden anhand der folgenden externen Quellen redaktionell eingeordnet.

  1. Vcl.ExtCtrls.TTimer – RAD Studio API Documentation (docwiki.embarcadero.com)
    VCL TTimer ist an die Nachrichtenverarbeitung im GUI-Thread gebunden und daher kein Präzisionstimer; unter Last werden Events verspätet ausgelöst.
  2. Acquiring high-resolution time stamps – Win32 apps | Microsoft Learn (learn.microsoft.com)
    Für präzise Zeitdifferenzen unter Windows sind monotone, hochauflösende Zeitstempel wie QueryPerformanceCounter vorgesehen; Auswahl der Timestamp-API hängt vom Präzisionsbedarf ab.
  3. Why can a `TSkAnimatedImage` stutter when FMX work is wrapped in `TThread.Synchronize`? – Stack Overflow for Agents (agents.stackoverflow.com)
    In FMX kann zusätzliche Main-Thread-Arbeit (z.B. durch Synchronize) zu Stottern/Verzögerung führen, weil die UI-Pipeline im Main Thread verarbeitet wird.
  4. performance – Why is a Timer on Android more accurate when a finger is touching the screen? – Stack Overflow (stackoverflow.com)
    Timer-Genauigkeit auf Android/FMX-Kontext kann durch Eventloop-Aktivität beeinflusst werden; Timer hängen praktisch an der Main-Thread-Verarbeitung.

Für dieses Thema sind auch Delphi Ttimer und Fmx Timer Drift wichtig. Der Beitrag ordnet diese Aspekte verständlich ein und zeigt, worauf es im Alltag ankommt.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Sonraki adım

Konu gerçek bir projeye dönüştüğünde, mimari, mevcut sistemler ve işletme erken dönemde birlikte değerlendirilmelidir.

Bireysel sorularda destek vermekle kalmıyoruz; kaynak kodu parçacıklarından, legacy konularından veya portal fikirlerinden sağlam bir kurumsal projeye dönüşene kadar da destek veriyoruz.

  • Mevcut durum, hedef durum ve teknik riskler birlikte değerlendirilir.
  • REST, veri erişimi, portallar ve Rollout daha sonra ortaya çıkan sonuçlar olarak ertelenmez.
  • Hangi yolun ekonomik ve işletme açısından sürdürülebilir olduğunu erken görürsünüz.

Gönderiyi paylaş

Bu gönderiyi doğrudan paylaş

LinkedIn, X, XING, Facebook, WhatsApp ve e-posta hemen kullanılabilir. Instagram için bağlantıyı ve kısa metni doğrudan hazırlıyoruz.

E-posta

Instagram yeni bir sekmede açılır. Bağlantı ve kısa metin önceden panoya kopyalanır.

Yorum yaz

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