@@ -1522,7 +1522,7 @@ def os_clients(all_hosts, all_client_options):
1522
1522
#
1523
1523
# Now we need to ensure that we start partitioning parameters correctly in both cases. And that means we
1524
1524
# need to start from (client) index 0 in both cases instead of 0 for indexA and 4 for indexB.
1525
- schedule = schedule_for (task , task_allocation . client_index_in_task , params_per_task [task ])
1525
+ schedule = schedule_for (task_allocation , params_per_task [task ])
1526
1526
async_executor = AsyncExecutor (
1527
1527
client_id , task , schedule , opensearch , self .sampler , self .cancel , self .complete ,
1528
1528
task .error_behavior (self .abort_on_error ), self .cfg )
@@ -1607,6 +1607,15 @@ async def __call__(self, *args, **kwargs):
1607
1607
# lazily initialize the schedule
1608
1608
self .logger .debug ("Initializing schedule for client id [%s]." , self .client_id )
1609
1609
schedule = self .schedule_handle ()
1610
+ self .schedule_handle .start ()
1611
+ rampup_wait_time = self .schedule_handle .ramp_up_wait_time
1612
+ if rampup_wait_time :
1613
+ self .logger .info ("client id [%s] waiting [%.2f]s for ramp-up." , self .client_id , rampup_wait_time )
1614
+ await asyncio .sleep (rampup_wait_time )
1615
+
1616
+ if rampup_wait_time :
1617
+ console .println (f" Client id { self .client_id } is running now." )
1618
+
1610
1619
self .logger .debug ("Entering main loop for client id [%s]." , self .client_id )
1611
1620
# noinspection PyBroadException
1612
1621
try :
@@ -1806,18 +1815,28 @@ def __repr__(self, *args, **kwargs):
1806
1815
1807
1816
1808
1817
class TaskAllocation :
1809
- def __init__ (self , task , client_index_in_task ):
1818
+ def __init__ (self , task , client_index_in_task , global_client_index , total_clients ):
1819
+ """
1820
+ :param task: The current task which is always a leaf task.
1821
+ :param client_index_in_task: The task-specific index for the allocated client.
1822
+ :param global_client_index: The globally unique index for the allocated client across
1823
+ all concurrently executed tasks.
1824
+ :param total_clients: The total number of clients executing tasks concurrently.
1825
+ """
1810
1826
self .task = task
1811
1827
self .client_index_in_task = client_index_in_task
1828
+ self .global_client_index = global_client_index
1829
+ self .total_clients = total_clients
1812
1830
1813
1831
def __hash__ (self ):
1814
- return hash (self .task ) ^ hash (self .client_index_in_task )
1832
+ return hash (self .task ) ^ hash (self .global_client_index )
1815
1833
1816
1834
def __eq__ (self , other ):
1817
- return isinstance (other , type (self )) and self .task == other .task and self .client_index_in_task == other .client_index_in_task
1835
+ return isinstance (other , type (self )) and self .task == other .task and self .global_client_index == other .global_client_index
1818
1836
1819
1837
def __repr__ (self , * args , ** kwargs ):
1820
- return "TaskAllocation [%d/%d] for %s" % (self .client_index_in_task , self .task .clients , self .task )
1838
+ return f"TaskAllocation [{ self .client_index_in_task } /{ self .task .clients } ] for { self .task } " \
1839
+ f"and [{ self .global_client_index } /{ self .total_clients } ] in total"
1821
1840
1822
1841
1823
1842
class Allocator :
@@ -1858,12 +1877,16 @@ def allocations(self):
1858
1877
clients_executing_completing_task = []
1859
1878
for sub_task in task :
1860
1879
for client_index in range (start_client_index , start_client_index + sub_task .clients ):
1861
- # this is the actual client that will execute the task. It may differ from the logical one in case we over-commit (i.e.
1862
- # more tasks than actually available clients)
1863
1880
physical_client_index = client_index % max_clients
1864
1881
if sub_task .completes_parent :
1865
1882
clients_executing_completing_task .append (physical_client_index )
1866
- allocations [physical_client_index ].append (TaskAllocation (sub_task , client_index - start_client_index ))
1883
+ ta = TaskAllocation (task = sub_task ,
1884
+ client_index_in_task = client_index - start_client_index ,
1885
+ global_client_index = client_index ,
1886
+ # if task represents a parallel structure this is the total number of clients
1887
+ # executing sub-tasks concurrently.
1888
+ total_clients = task .clients )
1889
+ allocations [physical_client_index ].append (ta )
1867
1890
start_client_index += sub_task .clients
1868
1891
1869
1892
# uneven distribution between tasks and clients, e.g. there are 5 (parallel) tasks but only 2 clients. Then, one of them
@@ -1941,7 +1964,7 @@ def clients(self):
1941
1964
1942
1965
# Runs a concrete schedule on one worker client
1943
1966
# Needs to determine the runners and concrete iterations per client.
1944
- def schedule_for (task , client_index , parameter_source ):
1967
+ def schedule_for (task_allocation , parameter_source ):
1945
1968
"""
1946
1969
Calculates a client's schedule for a given task.
1947
1970
@@ -1951,15 +1974,17 @@ def schedule_for(task, client_index, parameter_source):
1951
1974
:return: A generator for the operations the given client needs to perform for this task.
1952
1975
"""
1953
1976
logger = logging .getLogger (__name__ )
1977
+ task = task_allocation .task
1954
1978
op = task .operation
1955
- num_clients = task .clients
1956
1979
sched = scheduler .scheduler_for (task )
1980
+
1981
+ client_index = task_allocation .client_index_in_task
1957
1982
# guard all logging statements with the client index and only emit them for the first client. This information is
1958
1983
# repetitive and may cause issues in thespian with many clients (an excessive number of actor messages is sent).
1959
1984
if client_index == 0 :
1960
1985
logger .info ("Choosing [%s] for [%s]." , sched , task )
1961
1986
runner_for_op = runner .runner_for (op .type )
1962
- params_for_op = parameter_source .partition (client_index , num_clients )
1987
+ params_for_op = parameter_source .partition (client_index , task . clients )
1963
1988
if hasattr (sched , "parameter_source" ):
1964
1989
if client_index == 0 :
1965
1990
logger .debug ("Setting parameter source [%s] for scheduler [%s]" , params_for_op , sched )
@@ -1992,7 +2017,7 @@ def schedule_for(task, client_index, parameter_source):
1992
2017
else :
1993
2018
logger .info ("%s schedule will determine when the schedule for [%s] terminates." , str (loop_control ), task .name )
1994
2019
1995
- return ScheduleHandle (task . name , sched , loop_control , runner_for_op , params_for_op )
2020
+ return ScheduleHandle (task_allocation , sched , loop_control , runner_for_op , params_for_op )
1996
2021
1997
2022
1998
2023
def requires_time_period_schedule (task , task_runner , params ):
@@ -2009,27 +2034,40 @@ def requires_time_period_schedule(task, task_runner, params):
2009
2034
2010
2035
2011
2036
class ScheduleHandle :
2012
- def __init__ (self , task_name , sched , task_progress_control , runner , params ):
2037
+ def __init__ (self , task_allocation , sched , task_progress_control , runner , params ):
2013
2038
"""
2014
2039
Creates a generator that will yield individual task invocations for the provided schedule.
2015
2040
2016
- :param task_name : The name of the task for which the schedule is generated.
2041
+ :param task_allocation : The task allocation for which the schedule is generated.
2017
2042
:param sched: The scheduler for this task.
2018
2043
:param task_progress_control: Controls how and how often this generator will loop.
2019
2044
:param runner: The runner for a given operation.
2020
2045
:param params: The parameter source for a given operation.
2021
2046
:return: A generator for the corresponding parameters.
2022
2047
"""
2023
- self .task_name = task_name
2048
+ self .task_allocation = task_allocation
2024
2049
self .sched = sched
2025
2050
self .task_progress_control = task_progress_control
2026
2051
self .runner = runner
2027
2052
self .params = params
2028
2053
# TODO: Can we offload the parameter source execution to a different thread / process? Is this too heavy-weight?
2029
- #from concurrent.futures import ThreadPoolExecutor
2030
- #import asyncio
2031
- #self.io_pool_exc = ThreadPoolExecutor(max_workers=1)
2032
- #self.loop = asyncio.get_event_loop()
2054
+ # from concurrent.futures import ThreadPoolExecutor
2055
+ # import asyncio
2056
+ # self.io_pool_exc = ThreadPoolExecutor(max_workers=1)
2057
+ # self.loop = asyncio.get_event_loop()
2058
+ @property
2059
+ def ramp_up_wait_time (self ):
2060
+ """
2061
+ :return: the number of seconds to wait until this client should start so load can gradually ramp-up.
2062
+ """
2063
+ ramp_up_time_period = self .task_allocation .task .ramp_up_time_period
2064
+ if ramp_up_time_period :
2065
+ return ramp_up_time_period * (self .task_allocation .global_client_index / self .task_allocation .total_clients )
2066
+ else :
2067
+ return 0
2068
+
2069
+ def start (self ):
2070
+ self .task_progress_control .start ()
2033
2071
2034
2072
def before_request (self , now ):
2035
2073
self .sched .before_request (now )
@@ -2041,20 +2079,18 @@ async def __call__(self):
2041
2079
next_scheduled = 0
2042
2080
if self .task_progress_control .infinite :
2043
2081
param_source_knows_progress = hasattr (self .params , "percent_completed" )
2044
- self .task_progress_control .start ()
2045
2082
while True :
2046
2083
try :
2047
2084
next_scheduled = self .sched .next (next_scheduled )
2048
2085
# does not contribute at all to completion. Hence, we cannot define completion.
2049
2086
percent_completed = self .params .percent_completed if param_source_knows_progress else None
2050
- #current_params = await self.loop.run_in_executor(self.io_pool_exc, self.params.params)
2087
+ # current_params = await self.loop.run_in_executor(self.io_pool_exc, self.params.params)
2051
2088
yield (next_scheduled , self .task_progress_control .sample_type , percent_completed , self .runner ,
2052
2089
self .params .params ())
2053
2090
self .task_progress_control .next ()
2054
2091
except StopIteration :
2055
2092
return
2056
2093
else :
2057
- self .task_progress_control .start ()
2058
2094
while not self .task_progress_control .completed :
2059
2095
try :
2060
2096
next_scheduled = self .sched .next (next_scheduled )
0 commit comments