@@ -434,6 +434,8 @@ def __init__(self):
434434 self ._thread_id = None
435435 self ._clock_resolution = time .get_clock_info ('monotonic' ).resolution
436436 self ._exception_handler = None
437+ self ._settings = events .EventLoopSettings .DEFAULT
438+ self ._eager_state = constants .EAGER_IDLE
437439 self .set_debug (coroutines ._is_debug_mode ())
438440 # The preserved state of async generator hooks.
439441 self ._old_agen_hooks = None
@@ -667,6 +669,7 @@ def _run_forever_cleanup(self):
667669 loop cleanup logic.
668670 """
669671 self ._stopping = False
672+ self ._eager_state = constants .EAGER_IDLE
670673 self ._thread_id = None
671674 events ._set_running_loop (None )
672675 self ._set_coroutine_origin_tracking (False )
@@ -682,6 +685,8 @@ def run_forever(self):
682685 while True :
683686 self ._run_once ()
684687 if self ._stopping :
688+ if self ._eager_state >= 0 :
689+ self ._run_once ()
685690 break
686691 finally :
687692 self ._run_forever_cleanup ()
@@ -2009,10 +2014,13 @@ def _timer_handle_cancelled(self, handle):
20092014 def _run_once (self ):
20102015 """Run one full iteration of the event loop.
20112016
2012- This calls all currently ready callbacks, polls for I/O,
2013- schedules the resulting callbacks, and finally schedules
2014- 'call_later' callbacks.
2017+ This polls for I/O, schedules the resulting callbacks and the
2018+ 'call_later' callbacks that are due, and calls all ready
2019+ callbacks. Then it calls the callbacks scheduled by them
2020+ eagerly, within the limits of the loop settings.
20152021 """
2022+ left = self ._eager_state
2023+ nready = len (self ._ready )
20162024
20172025 sched_count = len (self ._scheduled )
20182026 if (sched_count > _MIN_SCHEDULED_TIMER_HANDLES and
@@ -2068,11 +2076,59 @@ def _run_once(self):
20682076
20692077 # This is the only place where callbacks are actually *called*.
20702078 # All other places just add them to ready.
2071- # Note: We run all currently scheduled callbacks, but not any
2072- # callbacks scheduled by callbacks run this time around --
2073- # they will be run the next time (after another I/O poll).
2079+ # Note: We run all currently scheduled callbacks. Callbacks
2080+ # scheduled by them are run eagerly in batches, within the limits
2081+ # of the loop settings; the rest will be run the next time (after
2082+ # another I/O poll).
20742083 # Use an idiom that is thread-safe without using locks.
2084+ self ._eager_state = constants .EAGER_IDLE
2085+ settings = self ._settings
2086+ if left >= 0 and self ._stopping :
2087+ # The final iteration after stop() during eager processing: run
2088+ # what the loop would run without eager processing, and keep the
2089+ # callbacks scheduled for later iterations queued.
2090+ left = min (left , nready )
2091+ nnew = len (self ._ready ) - nready
2092+ self ._run_handles (left , eager = False )
2093+ later = [self ._ready .popleft () for i in range (nready - left )]
2094+ self ._run_handles (nnew , eager = False )
2095+ self ._ready .extendleft (reversed (later ))
2096+ return
20752097 ntodo = len (self ._ready )
2098+ if left > 0 :
2099+ left = min (left , ntodo )
2100+ self ._run_handles (left , eager = False )
2101+ if self ._stopping :
2102+ return
2103+ ntodo -= left
2104+ left = len (self ._ready ) - ntodo
2105+ self ._run_handles (ntodo , eager = False )
2106+ if (not self ._ready or self ._eager_state == constants .EAGER_BREAK
2107+ or self ._stopping or settings .eager_max_time <= 0
2108+ or not settings .eager_max_callbacks ):
2109+ if left > 0 :
2110+ self ._eager_state = left
2111+ return
2112+
2113+ # Eager processing: run the callbacks scheduled by the callbacks
2114+ # above, in batches, within the limits of the loop settings.
2115+ deadline = (self .time () + settings .eager_max_time
2116+ + self ._clock_resolution )
2117+ remaining = settings .eager_max_callbacks
2118+ while True :
2119+ if left <= 0 :
2120+ left = len (self ._ready )
2121+ ntodo = min (left , remaining , constants .EAGER_BATCH_SIZE )
2122+ remaining -= ntodo
2123+ left -= self ._run_handles (ntodo , eager = True )
2124+ if (not remaining or not self ._ready
2125+ or self ._eager_state == constants .EAGER_BREAK
2126+ or self ._stopping or self .time () > deadline ):
2127+ break
2128+ if left > 0 or self ._stopping :
2129+ self ._eager_state = left
2130+
2131+ def _run_handles (self , ntodo , * , eager ):
20762132 for i in range (ntodo ):
20772133 handle = self ._ready .popleft ()
20782134 if handle ._cancelled :
@@ -2090,7 +2146,11 @@ def _run_once(self):
20902146 self ._current_handle = None
20912147 else :
20922148 handle ._run ()
2149+ if eager and self ._eager_state == constants .EAGER_BREAK :
2150+ ntodo = i + 1
2151+ break
20932152 handle = None # Needed to break cycles when an exception occurs.
2153+ return ntodo
20942154
20952155 def _set_coroutine_origin_tracking (self , enabled ):
20962156 if bool (enabled ) == bool (self ._coroutine_origin_tracking_enabled ):
@@ -2115,3 +2175,11 @@ def set_debug(self, enabled):
21152175
21162176 if self .is_running ():
21172177 self .call_soon_threadsafe (self ._set_coroutine_origin_tracking , enabled )
2178+
2179+ def get_settings (self ):
2180+ return self ._settings
2181+
2182+ def set_settings (self , settings ):
2183+ if not isinstance (settings , events .EventLoopSettings ):
2184+ raise TypeError ('settings must be EventLoopSettings instance' )
2185+ self ._settings = settings
0 commit comments