The first time I tried to track time on a local LLM run, I lost it.
I started a 3-hour Q4 quantization of a 7B model on Saturday morning. Started the timer in Toggl. Walked away. Came back at lunch to a sleeping Mac, an unfinished job, and a Toggl timer that had cheerfully clocked 3 hours and 14 minutes against the project "ml-experiments."
The data was a lie. The work didn't happen. The Mac slept after 20 minutes; the quantization stopped; my CPU usage graph for those 3 hours was a flat line. But Toggl didn't care — I'd told it I was working, so it counted me as working.
That's the structural problem with tracking time on local AI/ML work. The thing using your computer isn't you. The thing your time tracker is measuring is what you're doing, but the thing you actually want to know is whether the model is making progress. Those are different signals.
I've now spent enough time running fine-tunes, evaluations, and long-context generations locally on Apple Silicon that the workflow has settled into a few patterns. This is what I learned about tracking it correctly — and what Timex captures that other tools miss.
What "the model is running" actually looks like to a Mac
When Ollama or LM Studio or MLX or llama.cpp is doing work, your Mac's foreground app stays whatever you last clicked on. Usually that's the terminal you launched the job from, or the LM Studio chat window, or the browser tab pointed at localhost:11434. The job itself is a background process. As far as the OS is concerned, you are still on the terminal, even if you walked away an hour ago.
If you then close the lid or step away, two things happen:
- macOS treats you as idle (no input for >90 seconds).
- macOS treats the system as potentially sleepable, depending on power settings and whether the lid is closed.
The first thing makes your time tracker stop counting (correctly — you're not working). The second thing makes your model stop running (incorrectly — it was working).
The fix for the second thing is what I wrote about in Run Ollama in clamshell mode. Hold a power assertion, the Mac stays awake, the model keeps generating. Timex bundles this into the lid-down mode toggle so you don't have to remember the caffeinate flag.
The fix for the first thing is the topic of this post.
What you actually want to measure for ML work
There are three different durations you might care about for an LLM/ML run:
- Wall-clock time — when did the run start, when did it finish, total elapsed.
- Compute time — when was the model actively doing work (GPU/CPU > 0), not waiting on you or stuck.
- Your attention time — when were you actually watching/supervising the run.
For a quantization job, you mostly care about #1 and #2 (was it efficient, did the Mac stay awake the whole time). For an interactive eval session, you mostly care about #3 (how much of your time did this experiment consume). For deciding whether to keep using a model in production, you care about all three.
Most general-purpose time trackers conflate them. Toggl will tell you you spent 3 hours on "ml-experiments" if you start a timer and walk away. RescueTime will tell you you weren't at the computer. Both are technically correct and individually useless.
What you want is a record of when the run started, when it ended, and what you were doing during that window. Timex captures all three in one log.
How Timex sees a local LLM run
Here's the actual shape of the data for a Q4 quantization I ran last week. The run took 2 hours 41 minutes wall-clock. I was at the keyboard for 22 minutes of it.
-- The 3-hour window of the quantization run
SELECT
ts,
app_name,
window_title,
is_idle
FROM activity_samples
WHERE ts BETWEEN strftime('%s', '2026-05-31 09:14:00')
AND strftime('%s', '2026-05-31 11:55:00')
ORDER BY ts;
The rough shape of the rows (sampling every 15 minutes for readability):
09:14 Ghostty llama.cpp quantize-stats ... 0 (active, just started)
09:29 Ghostty llama.cpp quantize-stats ... 0 (active, watching output)
09:44 Ghostty llama.cpp quantize-stats ... 1 (idle, walked away)
09:59 loginwindow — 1 (locked screen)
10:14 loginwindow — 1 (still locked)
... [no rows from 10:30 to 11:30 — display slept, sampler paused]
11:32 Ghostty llama.cpp quantize-stats ... 0 (returned, woke up Mac)
11:47 Ghostty llama.cpp quantize-stats ... 0 (watching final phases)
11:55 Ghostty llama.cpp quantize-done 0 (job finished)
A few things to notice.
The window title is the same string the whole time (llama.cpp quantize-stats ...). That's important — it means I can grep the SQLite file later and find every quantization run by window title. The terminal preserves the foreground process name in the title bar; that's the breadcrumb I needed.
The rows go away during the display sleep window (10:30 to 11:30). That's correct. The sampler shouldn't be claiming activity when there's nothing being recorded. The gap in the strip is honest — it shows the Mac was asleep — and the gap is also what I want to investigate later (was that a clamshell-mode failure, or did I forget to enable it?).
The lock screen rows in between (loginwindow) are diagnostic gold. They tell me the screen locked at 09:59, which is when I left the room. If I'd been worried about whether the run was actually running while I was away, the absence of crash rows or different app rows is reassuring.
"Did the lid-down mode work this time?"
This is the question I ask about my own ML runs most often. Did the Mac stay awake the whole time? Or did clamshell fail at some point?
A SQL query gives the answer:
-- For a known run window, find any gap > 5 minutes (= sleep event)
WITH sample_gaps AS (
SELECT
ts AS t,
ts - LAG(ts) OVER (ORDER BY ts) AS gap_seconds
FROM activity_samples
WHERE ts BETWEEN strftime('%s', '2026-05-31 09:14:00')
AND strftime('%s', '2026-05-31 11:55:00')
)
SELECT datetime(t, 'unixepoch', 'localtime') AS gap_started, gap_seconds
FROM sample_gaps
WHERE gap_seconds > 300
ORDER BY t;
If lid-down mode worked perfectly, this query returns zero rows. If the Mac fell asleep at any point, you get a row for each sleep window with the start time and duration.
For the run above, the answer was: one gap, 53 minutes. The Mac slept from 10:30 to 11:23. The lid-down mode wasn't on. I'd forgotten to toggle it. The quantization paused for an hour, woke up when I came back and moved the mouse, and resumed.
The total run took 1 hour and 53 minutes longer than it should have. This is the kind of question you can't ask without a tracker that records honest gaps.
Annotating runs without leaving the terminal
The simplest "what was this run for" mechanism is the terminal window title. Most terminals let you set the title with an ANSI escape:
echo -ne "\033]0;quant-7b-q4 — eval-set-3\007"
ollama run mistral:7b-instruct-q4 < eval-set-3.jsonl > out.jsonl
That sets the window title to "quant-7b-q4 — eval-set-3" for the duration of the command. Timex captures that title in every sample row. Later, you can grep for eval-set-3 and pull out the exact window of that experiment.
The trick generalizes:
# Wrap any long-running ML command with a title tag
run_titled() {
local title="$1"
shift
echo -ne "\033]0;${title}\007"
"$@"
echo -ne "\033]0;${SHELL##*/}\007"
}
run_titled "mlx-finetune-llama3-r1-lr2e5" python finetune.py --lr 2e-5
Now every sample row inside that window shows up as mlx-finetune-llama3-r1-lr2e5 in the window title, and you can pull the exact start/end times and idle pattern out of SQLite weeks later. It's the cheapest form of experiment tracking imaginable, and it survives without any cloud service.
A real ML week
Here's what last week's mlx-* and llama-* titled work looked like in the data:
SELECT
window_title,
date(MIN(ts), 'unixepoch', 'localtime') AS started,
ROUND((MAX(ts) - MIN(ts)) / 60.0, 1) AS wall_minutes,
ROUND(SUM(CASE WHEN is_idle = 0 THEN 1 ELSE 0 END) / 60.0, 1) AS active_minutes
FROM activity_samples
WHERE window_title LIKE 'mlx-%' OR window_title LIKE 'llama-%'
GROUP BY window_title
ORDER BY MIN(ts);
Output:
mlx-finetune-llama3-r1-lr2e5 2026-05-26 189.2 wall 34.1 active
llama-cpp-quant-7b-q4-eval3 2026-05-31 161.8 wall 22.4 active
llama-cpp-quant-7b-q8-eval3 2026-06-01 178.5 wall 18.9 active
mlx-eval-mtbench-r1-vs-base 2026-06-02 87.0 wall 71.2 active
The eval (last row) was 87 minutes wall-clock and 71 minutes active — that was an interactive scoring session where I was watching outputs in real time. The two quantization jobs were 18–22 minutes of my attention for 2.5–3 hours of wall-clock — long unattended runs.
That ratio matters. The active-vs-wall column tells me which experiments cost my time and which cost my electricity. Different decisions follow.
What you give up versus a "real" ML experiment tracker
Here's what Timex is and isn't for the ML workflow.
A dedicated experiment tracker — Weights & Biases, MLflow, Aim — tracks the model. Loss curves, hyperparameters, metrics, artifacts. None of that lives in Timex. If you need that, run W&B alongside; the two are complementary.
What Timex tracks is the human side of the run. When did you start it. When did you walk away. When did it finish. What were you doing during it. Did the Mac stay awake. Did you go check Twitter while it was running. None of that lives in W&B.
If you do serious ML on your Mac and you're not logging any of the "human side" data — most people aren't — Timex is the cheapest way to start. The data is in a SQLite file you already understand. The queries are short. The graph is the strip in the Today view.
And on the practical side: the lid-down mode is the feature that makes overnight runs viable on a laptop. If you've ever woken up to a 30%-done training that should have been 100%-done, you already know why.
Run your next overnight job with Timex's clamshell mode on. The SQLite log will tell you in the morning whether it worked.