1818import math
1919import sys
2020
21- # Define set of long-running unit tests to run in isolated shard
22- ISOLATED_PACKAGES = {
23- "google-cloud-compute" ,
24- "google-cloud-compute-v1beta" ,
25- "google-cloud-dialogflow" ,
26- "google-cloud-dialogflow-cx" ,
21+ # Define weights for long-running packages to balance shard execution time.
22+ # Default weight for any package not listed is 1.
23+ PACKAGE_WEIGHTS = {
24+ "bigframes" : 6 ,
25+ "google-auth" : 5 ,
26+ "google-cloud-compute" : 12 ,
27+ "google-cloud-compute-v1beta" : 12 ,
28+ "google-cloud-dialogflow" : 6 ,
29+ "google-cloud-dialogflow-cx" : 6 ,
30+ "google-cloud-discoveryengine" : 8 ,
31+ "google-cloud-retail" : 5 ,
2732}
2833
2934
@@ -38,6 +43,7 @@ def get_packages():
3843 packages .extend (sorted (pkg_dirs ))
3944 return packages
4045
46+
4147def get_packages_to_test ():
4248 build_type = os .environ .get ('BUILD_TYPE' , 'presubmit' )
4349 target_branch = os .environ .get ('TARGET_BRANCH' , 'main' )
@@ -74,81 +80,72 @@ def group_packages(packages):
7480 if not packages :
7581 return []
7682
77- isolated_to_test = []
78- normal_to_test = []
79-
83+ # Map packages to their weights
84+ package_weights = []
85+ total_weight = 0
8086 for pkg in packages :
8187 pkg_name = pkg .strip ('/' ).split ('/' )[- 1 ]
82- if pkg_name in ISOLATED_PACKAGES :
83- isolated_to_test .append (pkg )
84- else :
85- normal_to_test .append (pkg )
88+ weight = PACKAGE_WEIGHTS .get (pkg_name , 1 )
89+ package_weights .append ((pkg , weight ))
90+ total_weight += weight
91+
92+ # Determine number of shards based on total weight
93+ # 1. Base shard count on weight capacity of 10 per shard
94+ num_shards = math .ceil (total_weight / 10 )
95+
96+ # Ensure at least 1 shard if we have packages
97+ num_shards = max (1 , num_shards )
98+
99+ # 2. Top out at 16 shards
100+ num_shards = min (16 , num_shards )
101+
102+ # Initialize shards as empty lists of packages with their current total weight
103+ shard_buckets = [{"packages" : [], "weight" : 0 } for _ in range (num_shards )]
104+
105+ # Sort packages descending by weight (LPT heuristic)
106+ # If weights are equal, sort alphabetically for stability
107+ sorted_packages = sorted (package_weights , key = lambda x : (- x [1 ], x [0 ]))
86108
109+ # Distribute greedily to the bucket with the minimum current weight
110+ for pkg , weight in sorted_packages :
111+ min_bucket = min (shard_buckets , key = lambda b : b ["weight" ])
112+ min_bucket ["packages" ].append (pkg )
113+ min_bucket ["weight" ] += weight
114+
115+ # Construct the final shards output list
87116 shards = []
88117 index = 1
118+ for bucket in shard_buckets :
119+ shard_packages = bucket ["packages" ]
120+ if not shard_packages :
121+ continue
122+ name = f"Shard { index } "
123+ num_in_shard = len (shard_packages )
124+
125+ # Sort packages alphabetically for a cleaner description
126+ sorted_shard_pkgs = sorted (shard_packages )
127+ if len (sorted_shard_pkgs ) == 1 :
128+ desc = sorted_shard_pkgs [0 ].strip ('/' ).split ('/' )[- 1 ]
129+ else :
130+ desc = f"{ sorted_shard_pkgs [0 ].strip ('/' ).split ('/' )[- 1 ]} ...{ sorted_shard_pkgs [- 1 ].strip ('/' ).split ('/' )[- 1 ]} ({ num_in_shard } packages)"
89131
90- # Add isolated packages to their own shards
91- for pkg in isolated_to_test :
92- pkg_name = pkg .strip ('/' ).split ('/' )[- 1 ]
93- clean_name = pkg_name .replace ("google-cloud-" , "" )
94132 shards .append ({
95- "name" : f"Shard { index } : { clean_name } " ,
133+ "name" : name ,
96134 "index" : index ,
97- "description" : pkg_name ,
98- "packages" : pkg ,
135+ "description" : desc ,
136+ "packages" : " " . join ( shard_packages ) ,
99137 "is_sharded" : True
100138 })
101139 index += 1
102140
103- # Group the remaining packages
104- if normal_to_test :
105- num_packages = len (normal_to_test )
106-
107- # 1. Only shard if > 10 packages are being touched
108- # 2. Only add a new shard if any shard would have > 10 packages.
109- # To guarantee that no shard contains more than 10 packages (when distributed evenly),
110- # we need S >= N / 10, which means S = ceil(N / 10).
111- num_shards = math .ceil (num_packages / 10 )
112-
113- # Ensure at least 1 shard if we have packages
114- num_shards = max (1 , num_shards )
115-
116- # 3. Top out at the remaining budget (total 16 shards minus isolated shards)
117- max_normal_shards = max (1 , 16 - len (isolated_to_test ))
118- num_shards = min (max_normal_shards , num_shards )
119-
120- # Distribute packages between them as evenly as possible
121- shard_size = math .ceil (num_packages / num_shards )
122-
123- for i in range (num_shards ):
124- start = i * shard_size
125- end = min ((i + 1 ) * shard_size , num_packages )
126- if start >= num_packages :
127- break
128- shard_packages = normal_to_test [start :end ]
129- name = f"Shard { index } "
130- num_in_shard = len (shard_packages )
131- if len (shard_packages ) == 1 :
132- desc = shard_packages [0 ].strip ('/' ).split ('/' )[- 1 ]
133- else :
134- desc = f"{ shard_packages [0 ].strip ('/' ).split ('/' )[- 1 ]} ...{ shard_packages [- 1 ].strip ('/' ).split ('/' )[- 1 ]} ({ num_in_shard } packages)"
135-
136- shards .append ({
137- "name" : name ,
138- "index" : index ,
139- "description" : desc ,
140- "packages" : " " .join (shard_packages ),
141- "is_sharded" : True
142- })
143- index += 1
144-
145141 # Set is_sharded dynamically based on the total number of shards
146142 total_shards = len (shards )
147143 for shard in shards :
148144 shard ["is_sharded" ] = total_shards > 1
149145
150146 return shards
151147
148+
152149if __name__ == "__main__" :
153150 packages = get_packages_to_test ()
154151 shards = group_packages (packages )
0 commit comments