Timers Revisited
The Problem
Basic Timer usage (set interval, start, repeat) covers simple cases. But real shells need sophisticated timing: debounce rapid input, chain timers sequentially, throttle expensive operations, and synchronize multiple update intervals. The naive single-timer approach breaks under these demands.
The Naive Approach
Multiple independent timers each polling their own data source:
Timer { interval: 1000; onTriggered: updateClock() } // clock
Timer { interval: 5000; onTriggered: updateCpu() } // CPU
Timer { interval: 30000; onTriggered: updateWeather() } // weatherEach timer wakes up independently, potentially causing N UI updates per second where one would suffice.
Multiple timers are like multiple alarm clocks in the same room. They all ring at different times, waking you up constantly. A unified scheduler is a single clock with multiple checkpoints — it rings once and tells you everything you need to do.
The Idea
Consolidate all periodic updates into a single heartbeat timer that delegates to different updaters based on elapsed time thresholds. Use debounce timers for input events and sequential timers for multi-step animations or staggered updates.
Let's Build It
import Quickshell
import QtQuick
import QtQuick.Layouts
Item {
id: root
property int cpuUsage: 0
property int memUsage: 0
property int diskUsage: 0
property int batteryPercent: 0
// Unified heartbeat — fires every second
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
var tick = root.TickCounter++
// Every second: clock only
updateClock()
// Every 5 seconds: CPU + memory
if (tick % 5 === 0) {
updateCpu()
updateMemory()
}
// Every 30 seconds: disk
if (tick % 30 === 0) updateDisk()
// Every 60 seconds: battery
if (tick % 60 === 0) updateBattery()
}
}
property int TickCounter: 0
function updateClock() {
clockText.text = new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
}
function updateCpu() { /* process call */ }
function updateMemory() { /* process call */ }
function updateDisk() { /* process call */ }
function updateBattery() { /* process call */ }
RowLayout {
anchors { fill: parent; margins: 8 }
Text { id: clockText; color: "#c0caf5"; font.pixelSize: 14 }
}
}Let's Improve It
Debounce Timer
Delay an action until after a pause in activity — perfect for search inputs or resize handlers:
Item {
property string searchText: ""
Timer {
id: debounceTimer
interval: 300
onTriggered: performSearch(searchText)
}
onSearchTextChanged: {
debounceTimer.restart() // Reset the 300ms delay on each keystroke
}
function performSearch(query) {
console.log("Searching for:", query)
}
}Sequential Timer Chain
Run timers one after another for staggered animations or progressive loading:
Item {
Timer {
id: step1
interval: 1000
onTriggered: {
console.log("Step 1")
step2.start()
}
}
Timer {
id: step2
interval: 1500
onTriggered: {
console.log("Step 2")
step3.start()
}
}
Timer {
id: step3
interval: 2000
onTriggered: console.log("Step 3 — done")
}
function startChain() {
step1.start()
}
}Throttle Timer
Ensure an expensive operation runs at most once per interval, even if triggered frequently:
Item {
Timer {
id: throttleTimer
interval: 1000
onTriggered: {
flushPendingUpdates()
ready = true
}
}
property bool ready: true
function requestUpdate() {
if (ready) {
ready = false
throttleTimer.start()
}
}
function flushPendingUpdates() {
// Aggregate and process all pending changes
}
}Timer drift accumulation. Using interval: 1000 with repeat: true does not guarantee exactly-once-per-second. The timer fires when the event loop gets to it. For precise intervals, calculate the next tick from the current system time rather than relying on counter increments.
Not resetting debounce timers. A debounce timer must call restart() instead of start() on each trigger event. start() does nothing if the timer is already running; restart() always resets the interval.
Stopping timers incorrectly. Calling stop() on a one-shot timer before it fires is fine. Calling stop() on a repeating timer stops all future ticks. Use running = false for property-binding-based control.
Under the Hood
Each Timer creates a QTimer in Qt's event loop. The minimum effective interval is ~1ms, but practical reliability starts at ~16ms (60fps cadence). Qt timers are coalesced by the OS — the actual firing time depends on event loop pressure. For sub-millisecond precision, you need a hardware timer or real-time thread, which QML cannot provide.
When you set interval: 5000, QTimer calls startTimer(5000, Qt::CoarseTimer) by default. Qt uses a timing wheel data structure internally to manage thousands of timers efficiently — O(1) insertion and removal, O(n) dispatch. The timer event is delivered through the normal event loop, meaning slow bindings or JavaScript execution can delay subsequent ticks.
Exercises
- Consolidate periodic updates into a single heartbeat timer with modulus dispatch
- Use debounce timers (restart on each trigger) for input events
- Chain timers sequentially for multi-step workflows
- Throttle timers ensure at-most-once-per-interval execution
- Timer accuracy depends on event loop pressure — not real-time