|
| 1 | +import subprocess |
| 2 | +import threading |
| 3 | +import re |
| 4 | +import time |
| 5 | +from datetime import datetime |
| 6 | +import xml.etree.ElementTree as ET |
| 7 | +import os |
| 8 | + |
| 9 | +def get_connected_devices(): |
| 10 | + result = subprocess.run(['adb', 'devices'], capture_output=True, text=True) |
| 11 | + lines = result.stdout.strip().split('\n') |
| 12 | + devices = [] |
| 13 | + for line in lines[1:]: |
| 14 | + if line.strip(): |
| 15 | + parts = line.split('\t') |
| 16 | + if len(parts) == 2 and parts[1] == 'device': |
| 17 | + devices.append(parts[0]) |
| 18 | + return devices |
| 19 | + |
| 20 | +def choose_device(devices): |
| 21 | + print("Multiple devices connected:") |
| 22 | + for idx, device in enumerate(devices): |
| 23 | + print(f"{idx + 1}. {device}") |
| 24 | + while True: |
| 25 | + try: |
| 26 | + choice = int(input("Select the device number to use: ")) |
| 27 | + if 1 <= choice <= len(devices): |
| 28 | + return devices[choice - 1] |
| 29 | + else: |
| 30 | + print(f"Please enter a number between 1 and {len(devices)}.") |
| 31 | + except ValueError: |
| 32 | + print("Invalid input. Please enter a number.") |
| 33 | + |
| 34 | +def start_browser_and_download(download_url, package_name, activity_name, device_id): |
| 35 | + adb_command = ['adb', 'shell', 'am', 'start', '-n', f'{package_name}/{activity_name}', |
| 36 | + '-a', 'android.intent.action.VIEW', '-d', download_url] |
| 37 | + if device_id: |
| 38 | + adb_command.insert(1, '-s') |
| 39 | + adb_command.insert(2, device_id) |
| 40 | + subprocess.run(adb_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| 41 | + time.sleep(5) |
| 42 | + |
| 43 | +def dump_ui_hierarchy(device_id): |
| 44 | + if os.path.exists('ui_dump.xml'): |
| 45 | + os.remove('ui_dump.xml') |
| 46 | + adb_command_dump = ['adb', 'shell', 'uiautomator', 'dump', '/sdcard/ui_dump.xml'] |
| 47 | + adb_command_pull = ['adb', 'pull', '/sdcard/ui_dump.xml'] |
| 48 | + if device_id: |
| 49 | + adb_command_dump.insert(1, '-s') |
| 50 | + adb_command_dump.insert(2, device_id) |
| 51 | + adb_command_pull.insert(1, '-s') |
| 52 | + adb_command_pull.insert(2, device_id) |
| 53 | + subprocess.run(adb_command_dump, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| 54 | + subprocess.run(adb_command_pull, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| 55 | + |
| 56 | +def find_element_bounds(xml_file, text=None, resource_id=None, content_desc=None, class_name=None): |
| 57 | + tree = ET.parse(xml_file) |
| 58 | + root = tree.getroot() |
| 59 | + for node in root.iter('node'): |
| 60 | + node_text = node.attrib.get('text') |
| 61 | + node_resource_id = node.attrib.get('resource-id') |
| 62 | + node_content_desc = node.attrib.get('content-desc') |
| 63 | + node_class = node.attrib.get('class') |
| 64 | + if resource_id and node_resource_id and resource_id == node_resource_id: |
| 65 | + bounds = node.attrib.get('bounds') |
| 66 | + return bounds |
| 67 | + if text and node_text and text.lower() in node_text.lower(): |
| 68 | + if class_name and node_class != class_name: |
| 69 | + continue |
| 70 | + bounds = node.attrib.get('bounds') |
| 71 | + return bounds |
| 72 | + if content_desc and node_content_desc and content_desc.lower() in node_content_desc.lower(): |
| 73 | + bounds = node.attrib.get('bounds') |
| 74 | + return bounds |
| 75 | + return None |
| 76 | + |
| 77 | +def get_center_coordinates(bounds): |
| 78 | + match = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', bounds) |
| 79 | + if match: |
| 80 | + left, top, right, bottom = map(int, match.groups()) |
| 81 | + center_x = (left + right) // 2 |
| 82 | + center_y = (top + bottom) // 2 |
| 83 | + return center_x, center_y |
| 84 | + else: |
| 85 | + return None |
| 86 | + |
| 87 | +def tap_screen(x, y, device_id): |
| 88 | + adb_command = ['adb', 'shell', 'input', 'tap', str(x), str(y)] |
| 89 | + if device_id: |
| 90 | + adb_command.insert(1, '-s') |
| 91 | + adb_command.insert(2, device_id) |
| 92 | + subprocess.run(adb_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| 93 | + |
| 94 | +def click_download_button(device_id): |
| 95 | + max_attempts = 5 |
| 96 | + for attempt in range(max_attempts): |
| 97 | + dump_ui_hierarchy(device_id) |
| 98 | + bounds = find_element_bounds('ui_dump.xml', resource_id='org.mozilla.firefox:id/positive_button') |
| 99 | + if not bounds: |
| 100 | + bounds = find_element_bounds('ui_dump.xml', resource_id='android:id/button1') |
| 101 | + if not bounds: |
| 102 | + bounds = find_element_bounds('ui_dump.xml', text='Download', class_name='android.widget.Button') |
| 103 | + if not bounds: |
| 104 | + bounds = find_element_bounds('ui_dump.xml', text='Download') |
| 105 | + if bounds: |
| 106 | + coordinates = get_center_coordinates(bounds) |
| 107 | + if coordinates: |
| 108 | + x, y = coordinates |
| 109 | + tap_screen(x, y, device_id) |
| 110 | + print(f"Tapped on Download button at ({x}, {y}).") |
| 111 | + return True |
| 112 | + else: |
| 113 | + print(f"Download button not found. Attempt {attempt + 1}/{max_attempts}") |
| 114 | + time.sleep(2) |
| 115 | + print("Failed to find and tap the download button after multiple attempts.") |
| 116 | + return False |
| 117 | + |
| 118 | +def parse_log_time(log_line): |
| 119 | + match = re.match(r'^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})', log_line) |
| 120 | + if match: |
| 121 | + timestamp_str = match.group(1) |
| 122 | + current_year = datetime.now().year |
| 123 | + timestamp_str_with_year = f"{current_year}-{timestamp_str}" |
| 124 | + try: |
| 125 | + timestamp = datetime.strptime(timestamp_str_with_year, '%Y-%m-%d %H:%M:%S.%f') |
| 126 | + return timestamp |
| 127 | + except ValueError as e: |
| 128 | + print(f"Failed to parse timestamp '{timestamp_str_with_year}': {e}") |
| 129 | + return None |
| 130 | + else: |
| 131 | + print(f"No timestamp found in log line: {log_line}") |
| 132 | + return None |
| 133 | + |
| 134 | +def monitor_adb_logs(start_event, end_event, result_dict, device_id): |
| 135 | + adb_command = ['adb', 'logcat', '-v', 'time'] |
| 136 | + if device_id: |
| 137 | + adb_command.insert(1, '-s') |
| 138 | + adb_command.insert(2, device_id) |
| 139 | + process = subprocess.Popen(adb_command, |
| 140 | + stdout=subprocess.PIPE, |
| 141 | + stderr=subprocess.STDOUT, |
| 142 | + universal_newlines=True, |
| 143 | + bufsize=1) |
| 144 | + start_pattern = re.compile(r'Open with FUSE\. FilePath: .*\.pending-.*') |
| 145 | + end_pattern = re.compile(r'Moving /storage/emulated/0/Download/\.pending-.* to /storage/emulated/0/Download/.*') |
| 146 | + for line in process.stdout: |
| 147 | + line = line.strip() |
| 148 | + if start_pattern.search(line): |
| 149 | + print(f"Download started: {line}") |
| 150 | + timestamp = parse_log_time(line) |
| 151 | + if timestamp: |
| 152 | + result_dict['start_time'] = timestamp |
| 153 | + start_event.set() |
| 154 | + elif end_pattern.search(line): |
| 155 | + print(f"Download finished: {line}") |
| 156 | + timestamp = parse_log_time(line) |
| 157 | + if timestamp: |
| 158 | + result_dict['end_time'] = timestamp |
| 159 | + end_event.set() |
| 160 | + break |
| 161 | + process.terminate() |
| 162 | + |
| 163 | +def calculate_download_duration(start_time, end_time): |
| 164 | + duration = (end_time - start_time).total_seconds() |
| 165 | + return duration |
| 166 | + |
| 167 | +def clear_adb_logs(device_id): |
| 168 | + adb_command = ['adb', 'logcat', '-c'] |
| 169 | + if device_id: |
| 170 | + adb_command.insert(1, '-s') |
| 171 | + adb_command.insert(2, device_id) |
| 172 | + subprocess.run(adb_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| 173 | + |
| 174 | +def close_browser(package_name, device_id): |
| 175 | + adb_command = ['adb', 'shell', 'am', 'force-stop', package_name] |
| 176 | + if device_id: |
| 177 | + adb_command.insert(1, '-s') |
| 178 | + adb_command.insert(2, device_id) |
| 179 | + subprocess.run(adb_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| 180 | + |
| 181 | +def start_profiling(package_name, device_id, profiling_duration, ndk_path): |
| 182 | + import threading |
| 183 | + |
| 184 | + print("Starting profiling with simpleperf...") |
| 185 | + app_profiler_path = os.path.join(ndk_path, 'simpleperf', 'app_profiler.py') |
| 186 | + profiling_cmd = [ |
| 187 | + app_profiler_path, |
| 188 | + '-p', package_name, |
| 189 | + '-r', "-g --duration 200 -f 1000 --trace-offcpu -e cpu-clock:u" |
| 190 | + ] |
| 191 | + |
| 192 | + profiling_process = subprocess.Popen( |
| 193 | + profiling_cmd, |
| 194 | + stdout=subprocess.PIPE, |
| 195 | + stderr=subprocess.PIPE, |
| 196 | + universal_newlines=True, |
| 197 | + bufsize=1 |
| 198 | + ) |
| 199 | + |
| 200 | + |
| 201 | + def read_output(pipe): |
| 202 | + for line in iter(pipe.readline, ''): |
| 203 | + print(line, end='') |
| 204 | + pipe.close() |
| 205 | + |
| 206 | + threading.Thread(target=read_output, args=(profiling_process.stdout,), daemon=True).start() |
| 207 | + threading.Thread(target=read_output, args=(profiling_process.stderr,), daemon=True).start() |
| 208 | + |
| 209 | + return profiling_process |
| 210 | + |
| 211 | + |
| 212 | +def stop_profiling(profiling_process): |
| 213 | + print("Stopping profiling...") |
| 214 | + profiling_process.wait() |
| 215 | + print("Profiling stopped.") |
| 216 | + |
| 217 | +def test_download_speed(download_url, browser_info, device_id, profiling_enabled=False, ndk_path=None): |
| 218 | + start_event = threading.Event() |
| 219 | + end_event = threading.Event() |
| 220 | + result_dict = {} |
| 221 | + |
| 222 | + if profiling_enabled: |
| 223 | + profiling_duration = 200 |
| 224 | + profiling_process = start_profiling(browser_info['package_name'], device_id, profiling_duration, ndk_path) |
| 225 | + if profiling_process is None: |
| 226 | + print("Exiting due to profiling start failure.") |
| 227 | + return None |
| 228 | + |
| 229 | + clear_adb_logs(device_id) |
| 230 | + |
| 231 | + log_thread = threading.Thread(target=monitor_adb_logs, |
| 232 | + args=(start_event, end_event, result_dict, device_id)) |
| 233 | + log_thread.start() |
| 234 | + |
| 235 | + start_browser_and_download(download_url, browser_info['package_name'], browser_info['activity_name'], device_id) |
| 236 | + |
| 237 | + time.sleep(5) |
| 238 | + |
| 239 | + if not click_download_button(device_id): |
| 240 | + print("Failed to initiate download.") |
| 241 | + log_thread.join() |
| 242 | + if profiling_enabled: |
| 243 | + stop_profiling(profiling_process) |
| 244 | + return None |
| 245 | + |
| 246 | + print("Waiting for download to start...") |
| 247 | + if not start_event.wait(timeout=60): |
| 248 | + print("Download did not start within 60 seconds.") |
| 249 | + log_thread.join() |
| 250 | + if profiling_enabled: |
| 251 | + stop_profiling(profiling_process) |
| 252 | + return None |
| 253 | + |
| 254 | + print("Waiting for download to complete...") |
| 255 | + if not end_event.wait(timeout=900): |
| 256 | + print("Download did not finish within 15 minutes.") |
| 257 | + log_thread.join() |
| 258 | + if profiling_enabled: |
| 259 | + stop_profiling(profiling_process) |
| 260 | + return None |
| 261 | + |
| 262 | + start_time = result_dict.get('start_time') |
| 263 | + end_time = result_dict.get('end_time') |
| 264 | + |
| 265 | + close_browser(browser_info['package_name'], device_id) |
| 266 | + |
| 267 | + if profiling_enabled: |
| 268 | + stop_profiling(profiling_process) |
| 269 | + |
| 270 | + if start_time and end_time: |
| 271 | + duration = calculate_download_duration(start_time, end_time) |
| 272 | + print(f"Download completed in {duration} seconds.") |
| 273 | + else: |
| 274 | + print("Could not determine download times.") |
| 275 | + duration = None |
| 276 | + |
| 277 | + log_thread.join() |
| 278 | + return duration |
| 279 | + |
| 280 | +def main(): |
| 281 | + #download_url = 'https://link.testfile.org/300MB |
| 282 | + # download_url = 'https://link.testfile.org/500MB' |
| 283 | + # download_url = 'https://testfile.org/1.3GBiconpng' |
| 284 | + # download_url = 'https://testfile.org/file-kali-3.9GB-2' |
| 285 | + download_url = 'https://testfile.org/files-5GB-zip' |
| 286 | + |
| 287 | + browsers = { |
| 288 | + '1': { |
| 289 | + 'name': 'Firefox', |
| 290 | + 'package_name': 'org.mozilla.firefox', |
| 291 | + 'activity_name': 'org.mozilla.gecko.BrowserApp', |
| 292 | + }, |
| 293 | + '2': { |
| 294 | + 'name': 'Chrome', |
| 295 | + 'package_name': 'com.android.chrome', |
| 296 | + 'activity_name': 'com.google.android.apps.chrome.Main', |
| 297 | + } |
| 298 | + } |
| 299 | + |
| 300 | + devices = get_connected_devices() |
| 301 | + if not devices: |
| 302 | + print("No devices connected. Please connect an Android device and try again.") |
| 303 | + exit(1) |
| 304 | + elif len(devices) == 1: |
| 305 | + device_id = devices[0] |
| 306 | + print(f"Using device: {device_id}") |
| 307 | + |
| 308 | + profiling_enabled = input("Do you want to enable profiling during the download? (yes/no): ").lower() == 'yes' |
| 309 | + ndk_path = None |
| 310 | + if profiling_enabled: |
| 311 | + ndk_path = input("Please enter the path to your android-ndk directory within mozbuild (e.g., ~/.mozbuild/android-ndk-r27b): ") |
| 312 | + while not os.path.isdir(ndk_path): |
| 313 | + print("Invalid path. Please try again.") |
| 314 | + ndk_path = input("Please enter the path to your android-ndk directory within mozbuild (e.g., ~/.mozbuild/android-ndk-r27b): ") |
| 315 | + else: |
| 316 | + print("Multiple devices connected:") |
| 317 | + for idx, device in enumerate(devices): |
| 318 | + print(f"{idx + 1}. {device}") |
| 319 | + while True: |
| 320 | + try: |
| 321 | + choice = int(input("Select the device number to use: ")) |
| 322 | + if 1 <= choice <= len(devices): |
| 323 | + device_id = devices[choice - 1] |
| 324 | + print(f"Using device: {device_id}") |
| 325 | + break |
| 326 | + else: |
| 327 | + print(f"Please enter a number between 1 and {len(devices)}.") |
| 328 | + except ValueError: |
| 329 | + print("Invalid input. Please enter a number.") |
| 330 | + |
| 331 | + profiling_enabled = False |
| 332 | + ndk_path = None |
| 333 | + print("Profiling is disabled when multiple devices are connected.") |
| 334 | + |
| 335 | + compare_speeds = input("Do you want to compare download speeds between two browsers? (yes/no): ").lower() == 'yes' |
| 336 | + |
| 337 | + durations = {} |
| 338 | + |
| 339 | + if compare_speeds: |
| 340 | + for key in ['1', '2']: |
| 341 | + browser_info = browsers[key] |
| 342 | + print(f"\nTesting download speed with {browser_info['name']} on device {device_id}...") |
| 343 | + duration = test_download_speed(download_url, browser_info, device_id, profiling_enabled, ndk_path) |
| 344 | + if duration is not None: |
| 345 | + print(f"Download with {browser_info['name']} completed in {duration} seconds.") |
| 346 | + durations[browser_info['name']] = duration |
| 347 | + else: |
| 348 | + print(f"Download test with {browser_info['name']} failed.") |
| 349 | + else: |
| 350 | + print("Select a browser to test:") |
| 351 | + for key, browser in browsers.items(): |
| 352 | + print(f"{key}. {browser['name']}") |
| 353 | + browser_choice = input("Enter the number of the browser: ") |
| 354 | + while browser_choice not in browsers: |
| 355 | + print("Invalid choice. Please try again.") |
| 356 | + browser_choice = input("Enter the number of the browser: ") |
| 357 | + browser_info = browsers[browser_choice] |
| 358 | + print(f"\nTesting download speed with {browser_info['name']} on device {device_id}...") |
| 359 | + duration = test_download_speed(download_url, browser_info, device_id, profiling_enabled, ndk_path) |
| 360 | + if duration is not None: |
| 361 | + print(f"Download completed in {duration} seconds.") |
| 362 | + durations[browser_info['name']] = duration |
| 363 | + else: |
| 364 | + print("\nDownload test failed.") |
| 365 | + |
| 366 | + if compare_speeds and len(durations) == 2: |
| 367 | + print("\nDownload speed comparison:") |
| 368 | + for browser_name, duration in durations.items(): |
| 369 | + print(f"{browser_name}: {duration} seconds") |
| 370 | + diff = abs(durations['Firefox'] - durations['Chrome']) |
| 371 | + if durations['Firefox'] < durations['Chrome']: |
| 372 | + faster_browser = 'Firefox' |
| 373 | + elif durations['Chrome'] < durations['Firefox']: |
| 374 | + faster_browser = 'Chrome' |
| 375 | + else: |
| 376 | + faster_browser = None |
| 377 | + print("\nBoth browsers had the same download duration.") |
| 378 | + if faster_browser: |
| 379 | + print(f"\n{faster_browser} was faster by {diff} seconds.") |
| 380 | + elif len(durations) == 1: |
| 381 | + browser_name = list(durations.keys())[0] |
| 382 | + print(f"\nDownload with {browser_name} completed in {durations[browser_name]} seconds.") |
| 383 | + |
| 384 | + if profiling_enabled: |
| 385 | + print("\nTo view the profiling data, run the following command:") |
| 386 | + print("samply import perf.data --breakpad-symbol-server https://symbols.mozilla.org/") |
| 387 | + |
| 388 | +if __name__ == "__main__": |
| 389 | + main() |
0 commit comments