mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-29 07:14:56 +00:00
Adds two new functions to harness.py: - _current_git_ref(): Gets the current git short SHA - append_profiling_history(): Appends JSON line to profiling/results/history.jsonl This enables profiling numbers to persist across sessions/dates, with every run appended to an immutable timeline rather than overwriting per-stage snapshots. Also adds corresponding test to verify the function writes valid JSON lines. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
124 lines
3.8 KiB
Python
124 lines
3.8 KiB
Python
# profiling/harness.py
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import time
|
|
from dataclasses import dataclass
|
|
from datetime import UTC
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
from typing import Any
|
|
from typing import Generic
|
|
from typing import TypeVar
|
|
|
|
import pytest
|
|
from django.db import connection
|
|
from django.test.utils import CaptureQueriesContext
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
from django.db.models import QuerySet
|
|
|
|
from profiling.seed import ScaleProfile
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def require_postgres() -> None:
|
|
if connection.vendor != "postgresql":
|
|
pytest.skip(
|
|
"Profiling requires PostgreSQL to reflect production query "
|
|
"planning; SQLite does not reproduce the varchar-cast planner "
|
|
"pathology this work fixes.",
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ProfileResult(Generic[T]):
|
|
best_seconds: float
|
|
all_seconds: tuple[float, ...]
|
|
query_count: int
|
|
result: T
|
|
|
|
|
|
def run_profile(fn: Callable[[], T], *, repeat: int = 5) -> ProfileResult[T]:
|
|
require_postgres()
|
|
all_seconds: list[float] = []
|
|
result: T | None = None
|
|
query_count = 0
|
|
for i in range(repeat):
|
|
with CaptureQueriesContext(connection) as ctx:
|
|
start = time.perf_counter()
|
|
result = fn()
|
|
all_seconds.append(time.perf_counter() - start)
|
|
if i == repeat - 1:
|
|
query_count = len(ctx.captured_queries)
|
|
assert result is not None # repeat >= 1 guarantees at least one assignment
|
|
return ProfileResult(
|
|
best_seconds=min(all_seconds),
|
|
all_seconds=tuple(all_seconds),
|
|
query_count=query_count,
|
|
result=result,
|
|
)
|
|
|
|
|
|
def capture_explain_analyze(queryset: QuerySet[Any]) -> str:
|
|
if connection.vendor != "postgresql":
|
|
raise RuntimeError("EXPLAIN ANALYZE capture is Postgres-only")
|
|
sql, params = queryset.query.sql_with_params()
|
|
with connection.cursor() as cur:
|
|
cur.execute(f"EXPLAIN ANALYZE {sql}", params)
|
|
return "\n".join(row[0] for row in cur.fetchall())
|
|
|
|
|
|
def _current_git_ref() -> str:
|
|
result = subprocess.run(
|
|
["git", "rev-parse", "--short", "HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
return result.stdout.strip() or "unknown"
|
|
|
|
|
|
def append_profiling_history(
|
|
*,
|
|
stage: str,
|
|
scenario: str,
|
|
best_seconds: float,
|
|
query_count: int,
|
|
scale: ScaleProfile,
|
|
code_ref: str | None = None,
|
|
) -> None:
|
|
"""
|
|
Append one line to profiling/results/history.jsonl -- an append-only,
|
|
cross-session record of every profiling run. Unlike the per-stage
|
|
before/after snapshot JSON files (which the next run of the same stage
|
|
overwrites), this never gets clobbered, so a profiling effort picked
|
|
back up days later has a full timeline to resume from instead of only
|
|
the single most recent comparison.
|
|
|
|
``code_ref`` should identify the *production* code under test (a short
|
|
SHA on the feature branch, not this `profiling` branch) -- pass it
|
|
explicitly when known. Left as ``None``, it falls back to this repo's
|
|
current ``git rev-parse --short HEAD``, which is only meaningful if
|
|
you're actually running from a checkout where that reflects the code
|
|
being measured.
|
|
"""
|
|
results_dir = Path(__file__).parent / "results"
|
|
results_dir.mkdir(exist_ok=True)
|
|
entry = {
|
|
"timestamp": datetime.now(UTC).isoformat(),
|
|
"code_ref": code_ref or _current_git_ref(),
|
|
"stage": stage,
|
|
"scenario": scenario,
|
|
"scale": scale,
|
|
"best_seconds": best_seconds,
|
|
"query_count": query_count,
|
|
}
|
|
with (results_dir / "history.jsonl").open("a") as f:
|
|
f.write(json.dumps(entry) + "\n")
|