Skip to content

Commit 9b328c6

Browse files
committed
[worker script
1 parent 58e1fd6 commit 9b328c6

14 files changed

Lines changed: 1429 additions & 0 deletions

File tree

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Distributed Micro-Benchmarking Orchestrator
4+
5+
Coordinates distributed benchmark execution across multiple VMs.
6+
"""
7+
8+
import argparse
9+
import json
10+
import sys
11+
import os
12+
import shutil
13+
from datetime import datetime
14+
from concurrent.futures import ThreadPoolExecutor, as_completed
15+
from helpers import gcs, vm_manager, job_generator, result_aggregator, report_generator
16+
17+
18+
def parse_args():
19+
parser = argparse.ArgumentParser(description="Orchestrate distributed GCSFuse benchmarks")
20+
parser.add_argument('--benchmark-id', type=str, required=True, help='Unique benchmark ID')
21+
parser.add_argument('--instance-group', type=str, required=True, help='Managed instance group name')
22+
parser.add_argument('--zone', type=str, required=True, help='GCP zone')
23+
parser.add_argument('--project', type=str, required=True, help='GCP project')
24+
parser.add_argument('--artifacts-bucket', type=str, required=True, help='GCS bucket for artifacts')
25+
parser.add_argument('--test-csv', type=str, required=True, help='Path to test cases CSV')
26+
parser.add_argument('--configs-csv', type=str, default=None, help='Path to configs CSV (optional)')
27+
parser.add_argument('--separate-configs', action='store_true', help='Generate separate reports per config')
28+
parser.add_argument('--fio-job-file', type=str, required=True, help='Path to FIO job template file')
29+
parser.add_argument('--bucket', type=str, required=True, help='GCS bucket for testing')
30+
parser.add_argument('--iterations', type=int, required=True, help='Iterations per test')
31+
parser.add_argument('--gcsfuse-commit', type=str, default='master', help='GCSFuse branch/commit (used if no configs-csv)')
32+
parser.add_argument('--gcsfuse-mount-args', type=str, default='', help='GCSFuse mount arguments (used if no configs-csv)')
33+
parser.add_argument('--poll-interval', type=int, default=30, help='Polling interval in seconds')
34+
parser.add_argument('--timeout', type=int, default=7200, help='Timeout in seconds')
35+
parser.add_argument('--run-name', type=str, default=None, help='Descriptive run name (default: benchmark-id)')
36+
parser.add_argument('--no-auto-plot', action='store_true', help='Disable automatic plot generation')
37+
parser.add_argument('--plot-metric-group', type=str, default='default', choices=['default', 'full'],
38+
help='Metric group for auto-generated plots: default (read_bw, avg_cpu, avg_sys_cpu, avg_page_cache) or full (all metrics)')
39+
return parser.parse_args()
40+
41+
42+
def main():
43+
args = parse_args()
44+
45+
try:
46+
run_benchmark(args)
47+
except KeyboardInterrupt:
48+
print("\n\nBenchmark interrupted by user")
49+
sys.exit(130)
50+
except Exception as e:
51+
print(f"\nERROR: Benchmark failed: {e}")
52+
sys.exit(1)
53+
54+
55+
def run_benchmark(args):
56+
print(f"========== Distributed Benchmark Orchestrator ==========")
57+
print(f"Benchmark ID: {args.benchmark_id}")
58+
print(f"Instance Group: {args.instance_group}")
59+
60+
# 0. Create results directory with benchmark ID
61+
results_dir = f"results/{args.benchmark_id}"
62+
os.makedirs(results_dir, exist_ok=True)
63+
64+
print(f"Results directory: {results_dir}")
65+
66+
# Save input files to preserve configuration
67+
shutil.copy(args.test_csv, f"{results_dir}/test-cases.csv")
68+
if args.configs_csv:
69+
shutil.copy(args.configs_csv, f"{results_dir}/configs.csv")
70+
shutil.copy(args.fio_job_file, f"{results_dir}/jobfile.fio")
71+
72+
# 1. Get active VMs from instance group
73+
vms = vm_manager.get_running_vms(args.instance_group, args.zone, args.project)
74+
if not vms:
75+
print("ERROR: No running VMs found in instance group")
76+
sys.exit(1)
77+
78+
print(f"\nFound {len(vms)} running VMs: {', '.join(vms)}")
79+
80+
# 2. Load test cases and optionally configs
81+
test_cases = job_generator.load_test_cases(args.test_csv)
82+
print(f"Loaded {len(test_cases)} test cases")
83+
84+
# Check if multi-config mode
85+
if args.configs_csv:
86+
configs = job_generator.load_configs(args.configs_csv)
87+
print(f"Loaded {len(configs)} config variations")
88+
89+
# Generate test matrix (cartesian product)
90+
test_matrix = job_generator.generate_test_matrix(test_cases, configs)
91+
print(f"Generated test matrix: {len(test_matrix)} total tests ({len(configs)} configs × {len(test_cases)} tests)")
92+
93+
distribution = job_generator.distribute_tests(test_matrix, vms)
94+
mode = "multi-config"
95+
else:
96+
# Single config mode (backwards compatible)
97+
print(f"Single config mode (commit: {args.gcsfuse_commit})")
98+
distribution = job_generator.distribute_tests(test_cases, vms)
99+
mode = "single-config"
100+
configs = None
101+
102+
# Save run configuration metadata
103+
run_config = {
104+
"timestamp": datetime.now().isoformat(),
105+
"benchmark_id": args.benchmark_id,
106+
"mode": mode,
107+
"num_vms": len(vms),
108+
"vm_names": vms,
109+
"num_tests": len(test_cases),
110+
"num_configs": len(configs) if configs else 1,
111+
"iterations": args.iterations,
112+
"gcsfuse_commit": args.gcsfuse_commit,
113+
"gcsfuse_mount_args": args.gcsfuse_mount_args,
114+
"bucket": args.bucket,
115+
"artifacts_bucket": args.artifacts_bucket,
116+
"instance_group": args.instance_group,
117+
"zone": args.zone,
118+
"project": args.project
119+
}
120+
with open(f"{results_dir}/run-config.json", 'w') as f:
121+
json.dump(run_config, f, indent=2)
122+
123+
print(f"✓ Run configuration saved to {results_dir}/run-config.json")
124+
125+
print(f"\nTest Distribution:")
126+
for vm_name, tests in distribution.items():
127+
print(f" {vm_name}: {len(tests)} tests")
128+
129+
# 3. Create config dict and upload to GCS
130+
config = {
131+
'mode': mode,
132+
'iterations': args.iterations,
133+
'bucket': args.bucket,
134+
'separate_configs': args.separate_configs
135+
}
136+
137+
# Add single-config params if applicable
138+
if mode == "single-config":
139+
config['gcsfuse_commit'] = args.gcsfuse_commit
140+
config['gcsfuse_mount_args'] = args.gcsfuse_mount_args
141+
142+
base_path = f"gs://{args.artifacts_bucket}/{args.benchmark_id}"
143+
144+
config_path = f"{base_path}/config.json"
145+
gcs.upload_json(config, config_path)
146+
print(f"Uploaded config: mode={mode}, iterations={args.iterations}, bucket={args.bucket}")
147+
148+
gcs.upload_test_cases(args.test_csv, base_path)
149+
print(f"Uploaded test cases to: {base_path}/test-cases.csv")
150+
151+
# Upload configs.csv if in multi-config mode
152+
if args.configs_csv:
153+
configs_dest = f"{base_path}/configs.csv"
154+
gcs.upload_test_cases(args.configs_csv, configs_dest)
155+
print(f"Uploaded configs to: {configs_dest}")
156+
157+
gcs.upload_fio_job_file(args.fio_job_file, base_path)
158+
print(f"Uploaded FIO job file to: {base_path}/jobfile.fio")
159+
160+
# 4. Generate and upload job files for each VM (in parallel)
161+
active_vms = []
162+
jobs_to_upload = []
163+
164+
# Calculate total test cases for modulo arithmetic
165+
num_test_cases = len(test_cases)
166+
167+
for vm_name, test_entries in distribution.items():
168+
if not test_entries:
169+
print(f"Skipping {vm_name}: No tests assigned")
170+
continue
171+
172+
active_vms.append(vm_name)
173+
174+
for entry in test_entries:
175+
if isinstance(entry, dict) and 'matrix_id' in entry and num_test_cases > 0:
176+
# Map global ID back to [0, num_test_cases-1]
177+
entry['test_id'] = entry['matrix_id'] % num_test_cases
178+
179+
job = job_generator.create_job_spec(
180+
vm_name=vm_name,
181+
benchmark_id=args.benchmark_id,
182+
test_entries=test_entries,
183+
bucket=args.bucket,
184+
artifacts_bucket=args.artifacts_bucket,
185+
iterations=args.iterations,
186+
mode=mode
187+
)
188+
189+
job_path = f"{base_path}/jobs/{vm_name}.json"
190+
jobs_to_upload.append((vm_name, job, job_path, len(test_entries)))
191+
192+
# Upload jobs in parallel
193+
def upload_job(job_info):
194+
vm_name, job, job_path, num_tests = job_info
195+
gcs.upload_json(job, job_path)
196+
return vm_name, num_tests
197+
198+
with ThreadPoolExecutor(max_workers=10) as executor:
199+
futures = [executor.submit(upload_job, job_info) for job_info in jobs_to_upload]
200+
for future in as_completed(futures):
201+
vm_name, num_tests = future.result()
202+
print(f"Uploaded job for {vm_name}: {num_tests} tests, {num_tests * args.iterations} total runs")
203+
204+
if not active_vms:
205+
print("\nERROR: No VMs have test assignments")
206+
sys.exit(1)
207+
208+
print(f"\nActive VMs: {len(active_vms)}/{len(vms)}")
209+
210+
# 5. Trigger VMs to start execution (in parallel)
211+
print(f"\nTriggering VMs...")
212+
worker_script = "workers/worker.sh"
213+
214+
def trigger_vm(vm_name):
215+
vm_manager.run_worker_script(
216+
vm_name,
217+
args.zone,
218+
args.project,
219+
worker_script,
220+
args.benchmark_id,
221+
args.artifacts_bucket
222+
)
223+
return vm_name
224+
225+
with ThreadPoolExecutor(max_workers=10) as executor:
226+
futures = [executor.submit(trigger_vm, vm_name) for vm_name in active_vms]
227+
for future in as_completed(futures):
228+
vm_name = future.result()
229+
print(f" Started {vm_name}")
230+
231+
# 6. Monitor progress by polling manifests
232+
print(f"\nMonitoring progress (polling every {args.poll_interval}s)...")
233+
completed = vm_manager.wait_for_completion(
234+
vms=active_vms,
235+
benchmark_id=args.benchmark_id,
236+
artifacts_bucket=args.artifacts_bucket,
237+
poll_interval=args.poll_interval,
238+
timeout=args.timeout
239+
)
240+
241+
if not completed:
242+
print("\nWARNING: Not all active VMs completed successfully")
243+
print("Continuing with report generation for successful VMs...")
244+
else:
245+
print(f"\n✓ All VMs completed successfully!")
246+
247+
# 7. Aggregate results
248+
print(f"\nAggregating results...")
249+
metrics = result_aggregator.aggregate_results(
250+
benchmark_id=args.benchmark_id,
251+
artifacts_bucket=args.artifacts_bucket,
252+
vms=active_vms,
253+
mode=mode
254+
)
255+
256+
if not metrics:
257+
print("\nERROR: No test results collected from any VM")
258+
sys.exit(1)
259+
260+
# 8. Generate report
261+
report_file = f"{results_dir}/combined_report.csv"
262+
report_generator.generate_report(metrics, report_file, mode=mode, separate_configs=args.separate_configs)
263+
264+
if args.separate_configs:
265+
print(f"\n✓ Reports generated in {results_dir}/")
266+
else:
267+
print(f"\n✓ Report generated: {report_file}")
268+
269+
# 9. Auto-generate plots (unless disabled)
270+
if not args.no_auto_plot:
271+
print(f"\nGenerating plots (metric group: {args.plot_metric_group})...")
272+
try:
273+
import subprocess
274+
subprocess.run([
275+
'python3', 'plot_reports.py',
276+
report_file,
277+
'--metric-group', args.plot_metric_group,
278+
'--output-file', f"{results_dir}/plots.png"
279+
], check=True)
280+
print(f"✓ Plots generated: {results_dir}/plots.png")
281+
except subprocess.CalledProcessError as e:
282+
print(f"✗ Plot generation failed: {e}")
283+
284+
# Update 'latest' symlink
285+
latest_link = "results/latest"
286+
if os.path.islink(latest_link):
287+
os.unlink(latest_link)
288+
elif os.path.exists(latest_link):
289+
shutil.rmtree(latest_link)
290+
os.symlink(args.benchmark_id, latest_link)
291+
292+
print(f"\n========== Benchmark Complete ==========")
293+
print(f"Results saved to: {results_dir}/")
294+
print(f" - Input files: test-cases.csv, configs.csv, jobfile.fio")
295+
print(f" - Report: combined_report.csv")
296+
if not args.no_auto_plot:
297+
print(f" - Plots: plots.png")
298+
print(f" - Latest: results/latest/")
299+
300+
# Exit with error code if some VMs failed
301+
if not completed:
302+
sys.exit(1)
303+
304+
305+
if __name__ == '__main__':
306+
main()

0 commit comments

Comments
 (0)