speed up registry load

This commit is contained in:
Xuwznln
2026-02-02 20:01:04 +08:00
parent 23ce145f74
commit 56eb7e2ab4

View File

@@ -4,6 +4,8 @@ import os
import sys import sys
import inspect import inspect
import importlib import importlib
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Union, Tuple from typing import Any, Dict, List, Union, Tuple
@@ -60,6 +62,7 @@ class Registry:
self.device_module_to_registry = {} self.device_module_to_registry = {}
self.resource_type_registry = {} self.resource_type_registry = {}
self._setup_called = False # 跟踪setup是否已调用 self._setup_called = False # 跟踪setup是否已调用
self._registry_lock = threading.Lock() # 多线程加载时的锁
# 其他状态变量 # 其他状态变量
# self.is_host_mode = False # 移至BasicConfig中 # self.is_host_mode = False # 移至BasicConfig中
@@ -177,8 +180,7 @@ class Registry:
"result": {}, "result": {},
"schema": test_latency_schema, "schema": test_latency_schema,
"goal_default": { "goal_default": {
arg["name"]: arg["default"] arg["name"]: arg["default"] for arg in test_latency_method_info.get("args", [])
for arg in test_latency_method_info.get("args", [])
}, },
"handles": {}, "handles": {},
}, },
@@ -262,18 +264,26 @@ class Registry:
# 标记setup已被调用 # 标记setup已被调用
self._setup_called = True self._setup_called = True
def load_resource_types(self, path: os.PathLike, complete_registry: bool, upload_registry: bool): def _load_single_resource_file(
abs_path = Path(path).absolute() self, file: Path, complete_registry: bool, upload_registry: bool
resource_path = abs_path / "resources" ) -> Tuple[Dict[str, Any], Dict[str, Any], bool]:
files = list(resource_path.glob("*/*.yaml")) """
logger.trace(f"[UniLab Registry] load resources? {resource_path.exists()}, total: {len(files)}") 加载单个资源文件 (线程安全)
current_resource_number = len(self.resource_type_registry) + 1
for i, file in enumerate(files): Returns:
(data, complete_data, is_valid): 资源数据, 完整数据, 是否有效
"""
try:
with open(file, encoding="utf-8", mode="r") as f: with open(file, encoding="utf-8", mode="r") as f:
data = yaml.safe_load(io.StringIO(f.read())) data = yaml.safe_load(io.StringIO(f.read()))
except Exception as e:
logger.warning(f"[UniLab Registry] 读取资源文件失败: {file}, 错误: {e}")
return {}, {}, False
if not data:
return {}, {}, False
complete_data = {} complete_data = {}
if data:
# 为每个资源添加文件路径信息
for resource_id, resource_info in data.items(): for resource_id, resource_info in data.items():
if "version" not in resource_info: if "version" not in resource_info:
resource_info["version"] = "1.0.0" resource_info["version"] = "1.0.0"
@@ -301,28 +311,68 @@ class Registry:
if len(class_info) and "module" in class_info: if len(class_info) and "module" in class_info:
if class_info.get("type") == "pylabrobot": if class_info.get("type") == "pylabrobot":
res_class = get_class(class_info["module"]) res_class = get_class(class_info["module"])
if callable(res_class) and not isinstance( if callable(res_class) and not isinstance(res_class, type):
res_class, type
): # 有的是类,有的是函数,这里暂时只登记函数类的
res_instance = res_class(res_class.__name__) res_instance = res_class(res_class.__name__)
res_ulr = tree_to_list([resource_plr_to_ulab(res_instance)]) res_ulr = tree_to_list([resource_plr_to_ulab(res_instance)])
resource_info["config_info"] = res_ulr resource_info["config_info"] = res_ulr
resource_info["registry_type"] = "resource" resource_info["registry_type"] = "resource"
resource_info["file_path"] = str(file.absolute()).replace("\\", "/") resource_info["file_path"] = str(file.absolute()).replace("\\", "/")
complete_data = dict(sorted(complete_data.items())) complete_data = dict(sorted(complete_data.items()))
complete_data = copy.deepcopy(complete_data) complete_data = copy.deepcopy(complete_data)
if complete_registry: if complete_registry:
try:
with open(file, "w", encoding="utf-8") as f: with open(file, "w", encoding="utf-8") as f:
yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper)
except Exception as e:
logger.warning(f"[UniLab Registry] 写入资源文件失败: {file}, 错误: {e}")
return data, complete_data, True
def load_resource_types(self, path: os.PathLike, complete_registry: bool, upload_registry: bool):
abs_path = Path(path).absolute()
resource_path = abs_path / "resources"
files = list(resource_path.glob("*/*.yaml"))
logger.debug(f"[UniLab Registry] resources: {resource_path.exists()}, total: {len(files)}")
if not files:
return
# 使用线程池并行加载
max_workers = min(8, len(files))
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_file = {
executor.submit(self._load_single_resource_file, file, complete_registry, upload_registry): file
for file in files
}
for future in as_completed(future_to_file):
file = future_to_file[future]
try:
data, complete_data, is_valid = future.result()
if is_valid:
results.append((file, data))
except Exception as e:
logger.warning(f"[UniLab Registry] 处理资源文件异常: {file}, 错误: {e}")
# 线程安全地更新注册表
current_resource_number = len(self.resource_type_registry) + 1
with self._registry_lock:
for i, (file, data) in enumerate(results):
self.resource_type_registry.update(data) self.resource_type_registry.update(data)
logger.trace( # type: ignore logger.trace(
f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(files)} " f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(results)} "
+ f"Add {list(data.keys())}" + f"Add {list(data.keys())}"
) )
current_resource_number += 1 current_resource_number += 1
else:
logger.debug(f"[UniLab Registry] Res File-{i+1}/{len(files)} Not Valid YAML File: {file.absolute()}") # 记录无效文件
valid_files = {r[0] for r in results}
for file in files:
if file not in valid_files:
logger.debug(f"[UniLab Registry] Res File Not Valid YAML File: {file.absolute()}")
def _extract_class_docstrings(self, module_string: str) -> Dict[str, str]: def _extract_class_docstrings(self, module_string: str) -> Dict[str, str]:
""" """
@@ -674,32 +724,34 @@ class Registry:
"handles": {}, "handles": {},
} }
def load_device_types(self, path: os.PathLike, complete_registry: bool): def _load_single_device_file(
# return self, file: Path, complete_registry: bool, get_yaml_from_goal_type
abs_path = Path(path).absolute() ) -> Tuple[Dict[str, Any], Dict[str, Any], bool, List[str]]:
devices_path = abs_path / "devices" """
device_comms_path = abs_path / "device_comms" 加载单个设备文件 (线程安全)
files = list(devices_path.glob("*.yaml")) + list(device_comms_path.glob("*.yaml"))
logger.trace( # type: ignore
f"[UniLab Registry] devices: {devices_path.exists()}, device_comms: {device_comms_path.exists()}, "
+ f"total: {len(files)}"
)
current_device_number = len(self.device_type_registry) + 1
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
for i, file in enumerate(files): Returns:
(data, complete_data, is_valid, device_ids): 设备数据, 完整数据, 是否有效, 设备ID列表
"""
try:
with open(file, encoding="utf-8", mode="r") as f: with open(file, encoding="utf-8", mode="r") as f:
data = yaml.safe_load(io.StringIO(f.read())) data = yaml.safe_load(io.StringIO(f.read()))
except Exception as e:
logger.warning(f"[UniLab Registry] 读取设备文件失败: {file}, 错误: {e}")
return {}, {}, False, []
if not data:
return {}, {}, False, []
complete_data = {} complete_data = {}
action_str_type_mapping = { action_str_type_mapping = {
"UniLabJsonCommand": "UniLabJsonCommand", "UniLabJsonCommand": "UniLabJsonCommand",
"UniLabJsonCommandAsync": "UniLabJsonCommandAsync", "UniLabJsonCommandAsync": "UniLabJsonCommandAsync",
} }
status_str_type_mapping = {} status_str_type_mapping = {}
if data: device_ids = []
# 在添加到注册表前处理类型替换
for device_id, device_config in data.items(): for device_id, device_config in data.items():
# 添加文件路径信息 - 使用规范化的完整文件路径
if "version" not in device_config: if "version" not in device_config:
device_config["version"] = "1.0.0" device_config["version"] = "1.0.0"
if "category" not in device_config: if "category" not in device_config:
@@ -717,10 +769,7 @@ class Registry:
if "init_param_schema" not in device_config: if "init_param_schema" not in device_config:
device_config["init_param_schema"] = {} device_config["init_param_schema"] = {}
if "class" in device_config: if "class" in device_config:
if ( if "status_types" not in device_config["class"] or device_config["class"]["status_types"] is None:
"status_types" not in device_config["class"]
or device_config["class"]["status_types"] is None
):
device_config["class"]["status_types"] = {} device_config["class"]["status_types"] = {}
if ( if (
"action_value_mappings" not in device_config["class"] "action_value_mappings" not in device_config["class"]
@@ -738,25 +787,17 @@ class Registry:
) )
for status_name, status_type in device_config["class"]["status_types"].items(): for status_name, status_type in device_config["class"]["status_types"].items():
if isinstance(status_type, tuple) or status_type in ["Any", "None", "Unknown"]: if isinstance(status_type, tuple) or status_type in ["Any", "None", "Unknown"]:
status_type = "String" # 替换成ROS的String便于显示 status_type = "String"
device_config["class"]["status_types"][status_name] = status_type device_config["class"]["status_types"][status_name] = status_type
try: try:
target_type = self._replace_type_with_class( target_type = self._replace_type_with_class(status_type, device_id, f"状态 {status_name}")
status_type, device_id, f"状态 {status_name}"
)
except ROSMsgNotFound: except ROSMsgNotFound:
continue continue
if target_type in [ if target_type in [dict, list]:
dict,
list,
]: # 对于嵌套类型返回的对象,暂时处理成字符串,无法直接进行转换
target_type = String target_type = String
status_str_type_mapping[status_type] = target_type status_str_type_mapping[status_type] = target_type
device_config["class"]["status_types"] = dict( device_config["class"]["status_types"] = dict(sorted(device_config["class"]["status_types"].items()))
sorted(device_config["class"]["status_types"].items())
)
if complete_registry: if complete_registry:
# 保存原有的 action 配置(用于保留 schema 的 description 和 handles 等)
old_action_configs = {} old_action_configs = {}
for action_name, action_config in device_config["class"]["action_value_mappings"].items(): for action_name, action_config in device_config["class"]["action_value_mappings"].items():
old_action_configs[action_name] = action_config old_action_configs[action_name] = action_config
@@ -766,7 +807,6 @@ class Registry:
for k, v in device_config["class"]["action_value_mappings"].items() for k, v in device_config["class"]["action_value_mappings"].items()
if not k.startswith("auto-") if not k.startswith("auto-")
} }
# 处理动作值映射
device_config["class"]["action_value_mappings"].update( device_config["class"]["action_value_mappings"].update(
{ {
f"auto-{k}": { f"auto-{k}": {
@@ -778,18 +818,15 @@ class Registry:
v["args"], v["args"],
k, k,
v.get("return_annotation"), v.get("return_annotation"),
# 传入旧的 schema 以保留字段 description
old_action_configs.get(f"auto-{k}", {}).get("schema"), old_action_configs.get(f"auto-{k}", {}).get("schema"),
), ),
"goal_default": {i["name"]: i["default"] for i in v["args"]}, "goal_default": {i["name"]: i["default"] for i in v["args"]},
# 保留原有的 handles 配置
"handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []), "handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []),
"placeholder_keys": { "placeholder_keys": {
i["name"]: ( i["name"]: (
"unilabos_resources" "unilabos_resources"
if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot"
or i["type"] or i["type"] == ("list", "unilabos.registry.placeholder_type:ResourceSlot")
== ("list", "unilabos.registry.placeholder_type:ResourceSlot")
else "unilabos_devices" else "unilabos_devices"
) )
for i in v["args"] for i in v["args"]
@@ -802,14 +839,12 @@ class Registry:
] ]
}, },
} }
# 不生成已配置action的动作
for k, v in enhanced_info["action_methods"].items() for k, v in enhanced_info["action_methods"].items()
if k not in device_config["class"]["action_value_mappings"] if k not in device_config["class"]["action_value_mappings"]
} }
) )
# 恢复原有的 description 信息(非 auto- 开头的动作)
for action_name, old_config in old_action_configs.items(): for action_name, old_config in old_action_configs.items():
if action_name in device_config["class"]["action_value_mappings"]: # 有一些会被删除 if action_name in device_config["class"]["action_value_mappings"]:
old_schema = old_config.get("schema", {}) old_schema = old_config.get("schema", {})
if "description" in old_schema and old_schema["description"]: if "description" in old_schema and old_schema["description"]:
device_config["class"]["action_value_mappings"][action_name]["schema"][ device_config["class"]["action_value_mappings"][action_name]["schema"][
@@ -838,7 +873,6 @@ class Registry:
action_config["handles"] = {} action_config["handles"] = {}
if "type" in action_config: if "type" in action_config:
action_type_str: str = action_config["type"] action_type_str: str = action_config["type"]
# 通过Json发放指令而不是通过特殊的ros action进行处理
if not action_type_str.startswith("UniLabJsonCommand"): if not action_type_str.startswith("UniLabJsonCommand"):
try: try:
target_type = self._replace_type_with_class( target_type = self._replace_type_with_class(
@@ -856,31 +890,78 @@ class Registry:
logger.warning( logger.warning(
f"[UniLab Registry] 设备 {device_id} 的动作 {action_name} 类型为空,跳过替换" f"[UniLab Registry] 设备 {device_id} 的动作 {action_name} 类型为空,跳过替换"
) )
complete_data[device_id] = copy.deepcopy(dict(sorted(device_config.items()))) # 稍后dump到文件 complete_data[device_id] = copy.deepcopy(dict(sorted(device_config.items())))
for status_name, status_type in device_config["class"]["status_types"].items(): for status_name, status_type in device_config["class"]["status_types"].items():
device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type] device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type]
for action_name, action_config in device_config["class"]["action_value_mappings"].items(): for action_name, action_config in device_config["class"]["action_value_mappings"].items():
if action_config["type"] not in action_str_type_mapping: if action_config["type"] not in action_str_type_mapping:
continue continue
action_config["type"] = action_str_type_mapping[action_config["type"]] action_config["type"] = action_str_type_mapping[action_config["type"]]
# 添加内置的驱动命令动作
self._add_builtin_actions(device_config, device_id) self._add_builtin_actions(device_config, device_id)
device_config["file_path"] = str(file.absolute()).replace("\\", "/") device_config["file_path"] = str(file.absolute()).replace("\\", "/")
device_config["registry_type"] = "device" device_config["registry_type"] = "device"
logger.trace( # type: ignore device_ids.append(device_id)
f"[UniLab Registry] Device-{current_device_number} File-{i+1}/{len(files)} Add {device_id} "
complete_data = dict(sorted(complete_data.items()))
complete_data = copy.deepcopy(complete_data)
try:
with open(file, "w", encoding="utf-8") as f:
yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper)
except Exception as e:
logger.warning(f"[UniLab Registry] 写入设备文件失败: {file}, 错误: {e}")
return data, complete_data, True, device_ids
def load_device_types(self, path: os.PathLike, complete_registry: bool):
abs_path = Path(path).absolute()
devices_path = abs_path / "devices"
device_comms_path = abs_path / "device_comms"
files = list(devices_path.glob("*.yaml")) + list(device_comms_path.glob("*.yaml"))
logger.trace(
f"[UniLab Registry] devices: {devices_path.exists()}, device_comms: {device_comms_path.exists()}, "
+ f"total: {len(files)}"
)
if not files:
return
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
# 使用线程池并行加载
max_workers = min(8, len(files))
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_file = {
executor.submit(self._load_single_device_file, file, complete_registry, get_yaml_from_goal_type): file
for file in files
}
for future in as_completed(future_to_file):
file = future_to_file[future]
try:
data, complete_data, is_valid, device_ids = future.result()
if is_valid:
results.append((file, data, device_ids))
except Exception as e:
logger.warning(f"[UniLab Registry] 处理设备文件异常: {file}, 错误: {e}")
# 线程安全地更新注册表
current_device_number = len(self.device_type_registry) + 1
with self._registry_lock:
for file, data, device_ids in results:
self.device_type_registry.update(data)
for device_id in device_ids:
logger.trace(
f"[UniLab Registry] Device-{current_device_number} Add {device_id} "
+ f"[{data[device_id].get('name', '未命名设备')}]" + f"[{data[device_id].get('name', '未命名设备')}]"
) )
current_device_number += 1 current_device_number += 1
complete_data = dict(sorted(complete_data.items()))
complete_data = copy.deepcopy(complete_data) # 记录无效文件
with open(file, "w", encoding="utf-8") as f: valid_files = {r[0] for r in results}
yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper) for file in files:
self.device_type_registry.update(data) if file not in valid_files:
else: logger.debug(f"[UniLab Registry] Device File Not Valid YAML File: {file.absolute()}")
logger.debug(
f"[UniLab Registry] Device File-{i+1}/{len(files)} Not Valid YAML File: {file.absolute()}"
)
def obtain_registry_device_info(self): def obtain_registry_device_info(self):
devices = [] devices = []