优化非转码业务查询链路性能

This commit is contained in:
taocong 2026-07-23 14:25:09 +08:00
parent 389c6eeecd
commit 9cb7240060
4 changed files with 196 additions and 83 deletions

View File

@ -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<void(bool, const QString &)> onDone)
void CodeSearchPage::runAsync(CallChainAnalyzer *ca, const QString &title,
std::function<QPair<bool, QString>(CallChainAnalyzer *)> task,
std::function<void(bool, const QString &)> 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<void(bool, const QString &)> onDone)
{
runAsync(ca, title, [](CallChainAnalyzer *ca) -> QPair<bool, QString> {
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<void(bool, const QString &)> onDone)
{
runAsync(ca, title, [skipFingerprint](CallChainAnalyzer *ca) -> QPair<bool, QString> {
QString msg;
const bool ok = ca->loadFromCache(ca->rootPath(), &msg, skipFingerprint);
return {ok, msg};
}, onDone);
}
void CodeSearchPage::onQueryByInput()

View File

@ -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<QPair<bool, QString>(CallChainAnalyzer *)> task,
std::function<void(bool, const QString &)> onDone);
/// 后台线程异步构建索引(扫描目录),完成后回调 onDone
void runIndexBuild(CallChainAnalyzer *ca, const QString &title,
std::function<void(bool, const QString &)> onDone);
/// 后台线程异步从缓存加载索引skipFingerprint 为 true 时跳过全量目录指纹校验
void runCacheLoad(CallChainAnalyzer *ca, const QString &title, bool skipFingerprint,
std::function<void(bool, const QString &)> onDone);
Mode m_mode = ConvertMode;

View File

@ -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<QString> vis, int depth) -> QList<TreeNode> {
QList<TreeNode> 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<QSharedPointer<const FunctionInfo>> 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<const FunctionInfo> &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<QString> nextVis = vis;
nextVis.insert(name);
QList<TreeNode> 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: <I>[函数名] 或 <MI>[函数名].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: <I>[函数名] / <MI>[函数名].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);
}
}

View File

@ -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<const FunctionInfo> 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<QString, QSharedPointer<FunctionInfo>> m_chineseNameIndex;