Files
wehub-resource-sync c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

346 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Scenario test: subprocess-backed Kuzu + LanceDB with a tight LRU cache.
Repeats the following ``N`` times, each pair in its own two fresh datasets:
1. An add → cognify → search cycle on a small inline snippet.
2. An add → cognify → search cycle on a distinct large public-domain
text lazily downloaded from Project Gutenberg to the system temp dir.
Total cycles executed = 2 × N. At ``--cycles 20`` that's 40 cycles drawing
from 20 distinct large texts.
After every cycle, prints the RSS of the main process and all of its
children (the subprocess DB workers).
Usage:
python ./cognee/tests/test_subprocess_rss.py [options]
Options (all have sensible defaults):
--cycles N Rounds of (small + large), 120 (default: 3).
Total cycles executed = 2 × N.
--lru-cache-size N DATABASE_MAX_LRU_CACHE_SIZE (default: 2).
--kuzu-buffer-mb N Kuzu buffer pool size in MiB (default: 32).
--kuzu-num-threads N Max threads for Kuzu queries (default: 1).
--subprocess, --no-subprocess
Toggle the subprocess-backed adapters for both
Kuzu and LanceDB. Default: on.
DATABASE_MAX_LRU_CACHE_SIZE must be set before cognee is imported — the
``@closing_lru_cache`` decorator captures it at import time — so argument
parsing and env-var setup happen at module top, before any ``import cognee``.
"""
from __future__ import annotations
import argparse
import os
def _cycles_type(raw: str) -> int:
try:
n = int(raw)
except ValueError:
raise argparse.ArgumentTypeError(f"--cycles must be an integer, got {raw!r}")
if not 1 <= n <= 20:
raise argparse.ArgumentTypeError(f"--cycles must be between 1 and 20 inclusive, got {n}")
return n
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Subprocess-backed RSS benchmark for Kuzu + LanceDB.",
)
parser.add_argument(
"--cycles",
type=_cycles_type,
default=3,
help=(
"Rounds of (small + large), 120 (default: 3). Each round runs "
"one small-text cycle followed by one large-text cycle in a fresh "
"dataset each. Total cycles = 2 × N."
),
)
parser.add_argument(
"--lru-cache-size",
type=int,
default=2,
help="DATABASE_MAX_LRU_CACHE_SIZE for adapter LRU caches (default: 2).",
)
parser.add_argument(
"--kuzu-buffer-mb",
type=int,
default=32,
help="Kuzu buffer pool size in MiB (default: 32).",
)
parser.add_argument(
"--kuzu-num-threads",
type=int,
default=1,
help="Max threads used by Kuzu for query execution (default: 1).",
)
parser.add_argument(
"--subprocess",
dest="subprocess_enabled",
action=argparse.BooleanOptionalAction,
default=True,
help=(
"Use subprocess-backed adapters for both graph and vector stores. "
"Disable with --no-subprocess to run everything in the main process "
"for comparison. Default: on."
),
)
return parser.parse_args(argv)
# Parse args first so we can set DATABASE_MAX_LRU_CACHE_SIZE BEFORE importing
# cognee. The LRU cache decorator reads it at class-definition time; setting
# the env var after ``import cognee`` has no effect.
ARGS = _parse_args()
os.environ["DATABASE_MAX_LRU_CACHE_SIZE"] = str(ARGS.lru_cache_size)
import asyncio # noqa: E402
import gc # noqa: E402
import pathlib # noqa: E402
import tempfile # noqa: E402
import urllib.request # noqa: E402
import psutil # noqa: E402
import cognee # noqa: E402
from cognee.modules.search.types import SearchType # noqa: E402
from cognee_db_workers.harness import collect_garbage_in_all_workers # noqa: E402
# Twenty distinct public-domain Gutenberg books (each roughly 400 KB 1.5 MB).
# One is used per large-round cycle; with ``--cycles 20`` we use all of them.
LARGE_TEXTS: list[tuple[str, str]] = [
("pride_and_prejudice.txt", "https://www.gutenberg.org/cache/epub/1342/pg1342.txt"),
("frankenstein.txt", "https://www.gutenberg.org/cache/epub/84/pg84.txt"),
("sherlock_holmes.txt", "https://www.gutenberg.org/cache/epub/1661/pg1661.txt"),
("moby_dick.txt", "https://www.gutenberg.org/cache/epub/2701/pg2701.txt"),
("tale_of_two_cities.txt", "https://www.gutenberg.org/cache/epub/98/pg98.txt"),
("alice_in_wonderland.txt", "https://www.gutenberg.org/cache/epub/11/pg11.txt"),
("dracula.txt", "https://www.gutenberg.org/cache/epub/345/pg345.txt"),
("dorian_gray.txt", "https://www.gutenberg.org/cache/epub/174/pg174.txt"),
("wuthering_heights.txt", "https://www.gutenberg.org/cache/epub/768/pg768.txt"),
("jane_eyre.txt", "https://www.gutenberg.org/cache/epub/1260/pg1260.txt"),
("huckleberry_finn.txt", "https://www.gutenberg.org/cache/epub/76/pg76.txt"),
("dubliners.txt", "https://www.gutenberg.org/cache/epub/2814/pg2814.txt"),
("treasure_island.txt", "https://www.gutenberg.org/cache/epub/120/pg120.txt"),
("war_of_the_worlds.txt", "https://www.gutenberg.org/cache/epub/36/pg36.txt"),
("metamorphosis.txt", "https://www.gutenberg.org/cache/epub/5200/pg5200.txt"),
("little_women.txt", "https://www.gutenberg.org/cache/epub/514/pg514.txt"),
("anne_of_green_gables.txt", "https://www.gutenberg.org/cache/epub/45/pg45.txt"),
("tom_sawyer.txt", "https://www.gutenberg.org/cache/epub/74/pg74.txt"),
("emma.txt", "https://www.gutenberg.org/cache/epub/158/pg158.txt"),
("the_iliad.txt", "https://www.gutenberg.org/cache/epub/6130/pg6130.txt"),
]
SMALL_TEXT = (
"Ada Lovelace worked with Charles Babbage on the Analytical Engine in the 1840s. "
"She is often credited as the first computer programmer for her notes on the machine."
)
# Lazy-download cache directory. Using the system temp dir means repeated
# runs share one cached copy across the whole machine instead of duplicating
# per-repo-checkout.
LARGE_TEXT_CACHE_DIR = pathlib.Path(tempfile.gettempdir()) / "cognee_subprocess_rss_texts"
def download_text(dest_path: pathlib.Path, url: str) -> str:
"""Lazy downloader: fetch only if the file isn't already cached."""
if dest_path.exists() and dest_path.stat().st_size > 0:
return str(dest_path)
dest_path.parent.mkdir(parents=True, exist_ok=True)
print(f"Downloading {url} ...", flush=True)
urllib.request.urlretrieve(url, str(dest_path))
print(
f" saved {dest_path.stat().st_size / 1024:.1f} KB to {dest_path}",
flush=True,
)
return str(dest_path)
RSS_HISTORY: list[dict] = []
def print_rss(label: str) -> None:
# Run gc in every live subprocess worker so their RSS reflects reachable
# objects only (no uncollected cycles). Best-effort; a mid-shutdown or
# crashed session is skipped silently.
collect_garbage_in_all_workers()
# Then gc in the main process so parent RSS is comparable.
gc.collect()
# PyArrow's default memory pool is a bump allocator that doesn't give
# pages back to the OS on its own — every cognify cycle builds pyarrow
# tables (embeddings, LanceDB writes, …) and the pool keeps growing
# even after the tables are collected. ``release_unused`` returns any
# pages not currently backing live allocations, which is essential for
# accurate per-cycle parent-RSS measurement.
try:
import pyarrow as _pa
_pa.default_memory_pool().release_unused()
except Exception:
pass
proc = psutil.Process(os.getpid())
parent_mb = proc.memory_info().rss / (1024 * 1024)
child_entries = []
total_children_mb = 0.0
for child in proc.children(recursive=True):
try:
rss_mb = child.memory_info().rss / (1024 * 1024)
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
total_children_mb += rss_mb
try:
name = child.name()
except (psutil.NoSuchProcess, psutil.AccessDenied):
name = "?"
try:
cmdline = " ".join(child.cmdline())
except (psutil.NoSuchProcess, psutil.AccessDenied):
cmdline = ""
child_entries.append((child.pid, name, rss_mb, cmdline))
total_mb = parent_mb + total_children_mb
print(f"\n[{label}] RSS summary", flush=True)
print(f" parent pid={proc.pid:<6} {parent_mb:8.1f} MB", flush=True)
for pid, name, rss_mb, cmdline in child_entries:
print(f" child pid={pid:<6} {rss_mb:8.1f} MB ({name})", flush=True)
if cmdline:
print(f" cmd: {cmdline}", flush=True)
print(
f" total children={len(child_entries)} "
f"children_rss={total_children_mb:.1f} MB parent+children={total_mb:.1f} MB",
flush=True,
)
RSS_HISTORY.append(
{
"label": label,
"parent_mb": parent_mb,
"children_mb": total_children_mb,
"total_mb": total_mb,
"num_children": len(child_entries),
}
)
def print_rss_history() -> None:
if not RSS_HISTORY:
return
print(f"\n{'=' * 78}", flush=True)
print("Per-cycle memory totals (parent + all children)", flush=True)
print(f"{'=' * 78}", flush=True)
print(
f"{'#':>3} {'label':<28} {'parent MB':>10} {'children MB':>12} "
f"{'#ch':>4} {'total MB':>10}",
flush=True,
)
print(f"{'-' * 3} {'-' * 28} {'-' * 10} {'-' * 12} {'-' * 4} {'-' * 10}", flush=True)
baseline_total = RSS_HISTORY[0]["total_mb"]
peak_total = max(e["total_mb"] for e in RSS_HISTORY)
for i, entry in enumerate(RSS_HISTORY):
print(
f"{i:>3} {entry['label'][:28]:<28} {entry['parent_mb']:>10.1f} "
f"{entry['children_mb']:>12.1f} {entry['num_children']:>4d} "
f"{entry['total_mb']:>10.1f}",
flush=True,
)
last_total = RSS_HISTORY[-1]["total_mb"]
print(f"{'-' * 78}", flush=True)
print(f" baseline total : {baseline_total:8.1f} MB", flush=True)
print(f" final total : {last_total:8.1f} MB", flush=True)
print(f" peak total : {peak_total:8.1f} MB", flush=True)
print(f" delta (final-baseline): {last_total - baseline_total:+.1f} MB", flush=True)
async def run_cycle(cycle_index: int, total_cycles: int, dataset_name: str, data) -> None:
print(f"\n{'=' * 60}", flush=True)
print(
f"Cycle {cycle_index}/{total_cycles} — dataset='{dataset_name}'",
flush=True,
)
print(f"{'=' * 60}", flush=True)
await cognee.add(data, dataset_name)
await cognee.cognify([dataset_name])
results = await cognee.search(
query_type=SearchType.GRAPH_COMPLETION,
query_text="What is this text about?",
datasets=[dataset_name],
)
print(f" search returned {len(results)} result(s)", flush=True)
print_rss(f"after cycle {cycle_index}")
async def main() -> None:
buffer_pool_bytes = max(1, ARGS.kuzu_buffer_mb) * 1024 * 1024
rounds = ARGS.cycles
total_cycles = 2 * rounds
print(
f"Running with: rounds={rounds} of (small + large) = {total_cycles} cycles total, "
f"lru_cache_size={ARGS.lru_cache_size}, "
f"kuzu_buffer_mb={ARGS.kuzu_buffer_mb}, "
f"kuzu_num_threads={ARGS.kuzu_num_threads}, "
f"subprocess={ARGS.subprocess_enabled}",
flush=True,
)
cognee.config.set_graph_db_config(
{
"graph_database_provider": "kuzu",
"graph_database_subprocess_enabled": ARGS.subprocess_enabled,
"kuzu_num_threads": ARGS.kuzu_num_threads,
"kuzu_buffer_pool_size": buffer_pool_bytes,
}
)
cognee.config.set_vector_db_config(
{
"vector_db_provider": "lancedb",
"vector_db_subprocess_enabled": ARGS.subprocess_enabled,
}
)
base_dir = pathlib.Path(__file__).parent.resolve()
cognee.config.data_root_directory(str(base_dir / ".data_storage" / "test_subprocess_rss"))
cognee.config.system_root_directory(str(base_dir / ".cognee_system" / "test_subprocess_rss"))
# Lazy-download exactly the N large texts we need — one per round.
selected_texts = LARGE_TEXTS[:rounds]
large_text_paths = [
download_text(LARGE_TEXT_CACHE_DIR / filename, url) for filename, url in selected_texts
]
await cognee.prune.prune_data()
await cognee.prune.prune_system(metadata=True)
print_rss("baseline (after prune)")
cycle = 0
for i in range(1, rounds + 1):
# Small cycle first, then the matching large text for this round,
# each in its own fresh dataset.
cycle += 1
await run_cycle(cycle, total_cycles, f"small_{i}", SMALL_TEXT)
cycle += 1
await run_cycle(cycle, total_cycles, f"large_{i}", [large_text_paths[i - 1]])
print("\nAll cycles complete.", flush=True)
print_rss_history()
if __name__ == "__main__":
asyncio.run(main())