123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455(* Time stamp counter
==================
This module tries to estimate time based on the CPU time stamp counter (TSC). The time
estimates reported by this module are monotonically increasing. It uses [Time.now ()]
as its measure of "real time" to do this.
Historically, the rate of increment of the TSC (sometimes referred to as the TSC
frequency) varied based of CPU overclocking, temperature, load etc. On modern Intel
CPU's the TSC is expected to be stable. On Linux systems, the "constant_tsc" in
/proc/cpuinfo indicates that the machine has a stable TSC rate. While this module
assumes that the TSC is relatively stable, it can adapt to small variations in the TSC
frequency.
Simple Overview
===============
Here is an explanation of how this module works. The module measures the change in
real time and the change in TSC at every calibration call and maintains an EWMA of
these deltas. It then uses the EWMA values to do linear regression where time is the
estimated value and TSC is the predictor. The linear regression done at every
calibration step produces an updated time/tsc slope. Using this time/tsc slope and
the latest value of real time, the module estimates time in terms of tsc.
Ensuring Monotonicity of Time
=============================
The simple picture above is complicated by the presence of noise. There are two
significant sources of noise. The first is the noise caused by variations in the
frequency of TSC. The second, and probably the more significant one, is noise in real
time, i.e. noise in the [Time.now ()] call.
(1) [Time.now ()] calls suffer from the overhead of transition from the user program to
a kernel vdso and
(2) It is affected by NTP updates.
(3) Another significant source of error comes from loss of precision. [Time.now]
reports a 64-bit float of which it has 52 bits of mantissa. The 52 bits of mantissa
for time in seconds from Unix epoch only allows for precision in the order of
micro-seconds. Consequently the measurement of time using [Time.now] can only be
precise in the order of micro-seconds.
Noise in measuring real time and in the rate of time/tsc implies that at each
calibration point the estimated time "jumps" up or down with respect to the estimate
value of time before calibration. In other words, the time estimated using the EWMA
linear regression is not strictly monotonic.
We report a monotonic time in terms of the estimated time, by maintaining a separate
slope called the "monotonic time/TSC slope". At every calibration point, we take the
last estimated time and adjust the monotonic time/TSC slope such that it catches up to
the estimated time in a fixed number of cycles. If the expected change in slope is too
high, we bound the rate of change of the monotonic time/TSC slope. As long as
monotonic time has not caught up with the estimated time we report time in terms of the
adjusted monotonic time slope. Once we have caught up to the estimated time, we start
reporting the estimated time.
We can chose the number of cycles to allow for catchup to be any number we wish. A
number in the order of 1E6-1E9 TSC steps allows for a gradual catchup rate without too
many abrupt changes in the rate of reported time. The bound to rate of change is
expressed in percentage terms of slope and is at max the ratio by which we expect the
underlying TSC frequency to change on the machine. It is defined as
[max_perc_change_from_real_slope] below.
It is worth noting that the approximation of the monotonic slope trying to catch up
with the estimate slope can be achieved in many other ways. A more principled approach
to this would be to use a PID controller that adapts to error and gets the reported
monotonic time to smoothly fit the estimated time. However PID controllers are
computationally more expensive and we use a simpler linear approximation.
*)[%%import"config.h"]open!ImportmoduleUnix=Core_unixletmax_percent_change_from_real_slope=0.20let()=assert(0.<=max_percent_change_from_real_slope);assert(max_percent_change_from_real_slope<=1.);;letewma~alpha~old~add=((1.-.alpha)*.old)+.(alpha*.add)typet=Int63.t[@@derivingbin_io,compare,sexp]typetsc=t[@@derivingbin_io,compare,sexp]include(Int63:Comparisons.Swithtypet:=t)letdifft1t2=Int63.(-)t1t2letaddts=Int63.(+)tsletof_int63t=tletto_int63t=t[%%ifdefJSC_ARCH_SIXTYFOUR](* noalloc on x86_64 only *)externalnow:unit->tsc="tsc_get"[@@noalloc]moduleCalibrator=struct(* performance hack: prevent writes to this record from boxing floats by making all
fields mutable floats *)typefloat_fields=(* the most recent observations and regression results *){mutabletime:float;mutablesec_per_cycle:float(* mutable sec_error_intercept : float; *)(* this time value is monotonically increasing *);mutablemonotonic_time:float;mutablemonotonic_sec_per_cycle:float(* for linear regression *);mutableewma_time_tsc:float;mutableewma_tsc_square:float;mutableewma_time:float;mutableewma_tsc:float(* for computing time in nanos *);mutablenanos_per_cycle:float;mutablemonotonic_nanos_per_cycle:float}[@@derivingbin_io,sexp]typet=(* the most recent observations and regression results *){mutabletsc:tsc(* this time value is monotonically increasing *);mutablemonotonic_until_tsc:tsc(* for computing time in nanos *);mutabletime_nanos:Int63.t;mutablemonotonic_time_nanos:Int63.t;floats:float_fields}[@@derivingbin_io,fields,sexp]lettsc_to_seconds_since_epoch=let[@inline]convertttscbasemul=base+.(mul*.Int63.to_float(difftsct.tsc))infun[@inline]ttsc->0.+.(* performance hack: stops float boxing *)(iftsc<t.monotonic_until_tscthenbegin0.+.(* performance hack: stops float boxing *)convertttsct.floats.monotonic_timet.floats.monotonic_sec_per_cycleendelsebegin0.+.(* performance hack: stops float boxing *)convertttsct.floats.timet.floats.sec_per_cycleend);;lettsc_to_nanos_since_epoch=letconvertttscbasemul=(* Scale an int by a float without intermediate allocation and overflow. *)Int63.(+)base(Float.int63_round_nearest_exn(mul*.Int63.to_float(difftsct.tsc)))infunttsc->iftsc<t.monotonic_until_tscthenconvertttsct.monotonic_time_nanost.floats.monotonic_nanos_per_cycleelseconvertttsct.time_nanost.floats.nanos_per_cycle;;(* The rate of response to the variations in TSC frequency can be controlled via alpha.
Alpha should be in (0,1] and controls the decay of the subsequent EWMA calculation.
A low number such as 0.01 suggests that the TSC is largely stable and small
variations should be treated as noise. Setting this number to 0.6 or higher
indicates that each new measurement of the TSC should significantly outweigh past
measurements which has the effect of making time calibration more responsive to
frequency changes. In this module we have chosen a value of alpha that varies with
the duration of time, i.e. longer time samples are given more weight and shorter time
samples are given lesser weight. *)letalpha_for_intervaltime_diff=0.+.(Float.max0.(1.-.exp(-0.5*.time_diff)));;letcatchup_cycles=1E9letinitial_alpha=1.(* performance hack: This function is the same as
{[
match Float.iround_up float with
| None -> if_iround_up_fails
| Some i -> Int63.(+) int i
]}
but I couldn't find a way to make the simple version stop allocating, even with
flambda turned on *)letiround_up_and_addint~if_iround_up_failsfloat=ifFloat.(>)float0.0thenbeginletfloat'=Caml.ceilfloatinifFloat.(<=)float'(Float.iround_ubound)thenInt63.(+)int(Int63.of_float_uncheckedfloat')elseif_iround_up_failsendelsebeginifFloat.(>=)floatFloat.iround_lboundthenInt63.(+)int(Int63.of_float_uncheckedfloat)elseif_iround_up_failsendlet[@inline]calibrate_usingt~tsc~time~am_initializing=letestimated_time=0.+.(* performance hack: stops float boxing *)tsc_to_seconds_since_epochttscinlettime_diff_est=time-.estimated_timeinlettime_diff=time-.t.floats.timeinlettsc_diff=Int63.to_float(difftsct.tsc)inletalpha=ifam_initializingtheninitial_alphaelsealpha_for_intervaltime_diffin(* update current times *)t.floats.time<-time;t.tsc<-tsc;(* update ewma and regression. *)t.floats.ewma_time_tsc<-ewma~alpha~old:t.floats.ewma_time_tsc~add:(tsc_diff*.time_diff);t.floats.ewma_tsc_square<-ewma~alpha~old:t.floats.ewma_tsc_square~add:(tsc_diff*.tsc_diff);t.floats.ewma_tsc<-ewma~alpha~old:t.floats.ewma_tsc~add:tsc_diff;t.floats.ewma_time<-ewma~alpha~old:t.floats.ewma_time~add:time_diff;(* linear regression *)t.floats.sec_per_cycle<-t.floats.ewma_time_tsc/.t.floats.ewma_tsc_square;(* t.sec_error_intercept <- t.ewma_time -. t.sec_per_cycle *. t.ewma_tsc; *)(* monotonic predicted time and slope. *)t.floats.monotonic_time<-estimated_time;ifnotam_initializingthenbeginletcatchup_sec_per_cycle=(* The slope so that after [catchup_cycles], the monotonic estimated time equals
the estimated time, i.e. solve for [monotonic_sec_per_cycle] in:
{[
t.monotonic_time + monotonic_sec_per_cycle * catchup_cycles
= t.time + t.sec_per_cycle * catchup_cycles
]}
Note that [time_diff_est = t.time - t.monotonic_time]. *)t.floats.sec_per_cycle+.(time_diff_est/.catchup_cycles)int.floats.monotonic_sec_per_cycle<-ifFloat.is_positivetime_diff_estthenbegin0.+.(* performance hack: stops float boxing *)Float.mincatchup_sec_per_cycle(t.floats.sec_per_cycle*.(1.+.max_percent_change_from_real_slope))endelsebegin0.+.(* performance hack: stops float boxing *)Float.maxcatchup_sec_per_cycle(t.floats.sec_per_cycle*.(1.-.max_percent_change_from_real_slope))end;(* Compute the number of cycles in the future at which monotonic estimated time
equals estimated time, i.e. solve for [cycles] in:
{[
t.monotonic_time + t.monotonic_sec_per_cycle * cycles
= t.time + t.sec_per_cycle * cycles
]}
This value might get very small when the two slopes are about the same. In such
cases we just use the estimated slope always. *)t.monotonic_until_tsc<-(time_diff_est/.(t.floats.monotonic_sec_per_cycle-.t.floats.sec_per_cycle))|>iround_up_and_addtsc~if_iround_up_fails:Int63.zero;end;(* Precompute values required for [tsc_to_nanos_since_epoch]. *)t.time_nanos<-Float.int63_round_nearest_exn(t.floats.time*.1E9);t.floats.nanos_per_cycle<-t.floats.sec_per_cycle*.1E9;t.monotonic_time_nanos<-Float.int63_round_nearest_exn(t.floats.monotonic_time*.1E9);t.floats.monotonic_nanos_per_cycle<-t.floats.monotonic_sec_per_cycle*.1E9;;;letnow_float()=1E-9*.Int.to_float(Time_ns.to_int_ns_since_epoch(Time_ns.now()))letinitializetsamples=List.itersamples~f:(fun(tsc,time)->calibrate_usingt~tsc~time~am_initializing:true);;;letcollect_samples~num_samples~interval=assert(Int.(>=)num_samples1);(* We sleep at differing intervals to improve the estimation of [sec_per_cycle]. *)letrecloopnsleep=letsample=(now(),now_float())inifInt.(=)n1then[sample]elsebeginignore(Unix.nanosleepsleep);sample::loop(n-1)(sleep+.interval)endinloopnum_samplesinterval;;letcreate_using~tsc~time~samples=lett={monotonic_until_tsc=Int63.zero;tsc=tsc;time_nanos=Int63.zero;monotonic_time_nanos=Int63.zero;floats={monotonic_time=time;sec_per_cycle=0.;monotonic_sec_per_cycle=0.;time=time;ewma_time_tsc=0.;ewma_tsc_square=0.;ewma_time=0.;ewma_tsc=0.;nanos_per_cycle=0.;monotonic_nanos_per_cycle=0.}}ininitializetsamples;tletcreate()=lettime=now_float()inlettsc=now()inletsamples=collect_samples~num_samples:3~interval:0.0005increate_using~tsc~time~samples;;(* Creating a calibrator takes about 3ms. *)lett=lazy(create())letcpu_mhz=Ok(funt->1./.(t.floats.sec_per_cycle*.1E6))(* performance hack: [@inline never] so [time] is always unboxed. [now_float] and
[calibrate_using] need to be inlined into the same function for unboxed [time].
Preventing [calibrate] from being inlined makes the compiler's inlining decision
more predictable. *)let[@inlinenever]calibratet=calibrate_usingt~tsc:(now())~time:(now_float())~am_initializing:false;;modulePrivate=structletcreate_using=create_usingletcalibrate_using=calibrate_usingletinitialize=initializeletnanos_per_cyclet=t.floats.nanos_per_cycleendend[%%else](* noalloc on x86_64 only *)externalnow:unit->tsc="tsc_get"(* Outside of x86_64, [now] returns the result of clock_gettime(), i.e. the current time
in nanos past epoch. *)moduleCalibrator=structtypet=unit[@@derivingbin_io,sexp]lettsc_to_seconds_since_epoch_ttsc=Int63.to_floattsc*.1e-9lettsc_to_nanos_since_epoch_ttsc=tscletcreate_using~tsc:_~time:_~samples:_=()letcreate()=()letinitialize_t_samples=()letcalibrate_using_t~tsc:_~time:_~am_initializing:_=()letcalibrate_=()lett=lazy(create())letcpu_mhz=Or_error.unimplemented"\
Time_stamp_counter.Calibrator.cpu_mhz is not defined for 32-bit platforms";;modulePrivate=structletcreate_using=create_usingletcalibrate_using=calibrate_usingletinitialize=initializeletnanos_per_cycle_=1.endend[%%endif]moduleSpan=structinclude(Int63:(moduletypeofstructincludeInt63endwithmodulePrivate:=Int63.Private))modulePrivate=structletof_int63t=tletto_int63t=tend[%%ifdefJSC_ARCH_SIXTYFOUR]letto_nst~(calibrator:Calibrator.t)=Float.int63_round_nearest_exn(Int63.to_floatt*.calibrator.floats.nanos_per_cycle);;(* If the calibrator has not been well calibrated and [ns] is a large value, the
following can overflow. This happens rarely in hydra in a way that difficult to
reproduce. We've improved the exn here so that we have more information to debug
these spurious errors when they come up. *)letof_nsns~(calibrator:Calibrator.t)=tryFloat.int63_round_nearest_exn(Int63.to_floatns/.calibrator.floats.nanos_per_cycle)withexn->raise_s[%message""~_:(exn:Exn.t)(calibrator:Calibrator.t)];;[%%else](* [tsc_get] already returns the current time in ns *)letto_nst~calibrator:_=tletof_nsns~calibrator:_=ns[%%endif]letto_time_spant~calibrator=Time.Span.of_ns(Int63.to_float(to_nst~calibrator));;endletcalibrator=Calibrator.tletto_timet~calibrator=Calibrator.tsc_to_seconds_since_epochcalibratort|>Time.Span.of_sec|>Time.of_span_since_epochletto_nanos_since_epocht~calibrator=Calibrator.tsc_to_nanos_since_epochcalibratort;;;letto_time_nst~calibrator=Time_ns.of_int63_ns_since_epoch(to_nanos_since_epoch~calibratort);;modulePrivate=structletewma=ewmaletof_int63=of_int63letmax_percent_change_from_real_slope=max_percent_change_from_real_slopeletto_nanos_since_epoch=to_nanos_since_epochend