From 40d91b266c77d33c2d9e549629748132232320de Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Fri, 15 May 2026 20:45:06 +0000 Subject: [PATCH 1/9] Optimize Android emulator initialization by batching reboots This change reduces the number of reboots during Android device setup by: - Adding a wait_for_reboot parameter to adb.write_data_to_file. - Tracking reboot status in initialize_device to skip the final reboot if one already occurred. - Disabling reboots when setting sanitizer options since the app restart is sufficient. These optimizations improve bot startup efficiency and overall fuzzing throughput. --- .../_internal/platforms/android/adb.py | 4 ++-- .../_internal/platforms/android/device.py | 17 +++++++++++------ .../_internal/platforms/android/sanitizer.py | 14 +++++++++----- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/adb.py b/src/clusterfuzz/_internal/platforms/android/adb.py index 47c59b31c72..1f5eefcd861 100755 --- a/src/clusterfuzz/_internal/platforms/android/adb.py +++ b/src/clusterfuzz/_internal/platforms/android/adb.py @@ -880,7 +880,7 @@ def write_command_line_file(command_line, app_path): write_data_to_file(command_line_file_contents, command_line_path) -def write_data_to_file(contents, file_path): +def write_data_to_file(contents, file_path, wait_for_reboot=True): """Writes content to file.""" # If this is a file in /system, we need to remount /system as read-write and # after file is written, revert it back to read-only. @@ -895,6 +895,6 @@ def write_data_to_file(contents, file_path): # Make command line file is readable. run_shell_command('chmod 0644 %s' % file_path, root=True) - if is_system_file: + if is_system_file and wait_for_reboot: reboot() wait_until_fully_booted() diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index 0c8d81ca686..e13ddf6dca4 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -195,9 +195,9 @@ def configure_system_build_properties(): current_md5 = adb.get_file_checksum(BUILD_PROP_PATH) if current_md5 is None: logs.error('Unable to find %s on device.' % BUILD_PROP_PATH) - return + return False if old_md5 == current_md5: - return + return False # Pull to tmp file. bot_tmp_directory = environment.get_value('BOT_TMPDIR') @@ -205,7 +205,7 @@ def configure_system_build_properties(): adb.run_command(['pull', BUILD_PROP_PATH, old_build_prop_path]) if not os.path.exists(old_build_prop_path): logs.error('Unable to fetch %s from device.' % BUILD_PROP_PATH) - return + return False # Write new build.prop. new_build_prop_path = os.path.join(bot_tmp_directory, 'new.prop') @@ -228,11 +228,13 @@ def configure_system_build_properties(): # Keep verified boot disabled for M and higher releases. This makes it easy # to modify system's app_process to load asan libraries. + reboot_done = False build_version = settings.get_build_version() if is_build_at_least(build_version, 'M'): adb.run_as_root() adb.run_command('disable-verity') reboot() + reboot_done = True # Make /system writable. adb.run_as_root() @@ -255,6 +257,8 @@ def configure_system_build_properties(): current_md5 = adb.get_file_checksum(BUILD_PROP_PATH) persistent_cache.set_value(constants.BUILD_PROP_MD5_KEY, current_md5) + return reboot_done + def get_debug_props_and_values(): """Return debug property names and values based on |ENABLE_DEBUG_CHECKS| @@ -306,16 +310,17 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - configure_system_build_properties() + reboot_done = configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - sanitizer.setup_asan_if_needed() + reboot_done = sanitizer.setup_asan_if_needed() or reboot_done # Reboot device as above steps would need it and also it brings device in a # good state. - reboot() + if not reboot_done: + reboot() # Make sure we are running as root after restart. adb.run_as_root() diff --git a/src/clusterfuzz/_internal/platforms/android/sanitizer.py b/src/clusterfuzz/_internal/platforms/android/sanitizer.py index 832f6a4a577..f5d954e0a2c 100644 --- a/src/clusterfuzz/_internal/platforms/android/sanitizer.py +++ b/src/clusterfuzz/_internal/platforms/android/sanitizer.py @@ -69,7 +69,9 @@ def set_options(sanitizer_tool_name, sanitizer_options): if not sanitizer_options_file_path: return - adb.write_data_to_file(sanitizer_options, sanitizer_options_file_path) + # Skip reboot as the app will pick up the options file on restart. + adb.write_data_to_file( + sanitizer_options, sanitizer_options_file_path, wait_for_reboot=False) def setup_asan_if_needed(): @@ -78,16 +80,16 @@ def setup_asan_if_needed(): # Only do this step if explicitly enabled in the job type. This cannot be # determined from libraries in application directory since they can go # missing in a bad build, so we want to catch that. - return + return False if settings.get_sanitizer_tool_name(): # If this is a sanitizer build, no need to setup ASAN (incompatible). - return + return False app_directory = environment.get_value('APP_DIR') if not app_directory: # No app directory -> No ASAN runtime library. No work to do, bail out. - return + return False # Initialize variables. android_directory = environment.get_platform_resources_directory() @@ -108,7 +110,7 @@ def setup_asan_if_needed(): result = process.run_and_wait() if result.return_code: logs.error('Failed to setup ASan on device.', output=result.output) - return + return False logs.info( 'ASan device setup script successfully finished, waiting for boot.', @@ -117,3 +119,5 @@ def setup_asan_if_needed(): # Wait until fully booted as otherwise shell restart followed by a quick # reboot can trigger data corruption in /data/data. adb.wait_until_fully_booted() + + return True From 13904960c99b771c85c398be393728cbb1884a06 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 16:57:40 +0000 Subject: [PATCH 2/9] Fix reboot_done tracking to ensure device settings are applied --- src/clusterfuzz/_internal/platforms/android/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index e13ddf6dca4..da186f4060f 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -310,12 +310,12 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - reboot_done = configure_system_build_properties() + configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - reboot_done = sanitizer.setup_asan_if_needed() or reboot_done + reboot_done = sanitizer.setup_asan_if_needed() # Reboot device as above steps would need it and also it brings device in a # good state. From e032f7a6c906f09679c6b08a739f975fb61cf4d3 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 17:05:15 +0000 Subject: [PATCH 3/9] Revert previous fix and use accumulated state for reboot_done --- src/clusterfuzz/_internal/platforms/android/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index da186f4060f..e13ddf6dca4 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -310,12 +310,12 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - configure_system_build_properties() + reboot_done = configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - reboot_done = sanitizer.setup_asan_if_needed() + reboot_done = sanitizer.setup_asan_if_needed() or reboot_done # Reboot device as above steps would need it and also it brings device in a # good state. From 0629dec273fcdbf4bd3c505a976417a7f3554e00 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 17:12:12 +0000 Subject: [PATCH 4/9] Invert logic to needs_reboot and use bitwise OR operator to fix state tracking and short-circuit issues --- src/clusterfuzz/_internal/platforms/android/device.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index e13ddf6dca4..570edbde43e 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -228,13 +228,11 @@ def configure_system_build_properties(): # Keep verified boot disabled for M and higher releases. This makes it easy # to modify system's app_process to load asan libraries. - reboot_done = False build_version = settings.get_build_version() if is_build_at_least(build_version, 'M'): adb.run_as_root() adb.run_command('disable-verity') reboot() - reboot_done = True # Make /system writable. adb.run_as_root() @@ -257,7 +255,7 @@ def configure_system_build_properties(): current_md5 = adb.get_file_checksum(BUILD_PROP_PATH) persistent_cache.set_value(constants.BUILD_PROP_MD5_KEY, current_md5) - return reboot_done + return True def get_debug_props_and_values(): @@ -310,16 +308,16 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - reboot_done = configure_system_build_properties() + needs_reboot = configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - reboot_done = sanitizer.setup_asan_if_needed() or reboot_done + needs_reboot |= sanitizer.setup_asan_if_needed() # Reboot device as above steps would need it and also it brings device in a # good state. - if not reboot_done: + if needs_reboot: reboot() # Make sure we are running as root after restart. From 7c64b50db74444e5c31887fe45f4e9675e5b763a Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 17:14:42 +0000 Subject: [PATCH 5/9] Fix needs_reboot logic so ASan setup clears pending reboots --- src/clusterfuzz/_internal/platforms/android/device.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index 570edbde43e..a80826ba465 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -313,7 +313,10 @@ def initialize_device(): add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - needs_reboot |= sanitizer.setup_asan_if_needed() + if sanitizer.setup_asan_if_needed(): + # ASan setup script performs a shell restart and waits for boot. + # This satisfies any pending reboot requirements from earlier steps. + needs_reboot = False # Reboot device as above steps would need it and also it brings device in a # good state. From 9836c8f2b54237e5038ec94409904f767f280fdc Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 17:17:37 +0000 Subject: [PATCH 6/9] Fix initialization reboot logic to correctly preserve base requirements --- src/clusterfuzz/_internal/platforms/android/device.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index a80826ba465..0cc1512c2ff 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -308,19 +308,16 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - needs_reboot = configure_system_build_properties() + needs_full_reboot = configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - if sanitizer.setup_asan_if_needed(): - # ASan setup script performs a shell restart and waits for boot. - # This satisfies any pending reboot requirements from earlier steps. - needs_reboot = False + asan_reboot_done = sanitizer.setup_asan_if_needed() # Reboot device as above steps would need it and also it brings device in a # good state. - if needs_reboot: + if needs_full_reboot or not asan_reboot_done: reboot() # Make sure we are running as root after restart. From 2d75802119012a77448fe8ab908dd7cbc440144e Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 17:21:36 +0000 Subject: [PATCH 7/9] Add unit tests for android device reboot logic --- .../core/platforms/android/device_test.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py index e10051fb7ef..726739dcad7 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py @@ -13,6 +13,9 @@ # limitations under the License. """Tests for device functions.""" +import unittest +from unittest import mock + from clusterfuzz._internal.platforms.android import device from clusterfuzz._internal.tests.test_libs import android_helpers @@ -23,3 +26,60 @@ class InitializeEnvironmentTest(android_helpers.AndroidTest): def test(self): """Ensure that initialize_environment throws no exceptions.""" device.initialize_environment() + + +class InitializeDeviceRebootLogicTest(unittest.TestCase): + """Tests the reboot batching logic in initialize_device.""" + + def setUp(self): + # Mock environment to bypass the engine fuzzer check + mock.patch('clusterfuzz._internal.system.environment.is_engine_fuzzer_job', + return_value=False).start() + + # Mock all the setup steps so we don't actually run ADB commands + self.mock_setup_adb = mock.patch('clusterfuzz._internal.platforms.android.adb.setup_adb').start() + self.mock_run_as_root = mock.patch('clusterfuzz._internal.platforms.android.adb.run_as_root').start() + self.mock_config_props = mock.patch('clusterfuzz._internal.platforms.android.device.configure_system_build_properties').start() + self.mock_config_settings = mock.patch('clusterfuzz._internal.platforms.android.device.configure_device_settings').start() + self.mock_add_accounts = mock.patch('clusterfuzz._internal.platforms.android.device.add_test_accounts_if_needed').start() + self.mock_setup_asan = mock.patch('clusterfuzz._internal.platforms.android.sanitizer.setup_asan_if_needed').start() + + # Mock the reboot function we are trying to track + self.mock_reboot = mock.patch('clusterfuzz._internal.platforms.android.device.reboot').start() + + # Mock the post-reboot steps + mock.patch('clusterfuzz._internal.platforms.android.wifi.configure').start() + mock.patch('clusterfuzz._internal.platforms.android.device.setup_host_and_device_forwarder_if_needed').start() + mock.patch('clusterfuzz._internal.platforms.android.settings.change_se_linux_to_permissive_mode').start() + mock.patch('clusterfuzz._internal.platforms.android.app.wait_until_optimization_complete').start() + mock.patch('clusterfuzz._internal.platforms.android.ui.clear_notifications').start() + mock.patch('clusterfuzz._internal.platforms.android.ui.unlock_screen').start() + + def tearDown(self): + mock.patch.stopall() + + def test_reboot_if_props_changed(self): + """Test that it reboots if build.prop changed, even if ASan didn't run.""" + self.mock_config_props.return_value = True + self.mock_setup_asan.return_value = False + + device.initialize_device() + self.mock_reboot.assert_called_once() + + def test_no_reboot_if_asan_ran(self): + """Test that the final reboot is skipped if ASan setup performed a shell restart.""" + # If build.prop didn't change, the ASan restart handles the clean state. + self.mock_config_props.return_value = False + self.mock_setup_asan.return_value = True + + device.initialize_device() + self.mock_reboot.assert_not_called() + + def test_reboot_if_clean_slate_needed(self): + """Test that it still reboots to ensure a clean state if no other steps restarted it.""" + self.mock_config_props.return_value = False + self.mock_setup_asan.return_value = False + + device.initialize_device() + self.mock_reboot.assert_called_once() + From 78fd5f983da5f1bc1c16a22a498f9375c253e1cc Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Mon, 18 May 2026 19:23:24 +0000 Subject: [PATCH 8/9] Fix formatting issues from linter in device_test.py --- .../core/platforms/android/device_test.py | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py index 726739dcad7..7a5f792f545 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py @@ -33,27 +33,47 @@ class InitializeDeviceRebootLogicTest(unittest.TestCase): def setUp(self): # Mock environment to bypass the engine fuzzer check - mock.patch('clusterfuzz._internal.system.environment.is_engine_fuzzer_job', - return_value=False).start() - + mock.patch( + 'clusterfuzz._internal.system.environment.is_engine_fuzzer_job', + return_value=False).start() + # Mock all the setup steps so we don't actually run ADB commands - self.mock_setup_adb = mock.patch('clusterfuzz._internal.platforms.android.adb.setup_adb').start() - self.mock_run_as_root = mock.patch('clusterfuzz._internal.platforms.android.adb.run_as_root').start() - self.mock_config_props = mock.patch('clusterfuzz._internal.platforms.android.device.configure_system_build_properties').start() - self.mock_config_settings = mock.patch('clusterfuzz._internal.platforms.android.device.configure_device_settings').start() - self.mock_add_accounts = mock.patch('clusterfuzz._internal.platforms.android.device.add_test_accounts_if_needed').start() - self.mock_setup_asan = mock.patch('clusterfuzz._internal.platforms.android.sanitizer.setup_asan_if_needed').start() - + self.mock_setup_adb = mock.patch( + 'clusterfuzz._internal.platforms.android.adb.setup_adb').start() + self.mock_run_as_root = mock.patch( + 'clusterfuzz._internal.platforms.android.adb.run_as_root').start() + self.mock_config_props = mock.patch( + 'clusterfuzz._internal.platforms.android.device.configure_system_build_properties' + ).start() + self.mock_config_settings = mock.patch( + 'clusterfuzz._internal.platforms.android.device.configure_device_settings' + ).start() + self.mock_add_accounts = mock.patch( + 'clusterfuzz._internal.platforms.android.device.add_test_accounts_if_needed' + ).start() + self.mock_setup_asan = mock.patch( + 'clusterfuzz._internal.platforms.android.sanitizer.setup_asan_if_needed' + ).start() + # Mock the reboot function we are trying to track - self.mock_reboot = mock.patch('clusterfuzz._internal.platforms.android.device.reboot').start() - + self.mock_reboot = mock.patch( + 'clusterfuzz._internal.platforms.android.device.reboot').start() + # Mock the post-reboot steps mock.patch('clusterfuzz._internal.platforms.android.wifi.configure').start() - mock.patch('clusterfuzz._internal.platforms.android.device.setup_host_and_device_forwarder_if_needed').start() - mock.patch('clusterfuzz._internal.platforms.android.settings.change_se_linux_to_permissive_mode').start() - mock.patch('clusterfuzz._internal.platforms.android.app.wait_until_optimization_complete').start() - mock.patch('clusterfuzz._internal.platforms.android.ui.clear_notifications').start() - mock.patch('clusterfuzz._internal.platforms.android.ui.unlock_screen').start() + mock.patch( + 'clusterfuzz._internal.platforms.android.device.setup_host_and_device_forwarder_if_needed' + ).start() + mock.patch( + 'clusterfuzz._internal.platforms.android.settings.change_se_linux_to_permissive_mode' + ).start() + mock.patch( + 'clusterfuzz._internal.platforms.android.app.wait_until_optimization_complete' + ).start() + mock.patch('clusterfuzz._internal.platforms.android.ui.clear_notifications' + ).start() + mock.patch( + 'clusterfuzz._internal.platforms.android.ui.unlock_screen').start() def tearDown(self): mock.patch.stopall() @@ -62,16 +82,16 @@ def test_reboot_if_props_changed(self): """Test that it reboots if build.prop changed, even if ASan didn't run.""" self.mock_config_props.return_value = True self.mock_setup_asan.return_value = False - + device.initialize_device() self.mock_reboot.assert_called_once() def test_no_reboot_if_asan_ran(self): """Test that the final reboot is skipped if ASan setup performed a shell restart.""" # If build.prop didn't change, the ASan restart handles the clean state. - self.mock_config_props.return_value = False + self.mock_config_props.return_value = False self.mock_setup_asan.return_value = True - + device.initialize_device() self.mock_reboot.assert_not_called() @@ -79,7 +99,6 @@ def test_reboot_if_clean_slate_needed(self): """Test that it still reboots to ensure a clean state if no other steps restarted it.""" self.mock_config_props.return_value = False self.mock_setup_asan.return_value = False - + device.initialize_device() self.mock_reboot.assert_called_once() - From 017b912c8cd07a2b18426e3ba3de0cf1e330f5f0 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Mon, 18 May 2026 23:07:33 +0000 Subject: [PATCH 9/9] Address PR 5280 comments: Add return types and docstrings Adds `-> bool` return type hints and explanatory docstrings to: - `configure_system_build_properties` - `setup_asan_if_needed` These updates clarify that the returned boolean indicates whether a device reboot occurred during the setup step. --- src/clusterfuzz/_internal/platforms/android/device.py | 8 ++++++-- src/clusterfuzz/_internal/platforms/android/sanitizer.py | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index 0cc1512c2ff..5a4461a84a5 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -183,9 +183,13 @@ def configure_device_settings(): adb.write_data_to_file(local_properties_file_contents, LOCAL_PROP_PATH) -def configure_system_build_properties(): +def configure_system_build_properties() -> bool: """Modifies system build properties in /system/build.prop for better boot - speed and power use.""" + speed and power use. + + Returns: + True if the device was rebooted during configuration, False otherwise. + """ adb.run_as_root() # Check md5 checksum of build.prop to see if already updated, diff --git a/src/clusterfuzz/_internal/platforms/android/sanitizer.py b/src/clusterfuzz/_internal/platforms/android/sanitizer.py index f5d954e0a2c..de51bd4207b 100644 --- a/src/clusterfuzz/_internal/platforms/android/sanitizer.py +++ b/src/clusterfuzz/_internal/platforms/android/sanitizer.py @@ -74,8 +74,12 @@ def set_options(sanitizer_tool_name, sanitizer_options): sanitizer_options, sanitizer_options_file_path, wait_for_reboot=False) -def setup_asan_if_needed(): - """Set up asan on device.""" +def setup_asan_if_needed() -> bool: + """Set up asan on device. + + Returns: + True if the device was rebooted or restarted during setup, False otherwise. + """ if not environment.get_value('ASAN_DEVICE_SETUP'): # Only do this step if explicitly enabled in the job type. This cannot be # determined from libraries in application directory since they can go