313 lines
14 KiB
Python
313 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright 2026 Google LLC
|
|
"""Live experiment demonstrating Artemis concurrency modes and device targeting.
|
|
|
|
Covers:
|
|
1. Single-device queueing and relay (FIFO mutual exclusion on the same device).
|
|
2. Multi-device parallel execution (per_device concurrency mode across real device & emulator).
|
|
3. Cross-device global serialization (global concurrency mode restricting all devices to 1 task).
|
|
4. Full parameter coverage (device_serial, set_device, Task objects, list_devices, get_idle_devices).
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
import time
|
|
from datetime import datetime
|
|
|
|
# Add project root to sys.path
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from artemis import ArtemisClient, ConcurrencyMode, Task
|
|
from artemis.runtime import DeviceBusyError, device_pool
|
|
|
|
|
|
def print_header(title: str):
|
|
print("\n" + "=" * 76)
|
|
print(f" {title}")
|
|
print("=" * 76)
|
|
|
|
|
|
def now_str() -> str:
|
|
return datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
|
|
|
|
async def run_experiment_1_single_device_relay(real_serial: str):
|
|
"""Experiment 1: Single device FIFO queueing & relay."""
|
|
print_header("[Experiment 1] Single-Device Task Queueing & Relay (FIFO Relay on Single Device)")
|
|
print(f"Target Device: {real_serial} (Physical Pixel 11 Pro)")
|
|
|
|
client = ArtemisClient(device_serial=real_serial, default_profile="flash", concurrency_mode="per_device")
|
|
print(f"Client Init: device_serial='{client.device_serial}', concurrency_mode='{client.concurrency_mode}'")
|
|
|
|
task_1_launched = 0.0
|
|
task_1_start = 0.0
|
|
task_1_end = 0.0
|
|
|
|
task_2_launched = 0.0
|
|
task_2_start = 0.0
|
|
task_2_end = 0.0
|
|
|
|
async def execute_task_1():
|
|
nonlocal task_1_launched, task_1_start, task_1_end
|
|
task_1_launched = time.time()
|
|
print(f"[{now_str()}] ▶ [Task 1] Submitted for scheduling (target device: {real_serial})")
|
|
task_1_start = time.time()
|
|
res = await client.run(
|
|
"Open Settings and check battery level",
|
|
device_serial=real_serial,
|
|
max_turns=3,
|
|
)
|
|
task_1_end = time.time()
|
|
print(
|
|
f"[{now_str()}] ◀ [Task 1] Execution completed! Status={res.status}, "
|
|
f"Trace={res.trace_id[:8]}..., Elapsed={task_1_end - task_1_start:.3f}s"
|
|
)
|
|
return res
|
|
|
|
async def execute_task_2():
|
|
nonlocal task_2_launched, task_2_start, task_2_end
|
|
task_2_launched = time.time()
|
|
print(f"[{now_str()}] ▶ [Task 2] Submitted concurrently (detects device occupied by Task 1, entering FIFO queue...)")
|
|
|
|
task_2_start = time.time()
|
|
res = await client.run(
|
|
"Return to home screen and open Clock",
|
|
device_serial=real_serial,
|
|
max_turns=3,
|
|
)
|
|
task_2_end = time.time()
|
|
print(
|
|
f"[{now_str()}] ◀ [Task 2] Relayed execution completed! Status={res.status}, "
|
|
f"Trace={res.trace_id[:8]}..., Elapsed={task_2_end - task_2_start:.3f}s"
|
|
)
|
|
return res
|
|
|
|
# Launch Task 1 first, then Task 2 40ms later while Task 1 is holding the lock
|
|
t_coro_1 = asyncio.create_task(execute_task_1())
|
|
await asyncio.sleep(0.04)
|
|
t_coro_2 = asyncio.create_task(execute_task_2())
|
|
res1, res2 = await asyncio.gather(t_coro_1, t_coro_2)
|
|
|
|
print("\n[Timing & Relay Analysis]:")
|
|
print(f" Task 1 runtime interval: [{task_1_start:.3f}s ~ {task_1_end:.3f}s] (elapsed {task_1_end - task_1_start:.3f}s)")
|
|
print(f" Task 2 submission time: {task_2_launched:.3f}s")
|
|
print(f" Task 2 actual start: {task_2_start:.3f}s (waited for Task 1 lock release)")
|
|
print(f" Task 2 end time: {task_2_end:.3f}s (elapsed {task_2_end - task_2_start:.3f}s)")
|
|
|
|
assert task_2_end > task_1_end, "Task 2 did not complete after Task 1!"
|
|
assert task_2_end - task_1_start >= (task_1_end - task_1_start) + 0.1, "Tasks failed to execute in sequential queue!"
|
|
print(" ✅ Verification successful: Task 2 queued properly and relayed immediately after Task 1 released the lock!")
|
|
|
|
|
|
async def run_experiment_2_multi_device_parallel(real_serial: str, emu_serial: str):
|
|
"""Experiment 2: Multi-device parallel execution under per_device concurrency."""
|
|
print_header("[Experiment 2] Multi-Device Parallel Execution Test")
|
|
print(f"Device A (Physical): {real_serial}")
|
|
print(f"Device B (Emulator): {emu_serial}")
|
|
|
|
client_a = ArtemisClient(device_serial=real_serial, default_profile="flash", concurrency_mode="per_device")
|
|
client_b = ArtemisClient(device_serial=emu_serial, default_profile="flash", concurrency_mode="per_device")
|
|
|
|
a_start, a_end = 0.0, 0.0
|
|
b_start, b_end = 0.0, 0.0
|
|
|
|
async def run_on_device_a():
|
|
nonlocal a_start, a_end
|
|
print(f"[{now_str()}] ▶ [Device A Task] Starting on {real_serial}")
|
|
a_start = time.time()
|
|
res = await client_a.run("Test operation A on physical device", max_turns=3)
|
|
a_end = time.time()
|
|
print(f"[{now_str()}] ◀ [Device A Task] Completed! Elapsed: {a_end - a_start:.3f}s, Device: {res.device_id}")
|
|
return res
|
|
|
|
async def run_on_device_b():
|
|
nonlocal b_start, b_end
|
|
print(f"[{now_str()}] ▶ [Device B Task] Starting on {emu_serial}")
|
|
b_start = time.time()
|
|
res = await client_b.run("Test operation B on emulator", max_turns=3)
|
|
b_end = time.time()
|
|
print(f"[{now_str()}] ◀ [Device B Task] Completed! Elapsed: {b_end - b_start:.3f}s, Device: {res.device_id}")
|
|
return res
|
|
|
|
t_start = time.time()
|
|
task_a = asyncio.create_task(run_on_device_a())
|
|
task_b = asyncio.create_task(run_on_device_b())
|
|
res_a, res_b = await asyncio.gather(task_a, task_b)
|
|
t_total = time.time() - t_start
|
|
|
|
print("\n[Dual-Device Concurrency Analysis]:")
|
|
print(f" Device A execution interval: {a_start:.3f}s ~ {a_end:.3f}s (elapsed {a_end - a_start:.3f}s)")
|
|
print(f" Device B execution interval: {b_start:.3f}s ~ {b_end:.3f}s (elapsed {b_end - b_start:.3f}s)")
|
|
print(f" Total concurrent duration: {t_total:.3f}s (sum of sequential durations: {(a_end - a_start) + (b_end - b_start):.3f}s)")
|
|
|
|
# Overlap verification
|
|
overlap = min(a_end, b_end) - max(a_start, b_start)
|
|
print(f" Parallel overlap duration: {overlap:.3f}s")
|
|
assert overlap > 0.0, "Dual devices failed to run concurrently with overlap!"
|
|
assert t_total < ((a_end - a_start) + (b_end - b_start) * 0.95), "Total duration should be less than sum of both tasks!"
|
|
print(" ✅ Verification successful: Tasks on both devices ran concurrently without blocking!")
|
|
|
|
|
|
async def run_experiment_3_global_concurrency_serialization(real_serial: str, emu_serial: str):
|
|
"""Experiment 3: Global concurrency serialization across devices."""
|
|
print_header("[Experiment 3] Global Concurrency Mode Test")
|
|
print("Mode description: concurrency_mode='global', entire system allows only 1 task across all devices.")
|
|
|
|
client_a = ArtemisClient(device_serial=real_serial, default_profile="flash", concurrency_mode="global")
|
|
client_b = ArtemisClient(device_serial=emu_serial, default_profile="flash", concurrency_mode="global")
|
|
|
|
# 3.1 Test non-blocking mode rejection
|
|
print("\n[Subtest 3.1: Non-blocking rejection limit]")
|
|
from artemis.runtime import DeviceExecutionLock
|
|
lock_a = DeviceExecutionLock(real_serial, "Placeholder test lock", concurrency_mode="global")
|
|
await asyncio.to_thread(lock_a.acquire)
|
|
try:
|
|
print(f"[{now_str()}] Device A ({real_serial}) holds the global lock...")
|
|
try:
|
|
print(f"[{now_str()}] Device B ({emu_serial}) attempts run with blocking=False...")
|
|
await client_b.run("Emulator non-blocking task", blocking=False)
|
|
print(" ❌ Exception: Was not intercepted by global lock!")
|
|
except DeviceBusyError as exc:
|
|
print(f" ✅ Correctly intercepted with DeviceBusyError: {exc}")
|
|
finally:
|
|
await asyncio.to_thread(lock_a.release)
|
|
|
|
# 3.2 Test cross-device queueing and relay
|
|
print("\n[Subtest 3.2: Cross-device global queueing & relay]")
|
|
t1_submitted, t1_end = 0.0, 0.0
|
|
t2_submitted, t2_end = 0.0, 0.0
|
|
|
|
async def run_global_task_1():
|
|
nonlocal t1_submitted, t1_end
|
|
t1_submitted = time.time()
|
|
print(f"[{now_str()}] ▶ [Global Task 1] Started on physical device ({real_serial})...")
|
|
res = await client_a.run("Global task 1", max_turns=3)
|
|
t1_end = time.time()
|
|
print(f"[{now_str()}] ◀ [Global Task 1] Completed! Elapsed: {t1_end - t1_submitted:.3f}s")
|
|
return res
|
|
|
|
async def run_global_task_2():
|
|
nonlocal t2_submitted, t2_end
|
|
# Submit task 2 shortly after task 1
|
|
await asyncio.sleep(0.04)
|
|
t2_submitted = time.time()
|
|
print(f"[{now_str()}] ▶ [Global Task 2] Submitted to emulator ({emu_serial}), entering queue due to global limit...")
|
|
res = await client_b.run("Global task 2", max_turns=3)
|
|
t2_end = time.time()
|
|
print(f"[{now_str()}] ◀ [Global Task 2] Relayed completion! Total time (including queue): {t2_end - t2_submitted:.3f}s")
|
|
return res
|
|
|
|
res1, res2 = await asyncio.gather(run_global_task_1(), run_global_task_2())
|
|
print(f" Global Task 1 interval: {t1_submitted:.3f}s ~ {t1_end:.3f}s (elapsed {t1_end - t1_submitted:.3f}s)")
|
|
print(f" Global Task 2 submit time: {t2_submitted:.3f}s")
|
|
print(f" Global Task 2 end time: {t2_end:.3f}s (waited for Task 1 completion before executing)")
|
|
assert t2_end > t1_end, "Global Task 2 did not complete after Global Task 1!"
|
|
assert (t2_end - t1_end) >= 0.08, "Global Task 2 did not execute sequentially after Task 1 released global lock!"
|
|
print(" ✅ Verification successful: Under global mode, strictly 1 task executed sequentially across different devices!")
|
|
|
|
|
|
async def run_experiment_4_parameters_and_utilities(real_serial: str, emu_serial: str):
|
|
"""Experiment 4: Comprehensive SDK parameter and utility testing."""
|
|
print_header("[Experiment 4] SDK Parameter & Utility Comprehensive Test")
|
|
|
|
client = ArtemisClient()
|
|
print(f"1. Default idle device selection: client.device_serial = '{client.device_serial}'")
|
|
|
|
# Chain switch device
|
|
client.set_device(real_serial)
|
|
print(f"2. Chain switch to physical device: client.set_device('{real_serial}') -> '{client.device_serial}'")
|
|
assert client.device_serial == real_serial
|
|
|
|
# Attribute switch
|
|
client.device_serial = emu_serial
|
|
print(f"3. Property assignment to emulator: client.device_serial = '{emu_serial}' -> '{client.device_id}'")
|
|
assert client.device_id == emu_serial
|
|
|
|
# Dynamically change concurrency mode
|
|
client.set_concurrency_mode(ConcurrencyMode.GLOBAL)
|
|
print(f"4. Chain change concurrency mode: client.concurrency_mode = '{client.concurrency_mode}'")
|
|
assert client.concurrency_mode == "global"
|
|
client.concurrency_mode = "per_device"
|
|
|
|
# Query all available devices
|
|
print("5. client.list_devices() status:")
|
|
devices = client.list_devices()
|
|
for d in devices:
|
|
print(f" • [{d.serial}] State: {d.state}, Model: {d.model}, IsEmulator: {d.is_emulator}, Busy: {d.is_busy}")
|
|
|
|
# Query idle devices
|
|
idle = client.get_idle_devices()
|
|
print(f"6. client.get_idle_devices(): {[d.serial for d in idle]}")
|
|
|
|
# Use Task structured object with run_task
|
|
print("7. Calling client.run_task() with structured Task entity:")
|
|
task_obj = Task(
|
|
goal="Verify physical device call via Task object",
|
|
profile="flash",
|
|
device_serial=real_serial,
|
|
concurrency_mode="per_device",
|
|
max_turns=3,
|
|
)
|
|
result = await client.run_task(task_obj)
|
|
print(f" Task result: Status={result.status}, Device={result.device_id}, Trace={result.trace_id[:8]}...")
|
|
assert result.device_id == real_serial
|
|
|
|
# Use stream_run for live event stream
|
|
print("8. Using client.stream_run() for live event stream:")
|
|
events_received = 0
|
|
async for ev in client.stream_run(
|
|
"Stream operation test",
|
|
device_serial=emu_serial,
|
|
profile="flash",
|
|
max_turns=3,
|
|
):
|
|
events_received += 1
|
|
print(f" • [{ev.event_type}] payload={ev.payload}")
|
|
assert events_received >= 2
|
|
print(f" ✅ Stream events captured successfully! Captured {events_received} events.")
|
|
|
|
|
|
async def main():
|
|
# Detect devices
|
|
devices = device_pool.list_devices()
|
|
print("=" * 76)
|
|
print("[ARTEMIS Python SDK Concurrency & Device Targeting Experiment]")
|
|
print(f"Detected connected devices count: {len(devices)}")
|
|
for d in devices:
|
|
print(f" • Serial: {d.serial} | Model: {d.model} | State: {d.state} | IsEmulator: {d.is_emulator}")
|
|
print("=" * 76)
|
|
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description="Live experiment demonstrating Artemis concurrency modes and device targeting.")
|
|
parser.add_argument("--phone", default=os.environ.get("ARTEMIS_PHONE_SERIAL"), help="Target physical phone serial number")
|
|
parser.add_argument("--emulator", default=os.environ.get("ARTEMIS_EMULATOR_SERIAL"), help="Target Android emulator serial number")
|
|
args, _ = parser.parse_known_args()
|
|
|
|
# Auto-detect connected devices if not explicitly provided
|
|
real_dev = next((d for d in devices if not d.is_emulator and d.state == "device"), None)
|
|
emu_dev = next((d for d in devices if d.is_emulator and d.state == "device"), None)
|
|
|
|
real_serial = args.phone or (real_dev.serial if real_dev else "63191FDKX00062")
|
|
emu_serial = args.emulator or (emu_dev.serial if emu_dev else "emulator-5554")
|
|
|
|
# Verify both devices are available
|
|
serials = [d.serial for d in devices]
|
|
if real_serial not in serials or emu_serial not in serials:
|
|
print(f"Error: Both physical device '{real_serial}' and emulator '{emu_serial}' must be connected for full experiment.")
|
|
print(f"Currently online devices: {serials}")
|
|
return
|
|
|
|
# Run experiments in sequence
|
|
await run_experiment_1_single_device_relay(real_serial)
|
|
await run_experiment_2_multi_device_parallel(real_serial, emu_serial)
|
|
await run_experiment_3_global_concurrency_serialization(real_serial, emu_serial)
|
|
await run_experiment_4_parameters_and_utilities(real_serial, emu_serial)
|
|
|
|
print_header("🎉 All experiments completed successfully! All concurrency and device targeting verified!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|