feat(pipeline): add metrics collection and v2 run mode

- Add RenderStage adapter that handles rendering pipeline
- Add EffectPluginStage with proper EffectContext
- Add DisplayStage with init handling
- Add ItemsStage for pre-fetched items
- Add metrics collection to Pipeline (StageMetrics, FrameMetrics)
- Add get_metrics_summary() and reset_metrics() methods
- Add --pipeline and --pipeline-preset flags for v2 mode
- Add PipelineNode.metrics for self-documenting introspection
- Add introspect_new_pipeline() method with performance data
- Add mise tasks: run-v2, run-v2-demo, run-v2-poetry, run-v2-websocket, run-v2-firehose
This commit is contained in:
2026-03-16 03:39:29 -07:00
parent bcb4ef0cfe
commit 31cabe9128
6 changed files with 622 additions and 3 deletions

View File

@@ -36,6 +36,7 @@ class PipelineNode:
description: str = ""
inputs: list[str] | None = None
outputs: list[str] | None = None
metrics: dict | None = None # Performance metrics (avg_ms, min_ms, max_ms)
class PipelineIntrospector:
@@ -76,6 +77,14 @@ class PipelineIntrospector:
if node.description:
label += f"\\n{node.description}"
if node.metrics:
avg = node.metrics.get("avg_ms", 0)
if avg > 0:
label += f"\\n⏱ {avg:.1f}ms"
impact = node.metrics.get("impact_pct", 0)
if impact > 0:
label += f" ({impact:.0f}%)"
node_entry = f' {node_id}["{label}"]'
if "DataSource" in node.name or "SourceRegistry" in node.name:
@@ -501,6 +510,78 @@ class PipelineIntrospector:
)
)
def introspect_new_pipeline(self, pipeline=None) -> None:
"""Introspect new unified pipeline stages with metrics.
Args:
pipeline: Optional Pipeline instance to collect metrics from
"""
stages_info = [
(
"ItemsSource",
"engine.pipeline.adapters",
"ItemsStage",
"Provides pre-fetched items",
),
(
"Render",
"engine.pipeline.adapters",
"RenderStage",
"Renders items to buffer",
),
(
"Effect",
"engine.pipeline.adapters",
"EffectPluginStage",
"Applies effect",
),
(
"Display",
"engine.pipeline.adapters",
"DisplayStage",
"Outputs to display",
),
]
metrics = None
if pipeline and hasattr(pipeline, "get_metrics_summary"):
metrics = pipeline.get_metrics_summary()
if "error" in metrics:
metrics = None
total_avg = metrics.get("pipeline", {}).get("avg_ms", 0) if metrics else 0
for stage_name, module, class_name, desc in stages_info:
node_metrics = None
if metrics and "stages" in metrics:
for name, stats in metrics["stages"].items():
if stage_name.lower() in name.lower():
impact_pct = (
(stats.get("avg_ms", 0) / total_avg * 100)
if total_avg > 0
else 0
)
node_metrics = {
"avg_ms": stats.get("avg_ms", 0),
"min_ms": stats.get("min_ms", 0),
"max_ms": stats.get("max_ms", 0),
"impact_pct": impact_pct,
}
break
self.add_node(
PipelineNode(
name=f"Pipeline: {stage_name}",
module=module,
class_name=class_name,
description=desc,
inputs=["data"],
outputs=["data"],
metrics=node_metrics,
)
)
def run(self) -> str:
"""Run full introspection."""
self.introspect_sources()