From 9cb7240060592a4489b5cb0de61e8d69a6e8a309 Mon Sep 17 00:00:00 2001 From: taocong Date: Thu, 23 Jul 2026 14:25:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=9D=9E=E8=BD=AC=E7=A0=81?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E6=9F=A5=E8=AF=A2=E9=93=BE=E8=B7=AF=E6=80=A7?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/codesearch/codesearchpage.cpp | 87 ++++++++---- src/pages/codesearch/codesearchpage.h | 9 +- src/utils/callchainanalyzer.cpp | 171 ++++++++++++++++-------- src/utils/callchainanalyzer.h | 12 +- 4 files changed, 196 insertions(+), 83 deletions(-) diff --git a/src/pages/codesearch/codesearchpage.cpp b/src/pages/codesearch/codesearchpage.cpp index 89e75a2..32894a7 100644 --- a/src/pages/codesearch/codesearchpage.cpp +++ b/src/pages/codesearch/codesearchpage.cpp @@ -706,34 +706,49 @@ void CodeSearchPage::autoBuildIndexIfNeeded() CallChainAnalyzer *ca = currentAnalyzer(); ca->setRootPath(path); - // 1) 优先从磁盘缓存读取(程序重启 / 页面切换时秒开,无需重新扫描目录) - QString message; - if (ca->loadFromCache(path, &message)) { - m_indexedPathByMode[m_mode] = path; - LogManager::instance()->log(QString("[代码检索-缓存] %1").arg(message)); - return; - } + // 统一的“全量构建/重建”流程:后台线程扫描目录,避免界面卡死; + // 构建完成后写入缓存,供下次直接读取。 + auto fullBuild = [this, path]() { + m_indexing = true; + runIndexBuild(currentAnalyzer(), "正在构建代码索引,请稍候...", + [this, path](bool ok, const QString &message) { + m_indexing = false; + if (ok) { + currentAnalyzer()->saveToCache(); + m_indexedPathByMode[m_mode] = path; + LogManager::instance()->log(QString("[代码检索-自动] %1").arg(message)); + } else { + m_indexedPathByMode[m_mode] = path; // 标记已尝试,避免每次显示重复重试 + } + }); + }; - // 2) 缓存不存在或已失效:后台线程扫描目录构建索引,期间显示进度框,避免界面卡死; - // 构建完成后写入缓存,供下次直接读取。 - m_indexing = true; - runIndexBuild(ca, "正在构建代码索引,请稍候...", - [this, path](bool ok, const QString &message) { - m_indexing = false; - if (ok) { - currentAnalyzer()->saveToCache(); - m_indexedPathByMode[m_mode] = path; - LogManager::instance()->log(QString("[代码检索-自动] %1").arg(message)); - } else { - m_indexedPathByMode[m_mode] = path; // 标记已尝试,避免每次显示重复重试 - } - }); + // 二级加速(方案②):仅 stat root 直接子项做快速粗判(毫秒级、主线程), + // 命中则后台直接读缓存;未命中再后台做全量指纹校验+构建(方案①,不阻塞 UI)。 + if (ca->cacheQuickValid(path)) { + m_indexing = true; + runCacheLoad(ca, "正在加载索引缓存,请稍候...", /*skipFingerprint=*/true, + [this, path, fullBuild](bool ok, const QString &message) { + m_indexing = false; + if (ok) { + m_indexedPathByMode[m_mode] = path; + LogManager::instance()->log(QString("[代码检索-缓存] %1").arg(message)); + } else { + // 粗判命中但读缓存失败(极少见,如缓存损坏):回退全量构建 + m_indexedPathByMode[m_mode] = path; // 标记已尝试 + fullBuild(); + } + }); + } else { + fullBuild(); + } } -void CodeSearchPage::runIndexBuild(CallChainAnalyzer *ca, const QString &title, - std::function onDone) +void CodeSearchPage::runAsync(CallChainAnalyzer *ca, const QString &title, + std::function(CallChainAnalyzer *)> task, + std::function onDone) { - // 模态进度框(不确定进度),后台构建期间维持界面响应 + // 模态进度框(不确定进度),后台任务期间维持界面响应 auto *dlg = new QProgressDialog(title, QString(), 0, 0, this); dlg->setWindowTitle("请稍候"); dlg->setWindowModality(Qt::WindowModal); @@ -754,13 +769,31 @@ void CodeSearchPage::runIndexBuild(CallChainAnalyzer *ca, const QString &title, } }); - // 在后台线程执行实际扫描,避免阻塞 UI 线程 + // 在后台线程执行实际任务,避免阻塞 UI 线程 auto future = QtConcurrent::run([=]() -> BuildResult { + return task(ca); + }); + watcher->setFuture(future); +} + +void CodeSearchPage::runIndexBuild(CallChainAnalyzer *ca, const QString &title, + std::function onDone) +{ + runAsync(ca, title, [](CallChainAnalyzer *ca) -> QPair { QString msg; const bool ok = ca->buildIndex(&msg); return {ok, msg}; - }); - watcher->setFuture(future); + }, onDone); +} + +void CodeSearchPage::runCacheLoad(CallChainAnalyzer *ca, const QString &title, bool skipFingerprint, + std::function onDone) +{ + runAsync(ca, title, [skipFingerprint](CallChainAnalyzer *ca) -> QPair { + QString msg; + const bool ok = ca->loadFromCache(ca->rootPath(), &msg, skipFingerprint); + return {ok, msg}; + }, onDone); } void CodeSearchPage::onQueryByInput() diff --git a/src/pages/codesearch/codesearchpage.h b/src/pages/codesearch/codesearchpage.h index 73a6753..c41126a 100644 --- a/src/pages/codesearch/codesearchpage.h +++ b/src/pages/codesearch/codesearchpage.h @@ -89,9 +89,16 @@ private: void applyMode(); CallChainAnalyzer *currentAnalyzer() const; - /// 后台线程异步构建索引(避免界面卡顿),构建期间显示进度框;完成后回调 onDone(ok, message) + /// 后台线程异步执行索引任务(避免界面卡顿),期间显示模态进度框;完成后回调 onDone(ok, message) + void runAsync(CallChainAnalyzer *ca, const QString &title, + std::function(CallChainAnalyzer *)> task, + std::function onDone); + /// 后台线程异步构建索引(扫描目录),完成后回调 onDone void runIndexBuild(CallChainAnalyzer *ca, const QString &title, std::function onDone); + /// 后台线程异步从缓存加载索引;skipFingerprint 为 true 时跳过全量目录指纹校验 + void runCacheLoad(CallChainAnalyzer *ca, const QString &title, bool skipFingerprint, + std::function onDone); Mode m_mode = ConvertMode; diff --git a/src/utils/callchainanalyzer.cpp b/src/utils/callchainanalyzer.cpp index 084d237..7628b80 100644 --- a/src/utils/callchainanalyzer.cpp +++ b/src/utils/callchainanalyzer.cpp @@ -179,6 +179,62 @@ QString CallChainAnalyzer::cacheFilePathFor(const QString &rootPath) return QDir(dir).filePath(safe + ".json"); } +QString CallChainAnalyzer::metaFilePathFor(const QString &rootPath) +{ + const QFileInfo fi(cacheFilePathFor(rootPath)); + return QDir(fi.dir()).filePath(fi.completeBaseName() + ".fp"); +} + +QString CallChainAnalyzer::quickFingerprint(const QString &rootPath) +{ + // 仅列举 root 的直接子项(不递归),O(直接子项数) 次 stat, + // 开销远低于 computeFingerprint 的整目录递归扫描,用于启动时的快速粗判。 + QDir dir(rootPath); + if (!dir.exists()) { + return QString(); + } + const QFileInfoList entries = + dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot); + QStringList parts; + parts.reserve(entries.size()); + for (const QFileInfo &fi : entries) { + // 排除目录不计入指纹,与索引扫描范围保持一致 + if (fi.isDir() && isExcludedDir(fi.filePath())) { + continue; + } + // 名称 + 修改时间 + 大小:可感知新增/删除/改名/修改 + parts.append(QString("%1|%2|%3") + .arg(fi.fileName()) + .arg(fi.lastModified().toSecsSinceEpoch()) + .arg(fi.size())); + } + parts.sort(); + return parts.join(";"); +} + +bool CallChainAnalyzer::cacheQuickValid(const QString &path) const +{ + // 先确认缓存 JSON 存在,再比较轻量指纹 sidecar + const QString cacheFile = cacheFilePathFor(path); + if (!QFile::exists(cacheFile)) { + return false; + } + const QString metaFile = metaFilePathFor(path); + QFile f(metaFile); + if (!f.exists() || !f.open(QIODevice::ReadOnly)) { + return false; // sidecar 缺失:无法粗判,交由调用方做全量校验 + } + const QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); + if (!doc.isObject()) { + return false; + } + const QJsonObject o = doc.object(); + if (o.value("rootPath").toString() != path) { + return false; + } + return o.value("quick").toString() == quickFingerprint(path); +} + void CallChainAnalyzer::saveToCache() const { if (!m_indexed || m_rootPath.isEmpty()) { @@ -221,9 +277,19 @@ void CallChainAnalyzer::saveToCache() const file.write(QJsonDocument(root).toJson(QJsonDocument::Compact)); file.commit(); } + + // 写入轻量指纹 sidecar,供下次启动做毫秒级粗判,避免整目录递归扫描 + QJsonObject side; + side["rootPath"] = m_rootPath; + side["quick"] = quickFingerprint(m_rootPath); + QSaveFile sf(metaFilePathFor(m_rootPath)); + if (sf.open(QIODevice::WriteOnly)) { + sf.write(QJsonDocument(side).toJson(QJsonDocument::Compact)); + sf.commit(); + } } -bool CallChainAnalyzer::loadFromCache(const QString &path, QString *message) +bool CallChainAnalyzer::loadFromCache(const QString &path, QString *message, bool skipFingerprint) { m_chineseNameIndex.clear(); m_fullNameIndex.clear(); @@ -251,8 +317,13 @@ bool CallChainAnalyzer::loadFromCache(const QString &path, QString *message) if (meta.value("rootPath").toString() != path) { return false; } - if (meta.value("fingerprint").toString() != computeFingerprint(path)) { - return false; + // skipFingerprint 为 true 时(快速粗判已命中)跳过整目录指纹扫描,直接信任缓存, + // 省去一次递归目录遍历(原 ~2s 主线程阻塞)。深层文件被修改而 root 直接子项未变时 + // 可能用到旧缓存,可点"刷新索引"强制重建兜底。 + if (!skipFingerprint) { + if (meta.value("fingerprint").toString() != computeFingerprint(path)) { + return false; + } } const QJsonArray funcs = root.value("functions").toArray(); @@ -514,21 +585,28 @@ CallChainAnalyzer::buildCallerForest(const QString &startName, const TreeNode &d } } - // 递归构造“上游”链:children 为调用方(caller),链末端为 startName + // 递归构造“上游”链:每个返回的 TreeNode 都是一条以最顶层调用方为根、 + // 链末端(叶子)为 name 的调用链;然后把 name 作为被调用方挂到链末端之下, + // 使调用方 -> 被调用方(含 startKey 的下游树)保持父子层级,而非平级。 + // 例:A>B>C,查询 C(下游 D>E>F)-> 应得到 A>B>C>D>E>F,而不是 A>B 与 C>D>E>F 平级。 auto recurse = [&](auto &self, const QString &name, QSet vis, int depth) -> QList { QList out; + + if (depth >= maxDepth) { + TreeNode n = (name == startKey) ? downTree : makeNode(name, false); + out.append(n); + return out; + } + const auto it = m_reverseIndex.find(name); QList> callers; if (it != m_reverseIndex.end()) { callers = it.value(); } - if (callers.isEmpty() || depth >= maxDepth) { - // 顶层调用方(无人再调用它):作为返回链的根 - TreeNode n = makeNode(name, name == startKey); - if (name == startKey) { - n = downTree; // 起始函数节点直接用下游树(自带内部调用) - } + if (callers.isEmpty()) { + // 顶层调用方(无人再调用它):作为链的根;若是起点则直接挂下游树 + TreeNode n = (name == startKey) ? downTree : makeNode(name, false); out.append(n); return out; } @@ -536,27 +614,24 @@ CallChainAnalyzer::buildCallerForest(const QString &startName, const TreeNode &d for (const QSharedPointer &C : callers) { const QString cName = C->chineseName; if (vis.contains(cName)) { - // 循环调用:标记并收尾 + // 循环调用:标记后作为链的根收尾,并把被调用方挂在下面 TreeNode cn = makeNode(cName, false); cn.state = "cycle"; - if (name == startKey) { - cn.children.append(downTree); - } else { - cn.children.append(makeNode(name, false)); - } + cn.children.append((name == startKey) ? downTree : makeNode(name, false)); out.append(cn); continue; } QSet nextVis = vis; nextVis.insert(name); QList sub = self(self, cName, nextVis, depth + 1); - for (TreeNode &s : sub) { - if (name == startKey) { - s.children.append(downTree); - } else { - s.children.append(makeNode(name, false)); + for (TreeNode &chain : sub) { + // chain 是一条“到 cName 为止”的调用方链,把当前 name 挂到链末端(叶子)之下 + TreeNode *leaf = &chain; + while (!leaf->children.isEmpty()) { + leaf = &leaf->children.last(); } - out.append(s); + leaf->children.append((name == startKey) ? downTree : makeNode(name, false)); + out.append(chain); } } return out; @@ -647,39 +722,27 @@ QStringList CallChainAnalyzer::extractFunctionCalls(const QString &code) // 先去除注释(// 行注释、/* */ 块注释),避免把注释里提到的函数当成真实调用 const QString cleaned = stripComments(code); - // 格式 1: [函数名] 或 [函数名](.uftfunction 中的调用格式) - { - static const QRegularExpression callRe1(R"(<(I|MI)>\s*\[([^\]]+)\])"); - QRegularExpressionMatchIterator it = callRe1.globalMatch(cleaned); - while (it.hasNext()) { - const QRegularExpressionMatch match = it.next(); - const QString name = match.captured(2).trimmed(); - if (!name.isEmpty() && !seen.contains(name)) { - if (name.startsWith("LS_") || name.startsWith("LF_") || - name.startsWith("AS_") || name.startsWith("AF_")) { - seen.insert(name); - result.append(name); - } - } + // 单次、按文档顺序的全局扫描,保持调用在代码中的先后顺序。 + // 两种格式合并到一条正则,避免“格式 1 全部排在格式 2 之前”导致的乱序: + // 格式 1: [函数名] / [函数名](.uftfunction) + // 格式 2: [LS_xxx] / [LF_xxx] / [AS_xxx] / [AF_xxx](.uftservice) + static const QRegularExpression callRe( + R"(<(?:I|MI)>\s*\[([^\]]+)\]|\[(L[SF]_[^\]]+|A[SF]_[^\]]+)\])"); + QRegularExpressionMatchIterator it = callRe.globalMatch(cleaned); + while (it.hasNext()) { + const QRegularExpressionMatch match = it.next(); + // 命中格式 1 取捕获组 1,命中格式 2 取捕获组 2 + QString name = match.captured(1).isEmpty() + ? match.captured(2) + : match.captured(1); + name = name.trimmed(); + if (name.isEmpty() || seen.contains(name)) { + continue; } - } - - // 格式 2: [LS_xxx] / [LF_xxx] / [AS_xxx] / [AF_xxx](.uftservice 中的调用格式) - // 如 [LF_证券周边_大约可买获取4][...参数...] - { - static const QRegularExpression callRe2( - R"(\[(L[SF]_[^\]]+|A[SF]_[^\]]+)\])"); - QRegularExpressionMatchIterator it = callRe2.globalMatch(cleaned); - while (it.hasNext()) { - const QRegularExpressionMatch match = it.next(); - const QString name = match.captured(1).trimmed(); - if (!name.isEmpty() && !seen.contains(name)) { - if (name.startsWith("LS_") || name.startsWith("LF_") || - name.startsWith("AS_") || name.startsWith("AF_")) { - seen.insert(name); - result.append(name); - } - } + if (name.startsWith("LS_") || name.startsWith("LF_") || + name.startsWith("AS_") || name.startsWith("AF_")) { + seen.insert(name); + result.append(name); } } diff --git a/src/utils/callchainanalyzer.h b/src/utils/callchainanalyzer.h index 17e1a1a..0ecf47d 100644 --- a/src/utils/callchainanalyzer.h +++ b/src/utils/callchainanalyzer.h @@ -50,9 +50,15 @@ public: // 缓存:将已构建的索引持久化到磁盘,避免每次打开都重新扫描目录 // 加载时按 rootPath + 目录指纹校验,命中则直接反序列化,否则返回 false 由调用方重建 - bool loadFromCache(const QString &path, QString *message = nullptr); + // skipFingerprint: 为 true 时跳过全量目录指纹校验(仅用于快速粗判已命中的场景), + // 可省去一次整目录递归扫描,毫秒级加载缓存。 + bool loadFromCache(const QString &path, QString *message = nullptr, bool skipFingerprint = false); void saveToCache() const; + // 快速粗判:仅 stat root 直接子项(不递归),用于判断是否可能命中缓存, + // 避免每次启动都做全量目录指纹扫描(耗时 ~2s)。命中不代表一定有效,仍需 loadFromCache 终判。 + bool cacheQuickValid(const QString &path) const; + // 根据函数中文名查找 QSharedPointer findByChineseName(const QString &name) const; // 根据 objectId-chineseName 查找 @@ -88,6 +94,10 @@ private: // 缓存辅助:根路径 -> 缓存文件路径(按路径哈希命名),以及目录内容指纹(用于失效判断) static QString cacheFilePathFor(const QString &rootPath); static QString computeFingerprint(const QString &rootPath); + // 仅列举 root 直接子项(不递归)的轻量指纹,O(直接子项数) 次 stat,远快于 computeFingerprint + static QString quickFingerprint(const QString &rootPath); + // 快速指纹 sidecar 文件路径(与缓存同名、扩展名 .fp),存放 rootPath + 轻量指纹 + static QString metaFilePathFor(const QString &rootPath); QString m_rootPath; QHash> m_chineseNameIndex;