import time _clock = time.perf_counter _tick = 1/60 #import platform import queue import json from ._notebook_helpers import _isnotebook if _isnotebook: import IPython import ipykernel ws_queue = queue.Queue() # Unresolved bug: rate(X) yields only about 0.8X iterations per second. MIN_RENDERS = 10 MAX_RENDERS = 60 INTERACT_PERIOD = 1.0/MAX_RENDERS USER_FRACTION = 0.5 ##_plat = platform.system() ##if _plat == 'Windows': ## # On Windows, the best timer is supposedly time.clock() ## _clock = time.clock ## _tick = 1/INTERACT_PERIOD ##elif _plat == 'Macintosh': ## # On platforms other than Windows, the best timer is supposedly time.time() ## _clock = time.time ## _tick = 0.01 ##else: # 'Unix' ## # On platforms other than Windows, the best timer is supposedly time.time() ## _clock = time.time ## _tick = 0.01 # though sleep seems to be accurate at the 1 millisecond level ##Possible way to get one-millisecond accuracy in sleep on Windows: ##http://msdn.microsoft.com/en-us/library/windows/desktop/ms686298(v=vs.85).aspx ##When your program starts, the Windows system's timer resolution has a ##seemingly random value that depends on which programs are running ##(and apparently, which programs were run and then exited). ##Common values for the resolution are 15 ms and 1 ms, but a range ##of values is possible (use timeGetDevCaps to determine this range). ##AFAICT, calling timeBeginPeriod() changes the system timer resolution ##for every call you make to a Win32 function with a timeout ##(e.g., MsgWaitForMultipleObjects() works exactly the same as Sleep() ##with respect to the timeout) and every call that every other application ##in the system makes to a Win32 function with a timeout. def _sleep(dt): # Windows sleep is quantized in multiples of 1/60 second. # Moreover, time.sleep can be quite inaccurate on Windows, # hence the use of the clock here to check time.sleep. if dt >= _tick: nticks = int(dt/_tick) dtsleep = nticks*_tick t = _clock() time.sleep(dtsleep) t = _clock()-t dt -= t if dt <= 0.0: return tend = _clock()+dt while _clock() < tend: pass class simulateDelay: """ Simulate rendering/compute times.. with an average value of delayAvg with a variance of something like delaySigma**2. """ def __init__(self, delayAvg=0.001, delaySigma=0.0001): self.delayAvg=delayAvg self.delaySigma=delaySigma self.callTimes = [] def __call__(self): self.callTimes.append(_clock()) class RateKeeper(object): def __init__(self, interactPeriod=INTERACT_PERIOD, interactFunc=simulateDelay): self.interactionPeriod = interactPeriod self.interactFunc = interactFunc self.initialized = False def initialize(self): self.delay = 0.0 self.userTime = 0.0 self.renderTime = 0.0 self.callTime = 0.0 self.count = 0 self.lastCount = 0 # value of self.count at start of a 1-second series self.rateCalls = 0 # number of calls to rate function before starting a new 1-sec series self.calls = 0 # number of calls to rate since since last render or start of 1-sec series self.lastSleep = self.start = _clock() # List of which calls to rate in a 1-second cycle should do a render: self.whenToRender = [] for i in range(MAX_RENDERS+2): self.whenToRender.append(0) self.renderIndex = 0 self.rateCount = 0 # counts calls to rate in a 1-second cycle (reset to 0 every second) def callInteract(self): t = _clock() self.interactFunc() dt = _clock() - t if self.count == 1: self.renderTime = 0.005 # first value is abnormal; make small nonzero elif self.count == 2: self.renderTime = dt # we now have a measure of actual loop render time elif dt < 0.2: # don't count long delays due to menu or similar operations self.renderTime = 0.95*self.renderTime + 0.05*dt # time spent in render code def distributeRenders(self, M, N): self.renderIndex = 0 self.rateCount = 0 self.renderWaits = 0 r = M/N x = 0.0 j = 0 for i in range(M): waits = 0 while i+1 > x: self.whenToRender[j] = i waits += 1 j += 1 x += r if waits > 0: self.renderWaits += waits-1 if i == M-1: self.whenToRender[j] = -1 def buildStrategy(self, rate): self.lastCount = self.count U = self.userTime R = self.renderTime M = int(rate) # M is number of user iterations/second N = int(1/self.interactionPeriod) # N is number of renders/second if M*U + N*R > 1: if M*U + MIN_RENDERS*R <= 1: N = int((1 - M*U)/R) elif MIN_RENDERS*R < 1-USER_FRACTION: N = MIN_RENDERS M = int((1 - N*R)/U) else: M = int(USER_FRACTION/U) N = int((1-USER_FRACTION)/R) if N > MAX_RENDERS: N = MAX_RENDERS if M < 1: M = 1 if N < 1: N = 1 self.rateCalls = M # Prepare the self.renderIndex array of indices for when to do renders: self.distributeRenders(M, N) # M = self.rateCalls = number of calls to rate/second # N = number of renders/second # callTime = time spent in rate function (very small) # waits = number of interact delays/second (due to multiple renders in a long slice) # T = self.interactionPeriod # M*(U + callTime + delay) + N*R + waits*T = 1 leads to the following delay # to be applied when there is no render when rate() is called: self.delay = (1.0 - N*R - self.renderWaits*self.interactionPeriod)/M - self.callTime - U ## print("%1.4f %i %i %i %1.6f %1.6f %1.6f %1.6f" % (_clock(), M, N, self.renderWaits, ## self.userTime, self.callTime, self.delay, self.renderTime)) def __call__(self, maxRate=100): #td.add('-------------------------') if not self.initialized: self.initialize() self.initialized = True calledTime = _clock() if maxRate < 1: raise ValueError("rate value must be greater than or equal to 1") self.count += 1 if self.count == 1: # first time rate has been called self.callInteract() self.lastEndRate = _clock() return dt = calledTime - self.lastEndRate # time spent in user code nr = self.whenToRender[self.renderIndex] if self.count == 2 or (self.count == self.lastCount + self.rateCalls): self.userTime = dt # the first time we have a user code time is self.count == 2 if self.calls > 0: # if there were some calls to rate after the last render dt = self.lastSleep + self.calls*(self.userTime + self.callTime + self.delay) - _clock() _sleep(dt) self.buildStrategy(maxRate) nr = self.whenToRender[0] self.calls = 0 self.lastSleep = _clock() elif dt < 0.2: # don't count long delays due to menu or similar operations self.userTime = 0.95*self.userTime + 0.05*dt dt = _clock() - calledTime # approximate amount of time spent in this function if self.callTime == 0.0: self.callTime = dt elif dt < 0.2: # don't count long delays due to menu or similar operations self.callTime = 0.95*self.callTime + 0.05*dt self.calls += 1 if nr == self.rateCount: # There is one or more render associated with this call to rate renders = sleeps = 0 while True: renders += 1 self.callInteract() self.renderIndex += 1 if self.whenToRender[self.renderIndex] == self.rateCount: sleeps += 1 _sleep(self.interactionPeriod) else: break # Determine how much time is left before the next predicted call to rate: dt = self.lastSleep + self.calls*(self.userTime + self.callTime + self.delay) + \ renders*self.renderTime + sleeps*self.interactionPeriod - _clock() _sleep(dt) self.lastSleep = _clock() self.calls = 0 self.rateCount += 1 self.lastEndRate = _clock() def message_send_wrapper(): """ The only purpose of this function is delay import of baseObj to eliminate what would otherwise be a circular import. __init__ imports rate, and vpython imports rate, and this cannot also import vpython at the same time. """ from .vpython import baseObj def message_sender(msg): baseObj.glow.handle_msg(msg) return message_sender class _RateKeeper2(RateKeeper): def __init__(self, interactPeriod=INTERACT_PERIOD, interactFunc=simulateDelay): self.rval = MAX_RENDERS self.tocall = None self._sender = None super(_RateKeeper2, self).__init__(interactPeriod=interactPeriod, interactFunc=self.sendtofrontend) def sendtofrontend(self): # This is called by the rate() function, through rate_control _RateKeeper callInteract(). # See the function commsend() for details of how the browser is updated. # Check if events to process from front end if _isnotebook: if not self._sender: self._sender = message_send_wrapper() while ws_queue.qsize() > 0: data = ws_queue.get() d = json.loads(data) # update_canvas info for m in d: # Must send events one at a time to GW.handle_msg because bound events need the loop code: msg = {'content':{'data':[m]}} # message format used by notebook self._sender(msg) def __call__(self, N): # rate(N) calls this function self.rval = N if self.rval < 1: raise ValueError("rate value must be greater than or equal to 1") super(_RateKeeper2, self).__call__(self.rval) ## calls __call__ in rate_control.py # The rate function: rate = _RateKeeper2(interactFunc = simulateDelay(delayAvg = 0.001))