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

426 lines
14 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.
"""
Cognee Load Tests using Locust
Usage examples:
# Note: ENV variables must be set before usage.
# Multi-dataset test through the web UI
locust -f locust_performance_analysis.py --host http://your-cognee-instance.com MultiDatasetCogneeTest
# Single-dataset test through the web UI
locust -f locust_performance_analysis.py --host http://your-cognee-instance.com SingleDatasetCogneeTest
# Both simultaneously
locust -f locust_performance_analysis.py --host http://your-cognee-instance.com
# Headles without UI, 10 Locust users, 5 minutes
locust -f locust_performance_analysis.py --host http://your-cognee-instance.com \
--headless -u 10 -r 2 --run-time 5m --csv results
Environment variables:
COGNEE_API_KEY - API key for authentication (required)
"""
import csv
import io
import os
import random
import uuid
from pathlib import Path
from datetime import datetime
from locust import HttpUser, SequentialTaskSet, between, events, tag, task
API_KEY = os.environ.get("COGNEE_API_KEY", "")
SEARCH_TYPE = os.environ.get("COGNEE_SEARCH_TYPE", "GRAPH_COMPLETION")
ENDPOINT_NAMES = ("/api/v1/add", "/api/v1/cognify", "/api/v1/search")
def read_endpoint_averages(stats_csv_path: Path) -> dict[str, float]:
"""Parse a locust ``_stats.csv`` and return ``{endpoint_name: avg_ms}``."""
averages: dict[str, float] = {}
with stats_csv_path.open(newline="") as f:
for row in csv.DictReader(f):
if row["Name"] in ENDPOINT_NAMES:
averages[row["Name"]] = float(row["Average Response Time"])
return averages
TOPICS = [
"quantum computing",
"machine learning",
"climate change",
"renewable energy",
"space exploration",
"genetic engineering",
"blockchain technology",
"artificial intelligence",
"ocean conservation",
"urban planning",
"medieval history",
"philosophy of mind",
"distributed systems",
"neuroscience",
"economic theory",
]
SUBTOPICS = [
"data analysis",
"pattern recognition",
"resource allocation",
"risk assessment",
"optimization algorithms",
"predictive modeling",
"system integration",
"scalability",
"error correction",
"signal processing",
"network topology",
"feedback loops",
"energy efficiency",
"material science",
"behavioral adaptation",
"information theory",
]
SENTENCE_TEMPLATES = [
"The field of {topic} has seen remarkable advances in recent years, particularly in the area of {subtopic}.",
"Researchers studying {topic} have discovered that {subtopic} plays a crucial role in understanding the broader implications.",
"A comprehensive review of {topic} literature reveals that {subtopic} remains one of the most debated aspects.",
"Recent experiments in {topic} demonstrate a strong correlation between {subtopic} and observed outcomes.",
"The intersection of {topic} and {subtopic} opens new possibilities for practical applications.",
"Experts in {topic} argue that {subtopic} will be the defining challenge of the next decade.",
"Historical analysis shows that {topic} has always been influenced by developments in {subtopic}.",
"New computational models for {topic} suggest that {subtopic} can be optimized through iterative approaches.",
"The economic impact of {topic} is closely tied to advancements in {subtopic}, according to recent studies.",
"Collaborative efforts in {topic} have led to breakthroughs in {subtopic} that were previously thought impossible.",
"Understanding {topic} requires a deep appreciation of how {subtopic} interacts with existing frameworks.",
"Policy makers are increasingly turning to {topic} research to inform decisions about {subtopic}.",
]
SEARCH_QUERIES = [
"What are the main findings about {topic}?",
"How does {topic} relate to recent developments?",
"What are the key challenges in {topic}?",
"Summarize the information about {topic}.",
"What practical applications exist for {topic}?",
"What is the current state of research in {topic}?",
]
def generate_paragraph(topic: str, num_sentences: int = 5) -> str:
sentences = []
for _ in range(num_sentences):
template = random.choice(SENTENCE_TEMPLATES)
subtopic = random.choice(SUBTOPICS)
sentences.append(template.format(topic=topic, subtopic=subtopic))
return " ".join(sentences)
def generate_document(num_paragraphs: int = 3) -> tuple[str, str]:
"""Returns (text, primary_topic) for a randomly generated document. Along with random UUID to ensure uniqueness across test runs."""
topic = random.choice(TOPICS)
paragraphs = [
generate_paragraph(topic, num_sentences=random.randint(300, 377))
for _ in range(num_paragraphs)
]
paragraphs.append(str(uuid.uuid4())) # Ensure uniqueness of document text across test runs
return "\n\n".join(paragraphs), topic
def generate_search_query(topic: str) -> str:
template = random.choice(SEARCH_QUERIES)
return template.format(topic=topic)
@events.init_command_line_parser.add_listener
def add_custom_arguments(parser):
parser.add_argument(
"--cognee-api-key",
type=str,
default="",
help="API key for Cognee (overrides COGNEE_API_KEY env var)",
)
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
api_key = environment.parsed_options.cognee_api_key or API_KEY
if not api_key:
environment.runner.quit()
raise SystemExit(
"No API key provided. Set COGNEE_API_KEY env var or pass --cognee-api-key."
)
class AddCognifySearchFlow(SequentialTaskSet):
"""
Sequential flow: Add text → Cognify → Search.
Subclasses set `dataset_name` and `api_key`.
"""
dataset_name: str = ""
api_key: str = ""
topic: str = ""
def _headers(self) -> dict:
return {
"X-Api-Key": self.api_key,
}
def _json_headers(self) -> dict:
return {
"X-Api-Key": self.api_key,
"Content-Type": "application/json",
}
@tag("add")
@task
def add_text(self):
# 1. Generate the text content
text, self.topic = generate_document(num_paragraphs=random.randint(2, 5))
# 2. Prepare the form data
form_data = {
"datasetName": self.dataset_name,
}
# 3. Prepare the file data
files = [("data", ("document.txt", io.BytesIO(text.encode("utf-8")), "text/plain"))]
# 4. Make the request
with self.client.post(
"/api/v1/add",
data=form_data,
files=files,
headers=self._headers(),
name="/api/v1/add",
catch_response=True,
timeout=600,
) as resp:
if resp.status_code == 200:
resp.success()
else:
# Enhanced error logging to capture the ErrorResponse model from FastAPI
resp.failure(f"Add failed: {resp.status_code} - {resp.text[:300]}")
@tag("cognify")
@task
def cognify(self):
payload = {
"datasets": [self.dataset_name],
"runInBackground": False,
}
with self.client.post(
"/api/v1/cognify",
json=payload,
headers=self._json_headers(),
name="/api/v1/cognify",
catch_response=True,
timeout=6000,
) as resp:
if resp.status_code == 200:
resp.success()
else:
resp.failure(f"Cognify failed: {resp.status_code} - {resp.text[:300]}")
@tag("search")
@task
def search(self):
query = generate_search_query(self.topic or random.choice(TOPICS))
payload = {
"searchType": SEARCH_TYPE,
"query": query,
"datasets": [self.dataset_name],
"only_context": True,
}
with self.client.post(
"/api/v1/search",
json=payload,
headers=self._json_headers(),
name="/api/v1/search",
catch_response=True,
timeout=200,
) as resp:
if resp.status_code == 200:
resp.success()
else:
resp.failure(f"Search failed: {resp.status_code} - {resp.text[:300]}")
self.interrupt()
# ---------------------------------------------------------------------------
# Scenario 1: Multiple datasets
# ---------------------------------------------------------------------------
class MultiDatasetFlow(AddCognifySearchFlow):
"""Each call a unique dataset."""
def on_start(self):
uid = uuid.uuid4().hex[:8]
self.dataset_name = f"loadtest_user_{uid}"
self.api_key = self.user.environment.parsed_options.cognee_api_key or API_KEY
class MultiDatasetCogneeTest(HttpUser):
"""
Scenario 1 Different virtual users (Locust users) making their own Add → Cognify → Search
on different datasets.
Each spawned user creates a unique dataset name and runs the full
pipeline independently. They use the same API key, simulating multiple users (locust users) from the same account creating
separate datasets and running operations on them.
"""
tasks = [MultiDatasetFlow]
wait_time = between(5, 10)
weight = 2
# ---------------------------------------------------------------------------
# Scenario 2: Single user, single shared dataset
# ---------------------------------------------------------------------------
SHARED_DATASET_NAME = f"loadtest_shared_{uuid.uuid4().hex[:8]}"
class SingleDatasetFlow(AddCognifySearchFlow):
"""All virtual users share the same dataset and API key."""
# TODO: Fix SingleDatasetFlow add/cognify/search operations keep being called async which makes perf measurements unreliable.
# Cognify operation on a dataset that is currently being cognified returns early.
def on_start(self):
self.dataset_name = SHARED_DATASET_NAME
self.api_key = self.user.environment.parsed_options.cognee_api_key or API_KEY
class SingleDatasetCogneeTest(HttpUser):
"""
Scenario 2. calling Add → Cognify → Search on a
single dataset. All virtual users share one dataset name and API key,
simulating concurrent calls from the same account.
"""
tasks = [SingleDatasetFlow]
wait_time = between(5, 10)
# TODO: Return weight when scenario is fixed to handle concurrent cognify calls on the same dataset.
weight = 0
if __name__ == "__main__":
import signal
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
def wait_for_server(url: str, timeout: float = 240.0) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
try:
with urllib.request.urlopen(url, timeout=2) as resp:
if resp.status == 200:
return
except (urllib.error.URLError, ConnectionError, TimeoutError):
pass
time.sleep(0.5)
raise SystemExit(f"Cognee server at {url} did not become ready in {timeout}s")
key_path = tempfile.NamedTemporaryFile(suffix=".key", delete=False).name
try:
# Generate API key in a separate process to avoid any potential issues with locust's monkey-patching of libraries like gevent.
perf_dir = str(Path(__file__).resolve().parent)
subprocess.run(
[sys.executable, "-m", "utils.bootstrap_script", key_path],
check=True,
cwd=perf_dir,
)
api_key = Path(key_path).read_text().strip()
finally:
try:
os.unlink(key_path)
except FileNotFoundError:
pass
host = os.environ.get("HTTP_API_HOST", "localhost")
port = os.environ.get("HTTP_API_PORT", "8000")
base_url = f"http://{host}:{port}"
server_proc = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "cognee.api.client:app", "--host", host, "--port", port],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
try:
wait_for_server(f"{base_url}/health")
env = {**os.environ, "COGNEE_API_KEY": api_key}
# Timestamped results to avoid overwriting previous runs and for easier identification of test runs.
now = datetime.now()
timestamp = now.strftime("%Y%m%d_%H%M%S")
# Results will be saved in the `results` directory with a unique name based on the timestamp of the test run.
result_folder = Path("results")
result_folder.mkdir(exist_ok=True)
result_location = result_folder / f"locust_run_{timestamp}"
html_result_location = f"{result_location}.html"
locust_log_location = Path(f"{result_location}.log")
locust_log_location.touch()
# Run locust command
cmd = [
sys.executable,
"-m",
"locust",
"-f",
__file__,
"--host",
base_url,
"--csv",
result_location,
"--html",
html_result_location,
"--logfile",
locust_log_location,
"--headless",
"-u",
"10",
"-r",
"1",
"--run-time",
"5min",
"MultiDatasetCogneeTest", # Run only the multi-dataset scenario
*sys.argv[1:],
]
rc = subprocess.run(cmd, env=env).returncode
finally:
try:
os.killpg(server_proc.pid, signal.SIGTERM)
server_proc.wait(timeout=10)
except ProcessLookupError:
pass
except subprocess.TimeoutExpired:
try:
os.killpg(server_proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
stats_csv = Path(f"{result_location}_stats.csv")
if stats_csv.exists():
averages = read_endpoint_averages(stats_csv)
print("\n=== Average response times (ms) ===")
for name in ENDPOINT_NAMES:
avg = averages.get(name)
if avg is None:
print(f" {name}: (no requests recorded)")
else:
print(f" {name}: {avg:.0f} ms")
else:
print(f"\nNo stats CSV found at {stats_csv}; skipping averages.")
sys.exit(rc)