diff --git a/alas.py b/alas.py index 30cda023f..e639ce95a 100644 --- a/alas.py +++ b/alas.py @@ -315,30 +315,36 @@ class AzurLaneAutoScript: Returns: str: Name of the next task. """ - from module.base.memory_opt import release_memory_after_task, release_memory_when_idle - release_memory_after_task() + task = self.config.get_next() self.config.task = task self.config.bind(task) + from module.base.resource import release_resources + if self.config.task.command != 'Alas': + release_resources(next_task=task.command) + if task.next_run > datetime.now(): - release_memory_when_idle() logger.info(f'Wait until {task.next_run} for task `{task.command}`') method = self.config.Optimization_WhenTaskQueueEmpty if method == 'close_game': logger.info('Close game during wait') self.device.app_stop() + release_resources() self.wait_until(task.next_run) self.run('start') elif method == 'goto_main': logger.info('Goto main page during wait') self.run('goto_main') + release_resources() self.wait_until(task.next_run) elif method == 'stay_there': logger.info('Stay there during wait') + release_resources() self.wait_until(task.next_run) else: logger.warning(f'Invalid Optimization_WhenTaskQueueEmpty: {method}, fallback to stay_there') + release_resources() self.wait_until(task.next_run) AzurLaneConfig.is_hoarding_task = False diff --git a/module/base/button.py b/module/base/button.py index 4fac4cc2c..aaebe3e62 100644 --- a/module/base/button.py +++ b/module/base/button.py @@ -6,10 +6,11 @@ from PIL import ImageDraw import module.config.server as server from module.base.decorator import cached_property +from module.base.resource import Resource from module.base.utils import * -class Button: +class Button(Resource): def __init__(self, area, color, button, file=None, name=None): """Initialize a Button instance. @@ -50,6 +51,8 @@ class Button: else: self.is_gif = False + self.resource_add(key=self.file) + def __str__(self): return self.name @@ -131,6 +134,10 @@ class Button: self.image = load_image(self.file, self.area) self._match_init = True + def resource_release(self): + self.image = None + self._match_init = False + def match(self, image, offset=30, threshold=0.85): """Detects button by template matching. To Some button, its location may not be static. diff --git a/module/base/decorator.py b/module/base/decorator.py index fbede4f8c..6c10ba5ee 100644 --- a/module/base/decorator.py +++ b/module/base/decorator.py @@ -1,8 +1,7 @@ +import random import re from functools import wraps -import numpy as np - from module.logger import logger @@ -63,7 +62,7 @@ class Config: flag = [value is None or self.config.__getattribute__(key) == value for key, value in record['options'].items()] - if not np.all(flag): + if not all(flag): continue return record['func'](self, *args, **kwargs) @@ -117,7 +116,7 @@ def function_drop(rate=0.5, default=None): def decorate(func): @wraps(func) def wrapper(*args, **kwargs): - if np.random.uniform(0, 1) > rate: + if random.uniform(0, 1) > rate: return func(*args, **kwargs) else: cls = '' diff --git a/module/base/resource.py b/module/base/resource.py new file mode 100644 index 000000000..1f8ee3b13 --- /dev/null +++ b/module/base/resource.py @@ -0,0 +1,142 @@ +import re + +import gc +import psutil + +from module.base.decorator import cached_property +from module.logger import logger + + +def del_cached_property(obj, name): + """ + Delete a cached property safely. + + Args: + obj: + name (str): + """ + if hasattr(obj, name): + del obj.__dict__[name] + + +def watch_memory(func): + """ + Show memory changes in log + release_resources: 181.555MB -> 163.066MB + """ + + def wrapper(*args, **kwargs): + before = psutil.Process().memory_info().rss / (1024 * 1024) + result = func(*args, **kwargs) + after = psutil.Process().memory_info().rss / (1024 * 1024) + logger.info(f'{func.__name__}: {round(before, 3)}MB -> {round(after, 3)}MB') + return result + + return wrapper + + +def get_assets_from_file(file, regex): + assets = set() + with open(file, 'r', encoding='utf-8') as f: + for row in f.readlines(): + result = regex.search(row) + if result: + assets.add(result.group(1)) + return assets + + +class PreservedAssets: + @cached_property + def ui(self): + assets = set() + assets |= get_assets_from_file( + file='./module/ui/assets.py', + regex=re.compile(r'^([A-Za-z][A-Za-z0-9_]+) = ') + ) + assets |= get_assets_from_file( + file='./module/ui/ui.py', + regex=re.compile(r'\(([A-Z][A-Z0-9_]+),') + ) + assets |= get_assets_from_file( + file='./module/handler/info_handler.py', + regex=re.compile(r'\(([A-Z][A-Z0-9_]+),') + ) + # MAIN_CHECK == MAIN_GOTO_CAMPAIGN + assets.add('MAIN_GOTO_CAMPAIGN') + return assets + + +_preserved_assets = PreservedAssets() + + +class Resource: + instances = {} + + def resource_add(self, key): + Resource.instances[key] = self + + def resource_release(self): + pass + + @classmethod + def is_loaded(cls, obj): + if hasattr(obj, '_image') and obj._image is None: + return False + elif hasattr(obj, 'image') and obj.image is None: + return False + return True + + @classmethod + def resource_show(cls): + logger.hr('Show resource') + for key, obj in cls.instances.items(): + if cls.is_loaded(obj): + continue + logger.info(f'{obj}: {key}') + + +def release_resources(next_task=''): + # Release all OCR models + # Usually to have 2 models loaded and each model takes about 20MB + # This will release 20-40MB + from module.ocr.ocr import OCR_MODEL + if 'Opsi' in next_task or 'commission' in next_task: + # OCR models will be used soon, don't release + models = [] + elif next_task: + # Release OCR models except 'azur_lane' + models = ['cnocr', 'jp', 'tw'] + else: + models = ['azur_lane', 'cnocr', 'jp', 'tw'] + for model in models: + del_cached_property(OCR_MODEL, model) + + # Release assets cache + # module.ui has about 80 assets and takes about 3MB + # Alas has about 800 assets, but they are not all loaded. + # Template images take more, about 6MB each + for key, obj in Resource.instances.items(): + # Preserve assets for ui switching + if next_task and str(obj) in _preserved_assets.ui: + continue + # if Resource.is_loaded(obj): + # logger.info(f'Release {obj}') + obj.resource_release() + + # Release cached images for map detection + from module.map_detection.utils_assets import ASSETS + attr_list = [ + 'ui_mask', + 'ui_mask_os', + 'ui_mask_stroke', + 'ui_mask_in_map', + 'ui_mask_os_in_map', + 'tile_center_image', + 'tile_corner_image', + 'tile_corner_image_list' + ] + for attr in attr_list: + del_cached_property(ASSETS, attr) + + # Useless in most cases, but just call it + gc.collect() diff --git a/module/base/template.py b/module/base/template.py index 9bc1a57c9..3a220faaf 100644 --- a/module/base/template.py +++ b/module/base/template.py @@ -5,11 +5,12 @@ import imageio import module.config.server as server from module.base.button import Button from module.base.decorator import cached_property +from module.base.resource import Resource from module.base.utils import * from module.map_detection.utils import Points -class Template: +class Template(Resource): def __init__(self, file): """ Args: @@ -21,6 +22,8 @@ class Template: self.is_gif = os.path.splitext(self.file)[1] == '.gif' self._image = None + self.resource_add(self.file) + @property def image(self): if self._image is None: @@ -28,9 +31,10 @@ class Template: self._image = [] for image in imageio.mimread(self.file): image = image[:, :, :3].copy() if len(image.shape) == 3 else image + image = self.pre_process(image) self._image += [image, cv2.flip(image, 1)] else: - self._image = load_image(self.file) + self._image = self.pre_process(load_image(self.file)) return self._image @@ -38,6 +42,19 @@ class Template: def image(self, value): self._image = value + def resource_release(self): + self._image = None + + def pre_process(self, image): + """ + Args: + image (np.ndarray): + + Returns: + np.ndarray: + """ + return image + @cached_property def size(self): if self.is_gif: diff --git a/module/handler/ambush.py b/module/handler/ambush.py index ac693e90b..6afa85b49 100644 --- a/module/handler/ambush.py +++ b/module/handler/ambush.py @@ -1,6 +1,5 @@ import numpy as np -from module.base.decorator import cached_property from module.base.timer import Timer from module.base.utils import red_overlay_transparency, get_color from module.combat.combat import Combat @@ -9,23 +8,16 @@ from module.handler.info_handler import info_letter_preprocess from module.logger import logger from module.template.assets import * +TEMPLATE_AMBUSH_EVADE_SUCCESS.pre_process = info_letter_preprocess +TEMPLATE_AMBUSH_EVADE_FAILED.pre_process = info_letter_preprocess +TEMPLATE_MAP_WALK_OUT_OF_STEP.pre_process = info_letter_preprocess + class AmbushHandler(Combat): MAP_AMBUSH_OVERLAY_TRANSPARENCY_THRESHOLD = 0.40 MAP_AIR_RAID_OVERLAY_TRANSPARENCY_THRESHOLD = 0.35 # Usually (0.50, 0.53) MAP_AIR_RAID_CONFIRM_SECOND = 0.5 - @cached_property - def _load_ambush_template(self): - TEMPLATE_AMBUSH_EVADE_SUCCESS.image = info_letter_preprocess(TEMPLATE_AMBUSH_EVADE_SUCCESS.image) - TEMPLATE_AMBUSH_EVADE_FAILED.image = info_letter_preprocess(TEMPLATE_AMBUSH_EVADE_FAILED.image) - return True - - @cached_property - def _load_walk_template(self): - TEMPLATE_MAP_WALK_OUT_OF_STEP.image = info_letter_preprocess(TEMPLATE_MAP_WALK_OUT_OF_STEP.image) - return True - def ambush_color_initial(self): MAP_AMBUSH.load_color(self.device.image) MAP_AIR_RAID.load_color(self.device.image) @@ -52,7 +44,6 @@ class AmbushHandler(Combat): def _handle_ambush_evade(self): logger.info('Map ambushed') - _ = self._load_ambush_template self.wait_until_appear_then_click(MAP_AMBUSH_EVADE) self.wait_until_appear(INFO_BAR_1) @@ -119,7 +110,6 @@ class AmbushHandler(Combat): if not self.appear(INFO_BAR_1): return False - _ = self._load_walk_template image = info_letter_preprocess(np.array(self.image_crop(INFO_BAR_DETECT))) if TEMPLATE_MAP_WALK_OUT_OF_STEP.match(image): logger.warning('Map walk out of step.') diff --git a/module/retire/enhancement.py b/module/retire/enhancement.py index ce9c67fa2..dcf25762b 100644 --- a/module/retire/enhancement.py +++ b/module/retire/enhancement.py @@ -1,7 +1,7 @@ from random import choice import numpy as np -from module.base.decorator import cached_property + from module.base.timer import Timer from module.combat.assets import GET_ITEMS_1 from module.handler.assets import INFO_BAR_DETECT @@ -12,6 +12,9 @@ from module.retire.dock import Dock, CARD_GRIDS from module.template.assets import TEMPLATE_ENHANCE_SUCCESS, TEMPLATE_ENHANCE_FAILED, TEMPLATE_ENHANCE_IN_BATTLE VALID_SHIP_TYPES = ['dd', 'ss', 'cl', 'ca', 'bb', 'cv', 'repair', 'others'] +TEMPLATE_ENHANCE_SUCCESS.pre_process = info_letter_preprocess +TEMPLATE_ENHANCE_FAILED.pre_process = info_letter_preprocess +TEMPLATE_ENHANCE_IN_BATTLE.pre_process = info_letter_preprocess class Enhancement(Dock): @@ -23,13 +26,6 @@ class Enhancement(Dock): return 10 return 2000 - @cached_property - def _load_enhance_template(self): - TEMPLATE_ENHANCE_SUCCESS.image = info_letter_preprocess(TEMPLATE_ENHANCE_SUCCESS.image) - TEMPLATE_ENHANCE_FAILED.image = info_letter_preprocess(TEMPLATE_ENHANCE_FAILED.image) - TEMPLATE_ENHANCE_IN_BATTLE.image = info_letter_preprocess(TEMPLATE_ENHANCE_IN_BATTLE.image) - return True - def _enhance_enter(self, favourite=False, ship_type=None): """ Pages: @@ -133,7 +129,6 @@ class Enhancement(Dock): True if able to enhance otherwise False Always paired with current ship_count """ - _ = self._load_enhance_template skip_until_ensured = True enhanced = False while 1: