Skip to content

Commit a1fab95

Browse files
committed
use pacakge weights instead of isolation
1 parent 89e5499 commit a1fab95

1 file changed

Lines changed: 59 additions & 62 deletions

File tree

ci/get_package_shards.py

Lines changed: 59 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@
1818
import math
1919
import 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+
4147
def 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+
152149
if __name__ == "__main__":
153150
packages = get_packages_to_test()
154151
shards = group_packages(packages)

0 commit comments

Comments
 (0)