At src/qt/modaloverlaycpp, around line 110:
progressDelta = blockProcessTime[0].second - sample.second;
timeDelta = blockProcessTime[0].first - sample.first;
progressPerHour = progressDelta / (double) timeDelta * 1000 * 3600;
remainingMSecs = (progressDelta > 0) ? remainingProgress / progressDelta * timeDelta : -1;
The progress per hour and time remaining is updated every 500 milliseconds, or if the same block is still the latest one processed after 500 milliseconds.
The progress and time deltas are calculated by subtracting the last processed block's time and progress by the second last processed block's time and progress (progress per hour is only displayed if there is at least two blocks processed),
then the progress per hour divides the progressDelta by timeDelta and then converts from milliseconds to hours.
As a bonus, it also calculates the remaining milliseconds from these two last blocks (which explains why the remaining time fluctuates wildly with the connection speed) by dividing the remaining progress (calculated above as 1 - progress), by the progress delta and then multiplying that by the timedelta.