Files
e49c282879 perf: KB caching, compiled index, lazy loading, parallel resolution (#38) (#142)
* perf: KB load-once cache, compiled index, lazy loading, parallel resolution (#38)

Closes #130, #131, #132, #133, #134.

- Loader node routes through a process-wide KnowledgeBaseLoader cache so the KB
  loads once per (path, version) and is reused across run() calls.
- Normalized lookup table compiled once per KB (per NSFW gating) and reused across
  resolves instead of rebuilt per resolve_scene. normalize_concept moved to
  knowledge_base (re-exported from the resolver).
- Incremental per-category lazy file loading behind KnowledgeBaseLoader.
- Optional parallel per-concept resolution (thread pool), merged in exact
  discovery order; deterministic and threshold-gated (default sequential).
- Performance regression guard (generous ceilings + relative ratio). Refreshed
  docs/benchmarks.md (resolution 57.6us -> 17.8us).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(changelog): note V2 performance improvements (#38)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: kunail0804 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 18:51:42 +02:00

59 lines
2.3 KiB
Python

"""Tests for incremental (lazy) Knowledge Base loading (issue #132, epic #38)."""
from __future__ import annotations
import json
from pathlib import Path
from compiler.common.knowledge_base import KnowledgeBaseLoader
REPO_KB = Path(__file__).resolve().parent.parent / "knowledge_base"
def _make_kb(tmp_path):
(tmp_path / "hair.json").write_text(
'[{"id": "long_hair", "tags": ["long hair"], "category": "hair"}]', encoding="utf-8"
)
(tmp_path / "clothing.json").write_text(
'[{"id": "dress", "tags": ["dress"], "category": "clothing"}]', encoding="utf-8"
)
(tmp_path / "gen_clothing.json").write_text(
'[{"id": "skirt", "tags": ["skirt"], "category": "clothing"}]', encoding="utf-8"
)
return tmp_path
def test_category_access_parses_only_that_categorys_files(tmp_path) -> None:
loader = KnowledgeBaseLoader(_make_kb(tmp_path))
hair = loader.category_entries("hair")
assert [e["id"] for e in hair] == ["long_hair"]
assert loader.files_read == {"hair.json"} # clothing files not touched
clothing = loader.category_entries("clothing")
assert {e["id"] for e in clothing} == {"dress", "skirt"} # curated + generated
assert loader.files_read == {"hair.json", "clothing.json", "gen_clothing.json"}
def test_category_access_is_cached(tmp_path) -> None:
loader = KnowledgeBaseLoader(_make_kb(tmp_path))
loader.category_entries("hair")
# Corrupt the file on disk; a cached second access must not re-read it.
(tmp_path / "hair.json").write_text("BROKEN", encoding="utf-8")
assert [e["id"] for e in loader.category_entries("hair")] == ["long_hair"]
def test_full_load_still_sees_complete_kb(tmp_path) -> None:
loader = KnowledgeBaseLoader(_make_kb(tmp_path))
loader.category_entries("hair") # subset access first
kb = loader.get() # full resolution sees everything
assert kb.get("long_hair") and kb.get("dress") and kb.get("skirt")
def test_reference_kb_category_access(tmp_path) -> None:
loader = KnowledgeBaseLoader(REPO_KB)
entries = loader.category_entries("clothing")
assert any(e["id"] == "thighhighs" for e in entries)
# Only clothing files were parsed, not the whole 16k-entry Knowledge Base.
assert loader.files_read <= {"clothing.json", "gen_clothing.json"}
assert len(json.dumps(entries)) > 0