Add structured compiler diagnostics
This commit is contained in:
parent
0fe968c475
commit
24bd13f9d9
20 changed files with 1386 additions and 58 deletions
220
src/docforge/telemetry.py
Normal file
220
src/docforge/telemetry.py
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
"""Bounded request-local diagnostics for repository gates and explicit profiling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal
|
||||
|
||||
CounterName = Literal[
|
||||
"project_loads",
|
||||
"source_files_parsed",
|
||||
"source_bytes_parsed",
|
||||
"adapter_projection_loads",
|
||||
"adapter_source_extractions",
|
||||
"source_generation_checks",
|
||||
"index_checks",
|
||||
"index_synchronizations",
|
||||
"index_builds",
|
||||
"render_prepare_calls",
|
||||
"render_output_bytes_built",
|
||||
"render_output_bytes_hashed",
|
||||
"viewer_manager_requests",
|
||||
]
|
||||
StageName = Literal[
|
||||
"source.generation",
|
||||
"source.parse",
|
||||
"adapter.projection",
|
||||
"adapter.extract",
|
||||
"index.check",
|
||||
"index.synchronize",
|
||||
"index.build",
|
||||
"index.read",
|
||||
"render.status",
|
||||
"render.prepare",
|
||||
"render.output_hash",
|
||||
"visualization.status",
|
||||
"viewer.manager",
|
||||
"mcp.runtime_validation",
|
||||
]
|
||||
|
||||
COUNTER_NAMES: tuple[CounterName, ...] = (
|
||||
"project_loads",
|
||||
"source_files_parsed",
|
||||
"source_bytes_parsed",
|
||||
"adapter_projection_loads",
|
||||
"adapter_source_extractions",
|
||||
"source_generation_checks",
|
||||
"index_checks",
|
||||
"index_synchronizations",
|
||||
"index_builds",
|
||||
"render_prepare_calls",
|
||||
"render_output_bytes_built",
|
||||
"render_output_bytes_hashed",
|
||||
"viewer_manager_requests",
|
||||
)
|
||||
STAGE_NAMES: frozenset[StageName] = frozenset(
|
||||
{
|
||||
"source.generation",
|
||||
"source.parse",
|
||||
"adapter.projection",
|
||||
"adapter.extract",
|
||||
"index.check",
|
||||
"index.synchronize",
|
||||
"index.build",
|
||||
"index.read",
|
||||
"render.status",
|
||||
"render.prepare",
|
||||
"render.output_hash",
|
||||
"visualization.status",
|
||||
"viewer.manager",
|
||||
"mcp.runtime_validation",
|
||||
}
|
||||
)
|
||||
OPERATION_NAMES = frozenset(
|
||||
{
|
||||
"test",
|
||||
"benchmark.m1",
|
||||
"mcp.invoke",
|
||||
"mcp.bootstrap",
|
||||
"mcp.sync",
|
||||
"mcp.project_info",
|
||||
"mcp.contract",
|
||||
"mcp.get_node",
|
||||
"mcp.get_logic",
|
||||
"mcp.search",
|
||||
"mcp.filter",
|
||||
"mcp.backlinks",
|
||||
"mcp.dependencies",
|
||||
"mcp.impact",
|
||||
"mcp.context",
|
||||
"mcp.validate_project",
|
||||
"mcp.render_status",
|
||||
"mcp.visualize",
|
||||
"mcp.visualization_status",
|
||||
"mcp.stop_visualization",
|
||||
"mcp.changeset",
|
||||
"mcp.mutation",
|
||||
"cli.onboard",
|
||||
"cli.info",
|
||||
"cli.validate",
|
||||
"cli.build",
|
||||
"cli.reindex",
|
||||
"cli.sync",
|
||||
"cli.check",
|
||||
"cli.validate-index",
|
||||
"cli.show",
|
||||
"cli.search",
|
||||
"cli.filter",
|
||||
"cli.backlinks",
|
||||
"cli.dependencies",
|
||||
"cli.impact",
|
||||
"cli.context",
|
||||
"cli.render",
|
||||
"cli.render-status",
|
||||
"cli.preview",
|
||||
"cli.apply",
|
||||
"cli.visualize",
|
||||
"cli.visualization-status",
|
||||
"cli.visualization-stop",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _StageAggregate:
|
||||
calls: int = 0
|
||||
elapsed_ns: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Collector:
|
||||
"""One bounded aggregate owned by the current request context."""
|
||||
|
||||
operation: str
|
||||
counters: dict[CounterName, int] = field(
|
||||
default_factory=lambda: {name: 0 for name in COUNTER_NAMES}
|
||||
)
|
||||
stages: dict[StageName, _StageAggregate] = field(default_factory=lambda: {})
|
||||
elapsed_ns: int = 0
|
||||
|
||||
def as_dict(self, *, outcome: str) -> dict[str, object]:
|
||||
if outcome not in {"ok", "error"}:
|
||||
raise ValueError("Telemetry outcome must be ok or error")
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"operation": self.operation,
|
||||
"outcome": outcome,
|
||||
"elapsed_ns": self.elapsed_ns,
|
||||
"stages": {
|
||||
name: {
|
||||
"calls": aggregate.calls,
|
||||
"elapsed_ns": aggregate.elapsed_ns,
|
||||
}
|
||||
for name, aggregate in sorted(self.stages.items())
|
||||
},
|
||||
"counters": {name: self.counters[name] for name in COUNTER_NAMES},
|
||||
}
|
||||
|
||||
|
||||
_CURRENT: ContextVar[Collector | None] = ContextVar(
|
||||
"docforge_telemetry",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def request(
|
||||
operation: str,
|
||||
*,
|
||||
enabled: bool,
|
||||
) -> Generator[Collector | None, None, None]:
|
||||
"""Collect one explicit request without affecting the disabled path."""
|
||||
|
||||
if operation not in OPERATION_NAMES:
|
||||
raise ValueError("Unknown telemetry operation")
|
||||
if not enabled:
|
||||
yield None
|
||||
return
|
||||
collector = Collector(operation=operation)
|
||||
token = _CURRENT.set(collector)
|
||||
started = time.perf_counter_ns()
|
||||
try:
|
||||
yield collector
|
||||
finally:
|
||||
collector.elapsed_ns = time.perf_counter_ns() - started
|
||||
_CURRENT.reset(token)
|
||||
|
||||
|
||||
def increment(counter: CounterName | str, amount: int = 1) -> None:
|
||||
"""Increment one fixed counter when a request collector is active."""
|
||||
|
||||
if counter not in COUNTER_NAMES:
|
||||
raise ValueError("Unknown telemetry counter")
|
||||
if type(amount) is not int or amount < 0:
|
||||
raise ValueError("Telemetry increments must be nonnegative integers")
|
||||
collector = _CURRENT.get()
|
||||
if collector is not None:
|
||||
collector.counters[counter] += amount
|
||||
|
||||
|
||||
@contextmanager
|
||||
def stage(name: StageName | str) -> Generator[None, None, None]:
|
||||
"""Aggregate one fixed stage while avoiding a clock read when disabled."""
|
||||
|
||||
if name not in STAGE_NAMES:
|
||||
raise ValueError("Unknown telemetry stage")
|
||||
collector = _CURRENT.get()
|
||||
if collector is None:
|
||||
yield
|
||||
return
|
||||
started = time.perf_counter_ns()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
aggregate = collector.stages.setdefault(name, _StageAggregate())
|
||||
aggregate.calls += 1
|
||||
aggregate.elapsed_ns += time.perf_counter_ns() - started
|
||||
Loading…
Add table
Add a link
Reference in a new issue