Skip to content

Commit 91e4745

Browse files
committed
perf(chat-api): 补充聊天接口追踪并异步化图片读取
- 新增后端 perf trace 工具,覆盖会话、消息、头像和图片接口 - 将图片路径探测、候选收集、解密读取与缓存写入移入线程,减少阻塞 - 补充缓存命中与候选选择追踪,并为图片接口增加 prefer_live
1 parent 284a71c commit 91e4745

3 files changed

Lines changed: 284 additions & 10 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import threading
5+
import time
6+
from typing import Any, Callable
7+
8+
9+
def create_perf_trace(logger: Any, category: str, **base_fields: Any) -> tuple[str, Callable[[str], None]]:
10+
trace_id = f"{category}-{int(time.time() * 1000)}-{threading.get_ident()}"
11+
started_at = time.perf_counter()
12+
last_at = started_at
13+
14+
def log(phase: str, **fields: Any) -> None:
15+
nonlocal last_at
16+
now = time.perf_counter()
17+
payload = {
18+
**base_fields,
19+
**fields,
20+
"elapsedMs": round((now - started_at) * 1000.0, 1),
21+
"deltaMs": round((now - last_at) * 1000.0, 1),
22+
}
23+
last_at = now
24+
try:
25+
payload_text = json.dumps(payload, ensure_ascii=False, default=str)
26+
except Exception:
27+
payload_text = str(payload)
28+
logger.info("[%s] %s %s %s", trace_id, category, phase, payload_text)
29+
30+
return trace_id, log

src/wechat_decrypt_tool/routers/chat.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
from ..database_filters import list_countable_database_names
7474
from ..key_store import remove_account_keys_from_store
7575
from ..path_fix import PathFixRoute
76+
from ..perf_trace import create_perf_trace
7677
from ..session_last_message import (
7778
build_session_last_message_table,
7879
get_session_last_message_status,
@@ -3998,6 +3999,17 @@ def list_chat_sessions(
39983999
contact_db_path = account_dir / "contact.db"
39994000
head_image_db_path = account_dir / "head_image.db"
40004001
base_url = str(request.base_url).rstrip("/")
4002+
_trace_id, trace = create_perf_trace(
4003+
logger,
4004+
"chat.sessions",
4005+
account=account_dir.name,
4006+
source=source_norm or "default",
4007+
limit=int(limit),
4008+
includeHidden=bool(include_hidden),
4009+
includeOfficial=bool(include_official),
4010+
preview=str(preview or ""),
4011+
)
4012+
trace("request:start")
40014013

40024014
rt_conn = None
40034015
rows: list[Any]
@@ -4122,6 +4134,12 @@ def _ts(v: Any) -> int:
41224134
finally:
41234135
sconn.close()
41244136

4137+
trace(
4138+
"rows:loaded",
4139+
rawCount=len(rows or []),
4140+
realtime=bool(source_norm == "realtime"),
4141+
)
4142+
41254143
filtered: list[Any] = []
41264144
for r in rows:
41274145
username = _session_row_get(r, "username", "") or ""
@@ -4133,8 +4151,18 @@ def _ts(v: Any) -> int:
41334151
continue
41344152
filtered.append(r)
41354153

4154+
trace(
4155+
"rows:filtered",
4156+
filteredCount=len(filtered),
4157+
)
4158+
41364159
raw_usernames = [str(_session_row_get(r, "username", "") or "").strip() for r in filtered]
41374160
top_flags = _load_contact_top_flags(contact_db_path, raw_usernames)
4161+
trace(
4162+
"top-flags:loaded",
4163+
usernameCount=len(raw_usernames),
4164+
topCount=sum(1 for value in top_flags.values() if value),
4165+
)
41384166

41394167
def _to_int(v: Any) -> int:
41404168
try:
@@ -4164,6 +4192,12 @@ def _session_sort_key(row: Any) -> tuple[int, int, int]:
41644192

41654193
contact_rows = _load_contact_rows(contact_db_path, usernames)
41664194
local_avatar_usernames = _query_head_image_usernames(head_image_db_path, usernames)
4195+
trace(
4196+
"contacts:loaded",
4197+
usernameCount=len(usernames),
4198+
contactRowCount=len(contact_rows),
4199+
localAvatarCount=len(local_avatar_usernames),
4200+
)
41674201

41684202
# Some sessions (notably enterprise groups / openim-related IDs) may be missing from decrypted contact.db
41694203
# (or lack nickname/avatar columns). In that case, fall back to WCDB APIs (same as WeFlow) to resolve
@@ -4212,6 +4246,12 @@ def _session_sort_key(row: Any) -> tuple[int, int, int]:
42124246
wcdb_display_names = {}
42134247
wcdb_avatar_urls = {}
42144248

4249+
trace(
4250+
"wcdb-fallback:loaded",
4251+
displayNameCount=len(wcdb_display_names),
4252+
avatarUrlCount=len(wcdb_avatar_urls),
4253+
)
4254+
42154255
preview_mode = str(preview or "").strip().lower()
42164256
if preview_mode not in {"latest", "index", "session", "db", "none"}:
42174257
preview_mode = "latest"
@@ -4299,6 +4339,14 @@ def _is_generic_location_preview(value: Any) -> bool:
42994339
except Exception:
43004340
pass
43014341

4342+
trace(
4343+
"previews:resolved",
4344+
previewMode=preview_mode,
4345+
previewCount=len(last_previews),
4346+
groupSenderDisplayCount=len(group_sender_display_names),
4347+
unresolvedGroupSenderCount=len(unresolved),
4348+
)
4349+
43024350
sessions: list[dict[str, Any]] = []
43034351
for r in filtered:
43044352
username = r["username"]
@@ -4416,6 +4464,10 @@ def _is_generic_location_preview(value: Any) -> bool:
44164464
}
44174465
)
44184466

4467+
trace(
4468+
"response:ready",
4469+
sessionCount=len(sessions),
4470+
)
44194471
return {
44204472
"status": "success",
44214473
"account": account_dir.name,
@@ -5169,11 +5221,24 @@ def list_chat_messages(
51695221
head_image_db_path = account_dir / "head_image.db"
51705222
message_resource_db_path = account_dir / "message_resource.db"
51715223
base_url = str(request.base_url).rstrip("/")
5224+
_trace_id, trace = create_perf_trace(
5225+
logger,
5226+
"chat.messages",
5227+
account=account_dir.name,
5228+
username=username,
5229+
source=source_norm or "default",
5230+
limit=int(limit),
5231+
offset=int(offset),
5232+
order=str(order or ""),
5233+
renderTypes=str(render_types or ""),
5234+
)
5235+
trace("request:start")
51725236

51735237
db_paths: list[Path] = []
51745238
if source_norm != "realtime":
51755239
db_paths = _iter_message_db_paths(account_dir)
51765240
if not db_paths:
5241+
trace("response:error", reason="no-message-dbs")
51775242
return {
51785243
"status": "error",
51795244
"account": account_dir.name,
@@ -5199,6 +5264,12 @@ def list_chat_messages(
51995264
resource_conn = None
52005265
resource_chat_id = None
52015266

5267+
trace(
5268+
"resource-db:resolved",
5269+
hasResourceDb=bool(resource_conn is not None),
5270+
resourceChatId=int(resource_chat_id or 0),
5271+
)
5272+
52025273
want_asc = str(order or "").lower() != "desc"
52035274

52045275
want_types: Optional[set[str]] = None
@@ -5337,6 +5408,16 @@ def pick(*keys: str) -> Any:
53375408
break
53385409
scan_take = next_take
53395410

5411+
trace(
5412+
"messages:collected",
5413+
scanTake=int(scan_take),
5414+
mergedCount=len(merged),
5415+
hasMoreAny=bool(has_more_any),
5416+
senderUsernameCount=len(sender_usernames),
5417+
quoteUsernameCount=len(quote_usernames),
5418+
patUsernameCount=len(pat_usernames),
5419+
)
5420+
53405421
# Self-heal (default source only): if the decrypted snapshot has no conversation table yet (new session),
53415422
# do a one-shot realtime->decrypted sync and re-query once. This avoids "暂无聊天记录" after turning off realtime.
53425423
if (
@@ -5352,6 +5433,7 @@ def pick(*keys: str) -> Any:
53525433
missing_table = True
53535434

53545435
if missing_table:
5436+
trace("self-heal:missing-table")
53555437
rt_conn2 = None
53565438
try:
53575439
rt_conn2 = WCDB_REALTIME.ensure_connected(account_dir)
@@ -5362,6 +5444,7 @@ def pick(*keys: str) -> Any:
53625444

53635445
if rt_conn2 is not None:
53645446
try:
5447+
trace("self-heal:sync:start")
53655448
with _realtime_sync_lock(account_dir.name, username):
53665449
msg_db_path2, table_name2 = _ensure_decrypted_message_table(account_dir, username)
53675450
_sync_chat_realtime_messages_for_table(
@@ -5373,7 +5456,9 @@ def pick(*keys: str) -> Any:
53735456
max_scan=max(200, int(limit) + 50),
53745457
backfill_limit=0,
53755458
)
5459+
trace("self-heal:sync:end")
53765460
except Exception:
5461+
trace("self-heal:sync:error")
53775462
pass
53785463

53795464
(
@@ -5393,6 +5478,11 @@ def pick(*keys: str) -> Any:
53935478
)
53945479
if want_types is not None:
53955480
merged = [m for m in merged if _normalize_render_type_key(m.get("renderType")) in want_types]
5481+
trace(
5482+
"self-heal:requery:end",
5483+
mergedCount=len(merged),
5484+
hasMoreAny=bool(has_more_any),
5485+
)
53965486

53975487
r"""
53985488
take = int(limit) + int(offset)
@@ -5886,8 +5976,17 @@ def sort_key(m: dict[str, Any]) -> tuple[int, int, int]:
58865976
if want_asc:
58875977
page = list(reversed(page))
58885978

5979+
trace(
5980+
"page:sliced",
5981+
mergedCount=len(merged),
5982+
pageCount=len(page),
5983+
hasMore=bool(has_more_global),
5984+
orderAsc=bool(want_asc),
5985+
)
5986+
58895987
# Hot path optimization: only enrich the page we return.
58905988
if not page:
5989+
trace("response:ready", pageCount=0)
58915990
return {
58925991
"status": "success",
58935992
"account": account_dir.name,
@@ -5961,6 +6060,12 @@ def sort_key(m: dict[str, Any]) -> tuple[int, int, int]:
59616060
)
59626061
sender_contact_rows = _load_contact_rows(contact_db_path, uniq_senders)
59636062
local_sender_avatars = _query_head_image_usernames(head_image_db_path, uniq_senders)
6063+
trace(
6064+
"senders:loaded",
6065+
uniqSenderCount=len(uniq_senders),
6066+
senderContactRowCount=len(sender_contact_rows),
6067+
localSenderAvatarCount=len(local_sender_avatars),
6068+
)
59646069

59656070
# contact.db may not include enterprise/openim contacts (or group chatroom records). WCDB has a more complete
59666071
# view of display names + avatar URLs, so we use it as a best-effort fallback.
@@ -5997,6 +6102,12 @@ def sort_key(m: dict[str, Any]) -> tuple[int, int, int]:
59976102
chatroom_id=username,
59986103
sender_usernames=uniq_senders,
59996104
)
6105+
trace(
6106+
"sender-fallbacks:loaded",
6107+
wcdbDisplayNameCount=len(wcdb_display_names),
6108+
wcdbAvatarUrlCount=len(wcdb_avatar_urls),
6109+
groupNicknameCount=len(group_nicknames),
6110+
)
60006111

60016112
for m in messages_window:
60026113
# If appmsg doesn't provide sourcedisplayname, try mapping sourceusername to display name.
@@ -6155,6 +6266,12 @@ def sort_key(m: dict[str, Any]) -> tuple[int, int, int]:
61556266
wcdb_display_names=wcdb_display_names,
61566267
)
61576268

6269+
trace(
6270+
"response:ready",
6271+
pageCount=len(page),
6272+
total=int(offset) + len(page) + (1 if has_more_global else 0),
6273+
hasMore=bool(has_more_global),
6274+
)
61586275
return {
61596276
"status": "success",
61606277
"account": account_dir.name,

0 commit comments

Comments
 (0)