QGIS提升速度
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
"""
|
||||
模板缓存引擎。解决 QgsProject 单例问题。
|
||||
模板缓存引擎(支持持久化)。
|
||||
|
||||
流程:
|
||||
1. 首次请求:project.read() 加载模板(慢,仅一次)
|
||||
2. 加载后 project.write() 保存到临时文件
|
||||
3. 后续同模板请求:从临时文件恢复(快,连接复用)
|
||||
4. 手动恢复文本/过滤/缩放(毫秒级)
|
||||
1. 首次加载:TemplateModifier 修改模板 → project.read() → 保存到磁盘缓存
|
||||
2. 同进程内缓存命中:从内存恢复(~1s)
|
||||
3. 跨进程/重启:从磁盘缓存恢复(首次 ~5s,后续 ~1s)
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
import tempfile
|
||||
|
||||
@@ -17,36 +19,37 @@ from app.config.paths import get_logger
|
||||
|
||||
logger = get_logger("qgis.cache")
|
||||
|
||||
# 持久化缓存目录(项目根目录下)
|
||||
_CACHE_DIR = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
|
||||
"app", "data", "cache", "qgis_templates",
|
||||
)
|
||||
_INDEX_FILE = os.path.join(_CACHE_DIR, "index.json")
|
||||
|
||||
|
||||
class TemplateCache:
|
||||
def __init__(self):
|
||||
self._cache: dict[str, dict] = {}
|
||||
|
||||
# ── 公开接口 ──
|
||||
|
||||
def is_loaded(self, template_path: str) -> bool:
|
||||
return template_path in self._cache
|
||||
|
||||
def load_template(self, template_path: str, layout_name: str = "A4") -> None:
|
||||
"""首次加载模板"""
|
||||
start = time.time()
|
||||
def save_current(self, template_path: str, layout_name: str = "A4") -> None:
|
||||
"""将当前已加载的项目保存到内存缓存 + 磁盘缓存"""
|
||||
project = QgsProject.instance()
|
||||
|
||||
logger.info(f"首次加载: {os.path.basename(template_path)}")
|
||||
project.read(template_path)
|
||||
logger.info(f"project.read() 耗时: {time.time() - start:.1f}s")
|
||||
|
||||
# 保存到临时文件
|
||||
tmp_file = tempfile.NamedTemporaryFile(
|
||||
suffix=".qgz", delete=False,
|
||||
dir=tempfile.gettempdir(),
|
||||
)
|
||||
tmp_path = tmp_file.name
|
||||
tmp_file.close()
|
||||
# ── 保存项目到持久化缓存目录 ──
|
||||
os.makedirs(_CACHE_DIR, exist_ok=True)
|
||||
key = self._path_key(template_path)
|
||||
cache_file = os.path.join(_CACHE_DIR, f"{key}.qgz")
|
||||
|
||||
t_save = time.time()
|
||||
project.write(tmp_path)
|
||||
logger.info(f"项目保存耗时: {time.time() - t_save:.1f}s")
|
||||
project.write(cache_file)
|
||||
logger.info(f" 缓存写入磁盘: {os.path.basename(cache_file)} ({time.time() - t_save:.1f}s)")
|
||||
|
||||
# 记录初始状态
|
||||
# ── 记录状态 ──
|
||||
texts = {}
|
||||
extent = None
|
||||
layout = project.layoutManager().layoutByName(layout_name)
|
||||
@@ -59,38 +62,41 @@ class TemplateCache:
|
||||
if map_item:
|
||||
extent = QgsRectangle(map_item.extent())
|
||||
|
||||
self._cache[template_path] = {
|
||||
"file": tmp_path,
|
||||
entry = {
|
||||
"file": cache_file,
|
||||
"texts": texts,
|
||||
"extent": extent,
|
||||
"layout": layout_name,
|
||||
}
|
||||
logger.info(f"模板加载完成,总耗时: {time.time() - start:.1f}s")
|
||||
self._cache[template_path] = entry
|
||||
|
||||
# ── 更新索引文件 ──
|
||||
self._update_index(template_path, entry)
|
||||
logger.info(f" 模板已缓存: {os.path.basename(template_path)} ({len(self._cache)} 个)")
|
||||
|
||||
def restore_template(self, template_path: str) -> tuple:
|
||||
"""从缓存恢复模板"""
|
||||
"""从缓存恢复模板(跳过图层解析以加速)"""
|
||||
cached = self._cache.get(template_path)
|
||||
if not cached:
|
||||
raise RuntimeError(f"模板未缓存: {template_path}")
|
||||
|
||||
start = time.time()
|
||||
project = QgsProject.instance()
|
||||
t0 = time.time()
|
||||
|
||||
logger.info(f"恢复模板: {os.path.basename(template_path)}")
|
||||
project.read(cached["file"])
|
||||
logger.info(f"project.read() 耗时: {time.time() - start:.1f}s")
|
||||
# FlagDontResolveLayers: 跳过数据库连接验证,缓存文件已含正确连接
|
||||
flags = QgsProject.ReadFlags()
|
||||
flags |= QgsProject.FlagDontResolveLayers
|
||||
project.read(cached["file"], flags)
|
||||
|
||||
logger.debug(f" 恢复耗时: {time.time() - t0:.1f}s")
|
||||
return project, cached["texts"], cached["extent"]
|
||||
|
||||
def reset_project_state(self, project: QgsProject, texts: dict, extent) -> None:
|
||||
"""重置项目到干净状态"""
|
||||
start = time.time()
|
||||
|
||||
for layer in project.mapLayers().values():
|
||||
if layer.subsetString():
|
||||
layer.setSubsetString("")
|
||||
|
||||
# 获取 layout 名称(从缓存中)
|
||||
layout_name = "A4"
|
||||
for cached in self._cache.values():
|
||||
layout_name = cached.get("layout", "A4")
|
||||
@@ -107,14 +113,82 @@ class TemplateCache:
|
||||
if map_item:
|
||||
map_item.zoomToExtent(extent)
|
||||
|
||||
logger.info(f"状态重置耗时: {time.time() - start:.3f}s")
|
||||
def load_persistent_cache(self) -> int:
|
||||
"""从磁盘恢复所有缓存到内存(qgis_runner 启动时调用)"""
|
||||
if not os.path.isfile(_INDEX_FILE):
|
||||
logger.info(" 磁盘缓存为空,首次启动需要加载模板")
|
||||
return 0
|
||||
|
||||
try:
|
||||
with open(_INDEX_FILE, "r", encoding="utf-8") as f:
|
||||
index = json.load(f)
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
logger.warning(f" 缓存索引损坏: {e}")
|
||||
return 0
|
||||
|
||||
count = 0
|
||||
for template_path, entry in index.get("templates", {}).items():
|
||||
cache_file = entry.get("file", "")
|
||||
if not cache_file or not os.path.isfile(cache_file):
|
||||
logger.debug(f" 跳过无效缓存: {os.path.basename(template_path)}")
|
||||
continue
|
||||
texts = entry.get("texts", {})
|
||||
extent = entry.get("extent")
|
||||
# 反序列化 extent
|
||||
if extent and isinstance(extent, dict):
|
||||
extent = QgsRectangle(
|
||||
extent.get("xmin", 0), extent.get("ymin", 0),
|
||||
extent.get("xmax", 0), extent.get("ymax", 0),
|
||||
)
|
||||
else:
|
||||
extent = None
|
||||
self._cache[template_path] = {
|
||||
"file": cache_file,
|
||||
"texts": texts,
|
||||
"extent": extent,
|
||||
"layout": entry.get("layout", "A4"),
|
||||
}
|
||||
count += 1
|
||||
|
||||
logger.info(f" 磁盘缓存加载: {count} 个模板")
|
||||
return count
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""清理所有临时文件"""
|
||||
for cached in self._cache.values():
|
||||
try:
|
||||
os.remove(cached["file"])
|
||||
except OSError:
|
||||
pass
|
||||
"""清理内存缓存(磁盘缓存保留)"""
|
||||
self._cache.clear()
|
||||
logger.info("已清理所有缓存")
|
||||
logger.info("已清理内存缓存")
|
||||
|
||||
# ── 内部方法 ──
|
||||
|
||||
@staticmethod
|
||||
def _path_key(template_path: str) -> str:
|
||||
"""模板路径 → 缓存文件名 key"""
|
||||
return hashlib.md5(template_path.encode()).hexdigest()[:16]
|
||||
|
||||
def _update_index(self, template_path: str, entry: dict) -> None:
|
||||
"""更新磁盘索引文件"""
|
||||
index = {}
|
||||
if os.path.isfile(_INDEX_FILE):
|
||||
try:
|
||||
with open(_INDEX_FILE, "r", encoding="utf-8") as f:
|
||||
index = json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
index.setdefault("templates", {})[template_path] = {
|
||||
"file": entry["file"],
|
||||
"layout": entry.get("layout", "A4"),
|
||||
"texts": entry.get("texts", {}),
|
||||
"extent": (
|
||||
{
|
||||
"xmin": entry["extent"].xMinimum(),
|
||||
"ymin": entry["extent"].yMinimum(),
|
||||
"xmax": entry["extent"].xMaximum(),
|
||||
"ymax": entry["extent"].yMaximum(),
|
||||
}
|
||||
if entry.get("extent") else None
|
||||
),
|
||||
}
|
||||
|
||||
with open(_INDEX_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(index, f, ensure_ascii=False, indent=2)
|
||||
|
||||
Reference in New Issue
Block a user