your timer is running on the UI, so will only block durning its callback. but your worker thread is 100% cpu bound and updates a UI component continuously, so these updates will block the UI thread.
if you don't want to use invoke, a common approach is a queue. the background thread inserts entries in a thread safe queue. the UI thread uses a timer to empty and process the queued items. In your case you would need to implement a debounce as there is minimum delay in loading the queue.
note: the timer uses the event loop to dispatch. but I suspect your sending so many UI update requests, there is no loop.
if you add a sleep to the background loop, I suspect your code will work.
while (_elapsed < 4010 && _running)
{
Console.WriteLine($"in while elapsed = {_elapsed}");
Thread.Sleep(10); // allow window.loop to run before next UI update
if (prog != null)
prog.Report(_elapsed);
}