stress_test_index.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 500章索引系统压力测试
  5. 测试目标:
  6. 1. index.db 大小增长曲线
  7. 2. 实体同步性能(entities_v3 → index.db)
  8. 3. 别名查询性能
  9. 4. 模糊搜索性能
  10. 5. 伏笔紧急度计算性能
  11. 6. 关系图查询性能
  12. 7. 并发读写稳定性
  13. 依赖:stress_test_500chapters.py 生成的 state.json
  14. """
  15. import json
  16. import os
  17. import sys
  18. import time
  19. import random
  20. import sqlite3
  21. import tempfile
  22. import shutil
  23. from pathlib import Path
  24. from datetime import datetime
  25. from typing import Dict, Any, List, Tuple
  26. # 添加脚本目录到路径
  27. script_dir = Path(__file__).resolve().parent
  28. sys.path.insert(0, str(script_dir))
  29. from security_utils import atomic_write_json, read_json_safe
  30. # Windows 编码修复
  31. if sys.platform == 'win32':
  32. import io
  33. sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
  34. sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
  35. # ============================================================================
  36. # 模拟配置(与 stress_test_500chapters.py 保持一致)
  37. # ============================================================================
  38. CONFIG = {
  39. "total_chapters": 500,
  40. "words_per_chapter": 3500,
  41. "new_character_base_rate": 0.8,
  42. "new_character_decay": 0.95,
  43. "new_location_rate": 0.3,
  44. "new_item_rate": 0.2,
  45. "foreshadow_plant_rate": 0.5,
  46. "foreshadow_resolve_rate": 0.3,
  47. "relationship_update_interval": 5,
  48. }
  49. SURNAME_POOL = ["林", "陈", "王", "李", "张", "刘", "赵", "黄", "周", "吴", "徐", "孙", "马", "朱", "胡", "郭", "何", "高", "罗", "郑"]
  50. NAME_POOL = ["天", "云", "风", "雷", "火", "水", "月", "星", "龙", "凤", "虎", "鹤", "剑", "刀", "枪", "棍", "拳", "掌", "指", "心"]
  51. class IndexMetrics:
  52. """索引性能指标收集器"""
  53. def __init__(self):
  54. self.checkpoints: List[Dict] = []
  55. self.sync_times: List[float] = []
  56. self.query_times: Dict[str, List[float]] = {
  57. "alias_lookup": [],
  58. "fuzzy_search": [],
  59. "foreshadow_urgency": [],
  60. "relationship_query": [],
  61. "entity_by_type": [],
  62. }
  63. self.errors: List[str] = []
  64. def record_checkpoint(self, chapter: int, db_path: Path, state: Dict):
  65. """记录检查点"""
  66. db_size = db_path.stat().st_size if db_path.exists() else 0
  67. # 统计各表行数
  68. table_counts = {}
  69. if db_path.exists():
  70. try:
  71. conn = sqlite3.connect(str(db_path))
  72. cursor = conn.cursor()
  73. for table in ["chapters", "entities", "entity_aliases", "entity_kv",
  74. "entity_history", "foreshadowing_index", "relationships"]:
  75. try:
  76. cursor.execute(f"SELECT COUNT(*) FROM {table}")
  77. table_counts[table] = cursor.fetchone()[0]
  78. except sqlite3.OperationalError:
  79. table_counts[table] = 0
  80. conn.close()
  81. except Exception as e:
  82. self.errors.append(f"DB stats error: {e}")
  83. self.checkpoints.append({
  84. "chapter": chapter,
  85. "db_size_kb": db_size / 1024,
  86. "table_counts": table_counts,
  87. "avg_sync_time_ms": sum(self.sync_times[-10:]) / max(len(self.sync_times[-10:]), 1) * 1000,
  88. "query_performance": {
  89. k: sum(v[-10:]) / max(len(v[-10:]), 1) * 1000
  90. for k, v in self.query_times.items()
  91. }
  92. })
  93. def record_sync_time(self, duration: float):
  94. self.sync_times.append(duration)
  95. def record_query_time(self, query_type: str, duration: float):
  96. if query_type in self.query_times:
  97. self.query_times[query_type].append(duration)
  98. def record_error(self, error: str):
  99. self.errors.append(error)
  100. def generate_report(self) -> str:
  101. """生成测试报告"""
  102. if not self.checkpoints:
  103. return "No data collected"
  104. final = self.checkpoints[-1]
  105. first = self.checkpoints[0] if self.checkpoints else final
  106. lines = [
  107. "=" * 70,
  108. "📊 500章索引系统压力测试报告",
  109. "=" * 70,
  110. "",
  111. "## index.db 增长",
  112. f"- 初始大小: {first['db_size_kb']:.2f} KB",
  113. f"- 最终大小: {final['db_size_kb']:.2f} KB",
  114. f"- 增长倍数: {final['db_size_kb'] / max(first['db_size_kb'], 0.1):.1f}x",
  115. "",
  116. "## 表行数统计",
  117. ]
  118. for table, count in final.get('table_counts', {}).items():
  119. lines.append(f" - {table}: {count:,}")
  120. lines.extend([
  121. "",
  122. "## 同步性能",
  123. f"- 平均同步时间: {sum(self.sync_times) / max(len(self.sync_times), 1) * 1000:.2f} ms",
  124. f"- 最大同步时间: {max(self.sync_times) * 1000:.2f} ms" if self.sync_times else "N/A",
  125. f"- 最小同步时间: {min(self.sync_times) * 1000:.2f} ms" if self.sync_times else "N/A",
  126. "",
  127. "## 查询性能(平均)",
  128. ])
  129. for query_type, times in self.query_times.items():
  130. if times:
  131. avg = sum(times) / len(times) * 1000
  132. lines.append(f" - {query_type}: {avg:.2f} ms")
  133. lines.extend([
  134. "",
  135. "## 错误统计",
  136. f"- 错误数: {len(self.errors)}",
  137. ])
  138. if self.errors:
  139. lines.append("- 错误详情:")
  140. for err in self.errors[:10]:
  141. lines.append(f" - {err[:80]}")
  142. # 增长曲线
  143. lines.extend([
  144. "",
  145. "## 增长曲线(每100章)",
  146. "| 章节 | DB大小(KB) | entities | aliases | foreshadow | 同步(ms) |",
  147. "|------|-----------|----------|---------|------------|----------|",
  148. ])
  149. for cp in self.checkpoints:
  150. if cp['chapter'] % 100 == 0 or cp['chapter'] == final['chapter']:
  151. tc = cp.get('table_counts', {})
  152. lines.append(
  153. f"| {cp['chapter']} | {cp['db_size_kb']:.1f} | "
  154. f"{tc.get('entities', 0)} | {tc.get('entity_aliases', 0)} | "
  155. f"{tc.get('foreshadowing_index', 0)} | {cp['avg_sync_time_ms']:.1f} |"
  156. )
  157. # 查询性能趋势
  158. lines.extend([
  159. "",
  160. "## 查询性能趋势(每100章)",
  161. "| 章节 | alias查询(ms) | 模糊搜索(ms) | 伏笔紧急度(ms) | 关系查询(ms) |",
  162. "|------|--------------|-------------|---------------|-------------|",
  163. ])
  164. for cp in self.checkpoints:
  165. if cp['chapter'] % 100 == 0 or cp['chapter'] == final['chapter']:
  166. qp = cp.get('query_performance', {})
  167. lines.append(
  168. f"| {cp['chapter']} | {qp.get('alias_lookup', 0):.2f} | "
  169. f"{qp.get('fuzzy_search', 0):.2f} | "
  170. f"{qp.get('foreshadow_urgency', 0):.2f} | "
  171. f"{qp.get('relationship_query', 0):.2f} |"
  172. )
  173. # 稳定性评估
  174. lines.extend([
  175. "",
  176. "## 稳定性评估",
  177. ])
  178. if final['db_size_kb'] < 1024:
  179. lines.append("✅ 数据库大小合理 (< 1MB)")
  180. elif final['db_size_kb'] < 5120:
  181. lines.append("⚠️ 数据库偏大 (1-5MB)")
  182. else:
  183. lines.append("❌ 数据库过大 (> 5MB)")
  184. avg_sync = sum(self.sync_times) / max(len(self.sync_times), 1) * 1000
  185. if avg_sync < 100:
  186. lines.append("✅ 同步性能良好 (< 100ms)")
  187. elif avg_sync < 500:
  188. lines.append("⚠️ 同步性能一般 (100-500ms)")
  189. else:
  190. lines.append("❌ 同步性能差 (> 500ms)")
  191. # 查询性能评估
  192. for query_type, times in self.query_times.items():
  193. if times:
  194. avg = sum(times) / len(times) * 1000
  195. if avg < 10:
  196. lines.append(f"✅ {query_type} 查询快速 (< 10ms)")
  197. elif avg < 50:
  198. lines.append(f"⚠️ {query_type} 查询一般 (10-50ms)")
  199. else:
  200. lines.append(f"❌ {query_type} 查询慢 (> 50ms)")
  201. if not self.errors:
  202. lines.append("✅ 无错误")
  203. else:
  204. lines.append(f"❌ 有 {len(self.errors)} 个错误")
  205. lines.append("")
  206. lines.append("=" * 70)
  207. return "\n".join(lines)
  208. class IndexSimulator:
  209. """索引系统模拟器"""
  210. def __init__(self, project_root: Path):
  211. self.project_root = project_root
  212. self.state_file = project_root / ".webnovel" / "state.json"
  213. self.db_path = project_root / ".webnovel" / "index.db"
  214. self.metrics = IndexMetrics()
  215. self.generated_names = set()
  216. self.entity_id_counter = 0
  217. def _generate_id(self, prefix: str) -> str:
  218. self.entity_id_counter += 1
  219. return f"{prefix}_{self.entity_id_counter:05d}"
  220. def _generate_character_name(self) -> str:
  221. for _ in range(100):
  222. name = random.choice(SURNAME_POOL) + random.choice(NAME_POOL) + random.choice(NAME_POOL)
  223. if name not in self.generated_names:
  224. self.generated_names.add(name)
  225. return name
  226. return f"角色_{len(self.generated_names)}"
  227. def _get_character_rate(self, chapter: int) -> float:
  228. decay_periods = chapter // 50
  229. rate = CONFIG["new_character_base_rate"] * (CONFIG["new_character_decay"] ** decay_periods)
  230. return max(rate, 0.1)
  231. def init_database(self):
  232. """初始化数据库"""
  233. conn = sqlite3.connect(str(self.db_path))
  234. cursor = conn.cursor()
  235. # 创建表结构(与 structured_index.py 一致)
  236. cursor.executescript("""
  237. -- 章节表
  238. CREATE TABLE IF NOT EXISTS chapters (
  239. chapter_num INTEGER PRIMARY KEY,
  240. title TEXT,
  241. word_count INTEGER,
  242. summary TEXT,
  243. main_location TEXT,
  244. characters TEXT,
  245. content_hash TEXT,
  246. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  247. );
  248. -- 实体主表
  249. CREATE TABLE IF NOT EXISTS entities (
  250. entity_id TEXT PRIMARY KEY,
  251. entity_type TEXT NOT NULL,
  252. canonical_name TEXT,
  253. tier TEXT,
  254. desc TEXT,
  255. created_chapter INTEGER,
  256. updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  257. );
  258. -- 别名表
  259. CREATE TABLE IF NOT EXISTS entity_aliases (
  260. alias TEXT,
  261. entity_id TEXT,
  262. entity_type TEXT,
  263. first_seen_chapter INTEGER,
  264. context TEXT,
  265. PRIMARY KEY (alias, entity_id)
  266. );
  267. CREATE INDEX IF NOT EXISTS idx_alias ON entity_aliases(alias);
  268. -- 实体属性 (KV)
  269. CREATE TABLE IF NOT EXISTS entity_kv (
  270. entity_id TEXT,
  271. key TEXT,
  272. value TEXT,
  273. last_chapter INTEGER,
  274. PRIMARY KEY (entity_id, key)
  275. );
  276. -- 实体历史
  277. CREATE TABLE IF NOT EXISTS entity_history (
  278. id INTEGER PRIMARY KEY AUTOINCREMENT,
  279. entity_id TEXT,
  280. chapter INTEGER,
  281. changes_json TEXT,
  282. reasons_json TEXT,
  283. added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  284. );
  285. -- 伏笔索引
  286. CREATE TABLE IF NOT EXISTS foreshadowing_index (
  287. foreshadow_id TEXT PRIMARY KEY,
  288. content TEXT,
  289. tier TEXT,
  290. status TEXT,
  291. planted_chapter INTEGER,
  292. target_chapter INTEGER,
  293. resolved_chapter INTEGER,
  294. urgency_score REAL
  295. );
  296. -- 关系表
  297. CREATE TABLE IF NOT EXISTS relationships (
  298. id INTEGER PRIMARY KEY AUTOINCREMENT,
  299. char1_id TEXT,
  300. char2_id TEXT,
  301. rel_type TEXT,
  302. intensity INTEGER,
  303. established_chapter INTEGER,
  304. description TEXT
  305. );
  306. CREATE INDEX IF NOT EXISTS idx_rel_char1 ON relationships(char1_id);
  307. CREATE INDEX IF NOT EXISTS idx_rel_char2 ON relationships(char2_id);
  308. """)
  309. conn.commit()
  310. conn.close()
  311. def init_project(self):
  312. """初始化模拟项目"""
  313. self.project_root.mkdir(parents=True, exist_ok=True)
  314. (self.project_root / ".webnovel").mkdir(exist_ok=True)
  315. # 初始 state.json
  316. initial_state = {
  317. "project_info": {"title": "索引测试小说", "genre": "玄幻"},
  318. "progress": {"current_chapter": 0, "total_words": 0},
  319. "protagonist_state": {"name": "林天", "realm": "练气", "layer": 1},
  320. "entities_v3": {"角色": {}, "地点": {}, "物品": {}, "势力": {}, "招式": {}},
  321. "alias_index": {},
  322. "foreshadowing": [],
  323. "relationships": [],
  324. }
  325. # 添加主角
  326. protagonist_id = "protagonist_lintian"
  327. initial_state["entities_v3"]["角色"][protagonist_id] = {
  328. "canonical_name": "林天",
  329. "desc": "主角",
  330. "tier": "核心",
  331. "aliases": ["林天", "天哥"],
  332. "current": {"realm": "练气"},
  333. "history": [],
  334. }
  335. initial_state["alias_index"]["林天"] = [{"type": "角色", "id": protagonist_id}]
  336. atomic_write_json(self.state_file, initial_state, backup=False)
  337. self.init_database()
  338. return initial_state
  339. def sync_to_index(self, state: Dict, chapter: int):
  340. """同步 state.json 到 index.db"""
  341. conn = sqlite3.connect(str(self.db_path))
  342. cursor = conn.cursor()
  343. try:
  344. # 同步章节
  345. cursor.execute("""
  346. INSERT OR REPLACE INTO chapters
  347. (chapter_num, title, word_count, summary)
  348. VALUES (?, ?, ?, ?)
  349. """, (chapter, f"第{chapter}章", CONFIG["words_per_chapter"], f"第{chapter}章摘要"))
  350. # 同步实体
  351. entities_v3 = state.get("entities_v3", {})
  352. for entity_type, entities in entities_v3.items():
  353. for entity_id, entity_data in entities.items():
  354. cursor.execute("""
  355. INSERT OR REPLACE INTO entities
  356. (entity_id, entity_type, canonical_name, tier, desc, created_chapter)
  357. VALUES (?, ?, ?, ?, ?, ?)
  358. """, (
  359. entity_id,
  360. entity_type,
  361. entity_data.get("canonical_name", ""),
  362. entity_data.get("tier", "装饰"),
  363. entity_data.get("desc", ""),
  364. chapter
  365. ))
  366. # 同步别名
  367. for alias in entity_data.get("aliases", []):
  368. cursor.execute("""
  369. INSERT OR IGNORE INTO entity_aliases
  370. (alias, entity_id, entity_type, first_seen_chapter)
  371. VALUES (?, ?, ?, ?)
  372. """, (alias, entity_id, entity_type, chapter))
  373. # 同步当前属性
  374. for key, value in entity_data.get("current", {}).items():
  375. cursor.execute("""
  376. INSERT OR REPLACE INTO entity_kv
  377. (entity_id, key, value, last_chapter)
  378. VALUES (?, ?, ?, ?)
  379. """, (entity_id, key, str(value), chapter))
  380. # 同步伏笔
  381. for fs in state.get("foreshadowing", []):
  382. # 计算紧急度
  383. if fs.get("status") == "未回收":
  384. target = fs.get("target_chapter", chapter + 100)
  385. urgency = max(0, 100 - (target - chapter))
  386. else:
  387. urgency = 0
  388. cursor.execute("""
  389. INSERT OR REPLACE INTO foreshadowing_index
  390. (foreshadow_id, content, tier, status, planted_chapter,
  391. target_chapter, resolved_chapter, urgency_score)
  392. VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  393. """, (
  394. fs.get("id", f"fs_{chapter}"),
  395. fs.get("content", ""),
  396. fs.get("tier", "装饰"),
  397. fs.get("status", "未回收"),
  398. fs.get("planted_chapter", chapter),
  399. fs.get("target_chapter"),
  400. fs.get("resolved_chapter"),
  401. urgency
  402. ))
  403. # 同步关系(使用 REPLACE 避免重复)
  404. # 先清空再重建(简化策略,实际生产应增量同步)
  405. cursor.execute("DELETE FROM relationships WHERE established_chapter <= ?", (chapter,))
  406. for rel in state.get("relationships", []):
  407. cursor.execute("""
  408. INSERT INTO relationships
  409. (char1_id, char2_id, rel_type, intensity, established_chapter)
  410. VALUES (?, ?, ?, ?, ?)
  411. """, (
  412. rel.get("char1_id", ""),
  413. rel.get("char2_id", ""),
  414. rel.get("type", "ally"),
  415. rel.get("intensity", 50),
  416. rel.get("established_chapter", chapter)
  417. ))
  418. conn.commit()
  419. finally:
  420. conn.close()
  421. def run_queries(self, state: Dict, chapter: int):
  422. """执行各类查询并计时"""
  423. conn = sqlite3.connect(str(self.db_path))
  424. cursor = conn.cursor()
  425. try:
  426. # 1. 别名查询
  427. alias_list = list(state.get("alias_index", {}).keys())
  428. if alias_list:
  429. test_alias = random.choice(alias_list)
  430. start = time.time()
  431. cursor.execute("SELECT entity_id, entity_type FROM entity_aliases WHERE alias = ?", (test_alias,))
  432. cursor.fetchall()
  433. self.metrics.record_query_time("alias_lookup", time.time() - start)
  434. # 2. 模糊搜索
  435. if alias_list:
  436. search_term = random.choice(alias_list)[:2] # 取前两个字
  437. start = time.time()
  438. cursor.execute("""
  439. SELECT DISTINCT entity_id, entity_type, alias
  440. FROM entity_aliases
  441. WHERE alias LIKE ?
  442. LIMIT 20
  443. """, (f"%{search_term}%",))
  444. cursor.fetchall()
  445. self.metrics.record_query_time("fuzzy_search", time.time() - start)
  446. # 3. 伏笔紧急度查询
  447. start = time.time()
  448. cursor.execute("""
  449. SELECT foreshadow_id, content, urgency_score
  450. FROM foreshadowing_index
  451. WHERE status = '未回收'
  452. ORDER BY urgency_score DESC
  453. LIMIT 10
  454. """)
  455. cursor.fetchall()
  456. self.metrics.record_query_time("foreshadow_urgency", time.time() - start)
  457. # 4. 关系查询
  458. entities_v3 = state.get("entities_v3", {})
  459. char_ids = list(entities_v3.get("角色", {}).keys())
  460. if char_ids:
  461. test_char = random.choice(char_ids)
  462. start = time.time()
  463. cursor.execute("""
  464. SELECT char2_id, rel_type, intensity
  465. FROM relationships
  466. WHERE char1_id = ?
  467. UNION
  468. SELECT char1_id, rel_type, intensity
  469. FROM relationships
  470. WHERE char2_id = ?
  471. """, (test_char, test_char))
  472. cursor.fetchall()
  473. self.metrics.record_query_time("relationship_query", time.time() - start)
  474. # 5. 按类型查询实体
  475. start = time.time()
  476. cursor.execute("""
  477. SELECT entity_id, canonical_name, tier
  478. FROM entities
  479. WHERE entity_type = '角色' AND tier = '核心'
  480. """)
  481. cursor.fetchall()
  482. self.metrics.record_query_time("entity_by_type", time.time() - start)
  483. finally:
  484. conn.close()
  485. def simulate_chapter(self, chapter: int, state: Dict) -> Dict:
  486. """模拟一章的数据变化(与主测试脚本类似)"""
  487. state["progress"]["current_chapter"] = chapter
  488. state["progress"]["total_words"] += CONFIG["words_per_chapter"]
  489. entities_v3 = state["entities_v3"]
  490. alias_index = state["alias_index"]
  491. # 新增角色
  492. if random.random() < self._get_character_rate(chapter):
  493. char_name = self._generate_character_name()
  494. char_id = self._generate_id("char")
  495. tier = random.choices(["核心", "支线", "装饰"], weights=[0.1, 0.3, 0.6])[0]
  496. entities_v3["角色"][char_id] = {
  497. "canonical_name": char_name,
  498. "desc": f"第{chapter}章出场",
  499. "tier": tier,
  500. "aliases": [char_name],
  501. "current": {"first_appearance": chapter},
  502. "history": [],
  503. }
  504. alias_index[char_name] = [{"type": "角色", "id": char_id}]
  505. # 额外别名
  506. if random.random() < 0.5:
  507. alias = char_name[0] + "兄"
  508. entities_v3["角色"][char_id]["aliases"].append(alias)
  509. if alias not in alias_index:
  510. alias_index[alias] = []
  511. alias_index[alias].append({"type": "角色", "id": char_id})
  512. # 新增地点
  513. if random.random() < CONFIG["new_location_rate"]:
  514. loc_name = random.choice(["天", "云", "龙"]) + random.choice(["山", "谷", "城"])
  515. loc_id = self._generate_id("loc")
  516. entities_v3["地点"][loc_id] = {
  517. "canonical_name": loc_name,
  518. "desc": f"第{chapter}章",
  519. "tier": "装饰",
  520. "aliases": [loc_name],
  521. "current": {},
  522. "history": [],
  523. }
  524. if loc_name not in alias_index:
  525. alias_index[loc_name] = []
  526. alias_index[loc_name].append({"type": "地点", "id": loc_id})
  527. # 伏笔
  528. if random.random() < CONFIG["foreshadow_plant_rate"]:
  529. state["foreshadowing"].append({
  530. "id": f"fs_{chapter}_{random.randint(1000, 9999)}",
  531. "content": f"第{chapter}章伏笔",
  532. "tier": random.choice(["核心", "支线", "装饰"]),
  533. "status": "未回收",
  534. "planted_chapter": chapter,
  535. "target_chapter": chapter + random.randint(10, 100),
  536. })
  537. # 回收伏笔
  538. for fs in state["foreshadowing"]:
  539. if (fs.get("status") == "未回收" and
  540. fs.get("target_chapter", 999) <= chapter and
  541. random.random() < CONFIG["foreshadow_resolve_rate"]):
  542. fs["status"] = "已回收"
  543. fs["resolved_chapter"] = chapter
  544. # 关系
  545. if chapter % CONFIG["relationship_update_interval"] == 0:
  546. char_ids = list(entities_v3["角色"].keys())
  547. if len(char_ids) >= 2:
  548. char1, char2 = random.sample(char_ids, 2)
  549. state["relationships"].append({
  550. "char1_id": char1,
  551. "char2_id": char2,
  552. "type": random.choice(["ally", "enemy", "romance", "rival"]),
  553. "intensity": random.randint(30, 100),
  554. "established_chapter": chapter,
  555. })
  556. return state
  557. def run_simulation(self, checkpoint_interval: int = 10):
  558. """运行完整模拟"""
  559. print("🚀 开始500章索引系统压力测试...")
  560. print(f"📁 测试目录: {self.project_root}")
  561. print()
  562. state = self.init_project()
  563. self.metrics.record_checkpoint(0, self.db_path, state)
  564. start_time = time.time()
  565. for chapter in range(1, CONFIG["total_chapters"] + 1):
  566. try:
  567. # 模拟章节数据
  568. state = self.simulate_chapter(chapter, state)
  569. # 保存 state.json
  570. atomic_write_json(self.state_file, state, use_lock=True, backup=False)
  571. # 同步到索引
  572. sync_start = time.time()
  573. self.sync_to_index(state, chapter)
  574. sync_duration = time.time() - sync_start
  575. self.metrics.record_sync_time(sync_duration)
  576. # 执行查询测试
  577. self.run_queries(state, chapter)
  578. # 记录检查点
  579. if chapter % checkpoint_interval == 0:
  580. self.metrics.record_checkpoint(chapter, self.db_path, state)
  581. elapsed = time.time() - start_time
  582. eta = elapsed / chapter * (CONFIG["total_chapters"] - chapter)
  583. db_size = self.db_path.stat().st_size / 1024 if self.db_path.exists() else 0
  584. print(f" 第 {chapter:3d} 章 | "
  585. f"DB {db_size:.1f}KB | "
  586. f"同步 {sync_duration*1000:.1f}ms | "
  587. f"ETA {eta:.0f}s")
  588. except Exception as e:
  589. self.metrics.record_error(f"Chapter {chapter}: {str(e)}")
  590. print(f" ❌ 第 {chapter} 章错误: {e}")
  591. # 最终检查点
  592. self.metrics.record_checkpoint(CONFIG["total_chapters"], self.db_path, state)
  593. total_time = time.time() - start_time
  594. print()
  595. print(f"✅ 索引测试完成!总耗时: {total_time:.1f}s")
  596. print()
  597. return self.metrics.generate_report()
  598. def main():
  599. """主函数"""
  600. test_dir = Path(tempfile.mkdtemp(prefix="webnovel_index_test_"))
  601. try:
  602. simulator = IndexSimulator(test_dir)
  603. report = simulator.run_simulation(checkpoint_interval=10)
  604. print(report)
  605. # 保存报告
  606. report_file = test_dir / "index_stress_test_report.md"
  607. report_file.write_text(report, encoding="utf-8")
  608. print(f"\n📄 报告已保存: {report_file}")
  609. print(f"\n测试数据目录: {test_dir}")
  610. except KeyboardInterrupt:
  611. print("\n⚠️ 测试被中断")
  612. except Exception as e:
  613. print(f"\n❌ 测试失败: {e}")
  614. import traceback
  615. traceback.print_exc()
  616. if __name__ == "__main__":
  617. main()