Искусство вычисления прошедшего времени в Ruby

Привет, коллеги-разработчики! Вы когда-нибудь задумывались о том, как вы считаете прошедшее время в Ruby? Если вы чем-то похожи на меня, вы, вероятно, не раз следовали этому шаблону:

starting = Time.now
# Some time-consuming operation
ending = Time.now
elapsed = ending - starting
elapsed # => 10.822178

Но держись! Здесь есть подвох, и все дело в том, как движется время. Приготовьтесь, потому что мы собираемся погрузиться в мир времени в Ruby и выясним, почему приведенный выше фрагмент не является идеальным способом измерения прошедшего времени.

Иллюзия линейного времени

Во-первых, давайте разберемся, что Time.now делает в Ruby. В зависимости от настроек вашей операционной системы, Ruby Time.now предназначен для использования функций Linux, таких как gettimeofday или clock_gettime из time.h.

gettimeofday возвращает количество секунд и микросекунд с начала эпохи, эффективно возвращая время стены в Linux. Однако здесь есть подвох:

На время, возвращаемое функцией gettimeofday(), влияют прерывистые скачки системного времени, в том числе ручная корректировка и автоматическое согласование системных часов.

Например, если ваш сервер использует сетевой протокол времени (NTP), эталонное время в идеале должно оставаться одинаковым во всем мире. Но даже NTP не гарантирует безупречной синхронизации.

В мире вычислений хронометраж — несовершенная наука, и физические условия процессора, такие как температура, давление воздуха и магнитные поля, влияют на эту точность. В результате вашей ОС, возможно, придется время от времени корректировать системное время. Примечательно, что эта корректировка может быть даже обратной, что приводит к сбоям в расчете прошедшего времени на основе времени стены.

Более того, некоторые ЦП с трудом справляются с дополнительными секундами, что приводит к аномалиям настенных часов.

Подводя итог, можно сказать, что использование системных часов для расчета прошедшего времени может привести к ошибкам или, в худшем случае, к сбоям в работе системы.

Правильный подход к измерению прошедшего времени

Итак, как мы можем ориентироваться в этом море переменных и точно вычислять прошедшее время? 🤔

Системы Posix имеют решение в виде монотонных часов. Этот таймер запускается с событием, и на него не влияют проблемы смещения времени. Он возвращает время, прошедшее с момента этого события, каждый раз, когда вы его вызываете.

К счастью, начиная с Ruby 2.1+ (MRI 2.1+ и JRuby 9.0.0.0+), в нашем распоряжении появился новый метод: Process.clock_gettime. Это позволяет нам получить доступ к текущим значениям различных типов часов, включая монотонные часы:

t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
# => 2810266.714992
t / (24 * 60 * 60.0) # time / days
# => 32.52623512722222

Здесь t представляет время в секундах с момента загрузки системы.

Теперь мы можем использовать это для более надежного измерения прошедшего времени:

starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
# time consuming operation
ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
elapsed = ending - starting
elapsed # => 9.183449000120163 seconds

Подведение итогов

Если вы работали в экосистеме Ruby, вас может удивить, что многочисленные расчеты прошедшего времени неточны из-за этого недоразумения. Итак, у вас есть прекрасная возможность сделать шаг вперед, внести свой вклад в сообщество открытого исходного кода 💚💎 и исправить эти проблемы.

И помните об этом фундаментальном