Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/flameox/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
from flameox.adapters.setup_runtime import * # noqa: F403
from flameox.adapters.torch_profiler import * # noqa: F403
from flameox.adapters.toxiproxy import * # noqa: F403
from flameox.adapters.v8_cpu_prof import * # noqa: F403
from flameox.adapters.v8_heap_prof import * # noqa: F403


_MODULES = (
Expand All @@ -50,6 +52,8 @@
"registry",
"setup_runtime",
"torch_profiler",
"v8_cpu_prof",
"v8_heap_prof",
"toxiproxy",
)

Expand Down Expand Up @@ -135,6 +139,10 @@
"TraceProcessorInstallation",
"TraceWindowResult",
"TritonCompilerOptions",
"V8CpuProfExtractionResult",
"V8CpuProfExtractor",
"V8HeapProfExtractionResult",
"V8HeapProfExtractor",
Comment on lines +142 to +145

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Expose the V8 extractors through CLI and MCP

These exports make the extractors callable only from Python; the reviewed src/flameox/cli.py extract command group and src/flameox/mcp/server.py tool registrations contain no CPU- or heap-V8 extraction entry point. Thus an agent following the documented capture → extract workflow can capture these artifacts but cannot produce the newly implemented normalized evidence through either supported transport, unlike the existing Memray and other extractors. Add corresponding CLI commands and MCP tools over the same extraction behavior.

AGENTS.md reference: AGENTS.md:L40-L45

Useful? React with 👍 / 👎.

"VllmAggregateMetrics",
"VllmMeasurementRow",
"VllmResultDocument",
Expand Down
120 changes: 120 additions & 0 deletions src/flameox/adapters/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,51 @@ class CaptureInvocation:
expected_overhead="No profiler overhead; process output only.",
capture_limitations=("No sampled stack or operator evidence is collected.",),
),
BuiltinAdapter(
name="node-cpu-prof",
dependency_kind=AdapterDependencyKind.EXECUTABLE,
dependency="node",
supported_modes=("record",),
supported_formats=("v8-cpuprofile",),
features=("sampled_stacks", "javascript_symbols"),
remediation=(
"Install Node.js 20.16+ or 22.4+ which expose stable --cpu-prof flags.",
),
version_args=("--version",),
Comment on lines +82 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the declared Node version compatibility floor

On a host with Node older than the stated 20.16+/22.4+ stable-support floor, capability discovery and active probing still report these adapters as available because they only resolve node and run node --version; no code interprets or constrains the returned version. Planning can therefore authorize a producer version the adapter describes as unsupported without any compatibility limitation. Parse the probed Node version and mark older releases incompatible, or truthfully declare and qualify the broader supported range.

AGENTS.md reference: AGENTS.md:L43-L45

Useful? React with 👍 / 👎.

output_filename="cpu.cpuprofile",
artifact_kinds=(ArtifactKind.SAMPLE_PROFILE,),
Comment on lines +86 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve valid V8 profiles from failing workloads

When a Node workload exits nonzero after writing its profile—for example, an uncaught exception—both --cpu-prof and --heap-prof still emit parseable native artifacts, but these descriptors leave preserve_artifact_on_nonzero at its default False. The capture finalization path therefore quarantines the profile and omits it from the run precisely when reliability investigations need evidence from the failed attempt; set the preservation flag for both Node adapters while retaining the nonzero-exit limitation.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

expected_overhead=(
"V8 CPU sampling overhead; exact rate depends on --cpu-prof-interval."
),
capture_limitations=(
"Only the main Node.js thread is profiled; worker threads are not sampled.",
"The CPU profile contains sampled stack locations, not wall-clock or "
"allocation evidence.",
),
),
BuiltinAdapter(
name="node-heap-prof",
dependency_kind=AdapterDependencyKind.EXECUTABLE,
dependency="node",
supported_modes=("record",),
supported_formats=("v8-sampling-heap-profile",),
features=("allocations", "sampled_allocations", "stacks"),
remediation=(
"Install Node.js 20.16+ or 22.4+ which expose stable --heap-prof flags.",
),
version_args=("--version",),
output_filename="heap.heapprofile",
artifact_kinds=(ArtifactKind.MEMORY_PROFILE,),
expected_overhead=(
"V8 heap sampling overhead; exact rate depends on --heap-prof-interval."
),
capture_limitations=(
"Sampled allocation bytes are an estimate, not the exact retained heap or "
"process RSS.",
"Only allocations sampled by V8 are reported; small or short-lived "
"allocations may be underrepresented.",
),
),
BuiltinAdapter(
name="benchmark-samples",
dependency_kind=AdapterDependencyKind.INTERNAL,
Expand Down Expand Up @@ -520,6 +565,15 @@ def build_capture_invocation( # noqa: C901 - provider routing is intentionally
output,
*target,
)
elif adapter_name in {"node-cpu-prof", "node-heap-prof"}:
return _node_v8_capture_invocation(
adapter_name,
adapter,
workload_argv,
output_root,
output,
executable=executable,
)
elif adapter_name == "torch.profiler":
return _torch_capture_invocation(
adapter,
Expand Down Expand Up @@ -742,6 +796,72 @@ def _torch_capture_invocation(
)


def _node_v8_capture_invocation(
adapter_name: str,
adapter: BuiltinAdapter,
workload_argv: tuple[str, ...],
output_root: Path,
output: str,
*,
executable: str | None,
) -> CaptureInvocation:
"""Inject Node.js --cpu-prof or --heap-prof flags into a declared Node workload.

Node.js exposes stable V8 profiling through CLI flags (Node 20.16+ / 22.4+).
The declared workload argv already starts with the Node executable, so the
adapter inserts the profiling flags immediately after argv[0] and before the
user script and its arguments. The output directory and file name are bound
explicitly so Flameox owns the artifact path and can preserve it.
"""
if not workload_argv:
raise DomainError(
ErrorCode.INVALID_CAPTURE_PLAN,
"A declared Node.js workload command is required for V8 profiling.",
)
node_executable = workload_argv[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind producer metadata to the Node executable actually run

When a workload names a custom Node executable such as /opt/node18/bin/node while capability discovery resolves a different PATH node, this assignment runs the workload's executable but the capture plan records the capability executable's version as adapter_version, which is later registered as the artifact's producer version. The result is internally inconsistent provenance and can qualify or compare a Node 18 artifact as though Node 24 produced it. Probe and bind the declared workload executable itself, or reject a mismatch instead of ignoring the supplied resolved adapter executable.

AGENTS.md reference: AGENTS.md:L31-L35

Useful? React with 👍 / 👎.

node_name = Path(node_executable).name
if not (
node_name == "node"
or node_name.startswith("node")
or node_executable.endswith("node")
):
raise DomainError(
ErrorCode.INVALID_CAPTURE_PLAN,
f"V8 profiling requires a Node.js workload; the declared "
f"executable is {node_executable!r}.",
remediation=("Declare a Node.js command (e.g. `node script.js`) as the workload.",),
)
if len(workload_argv) < 2:
raise DomainError(
ErrorCode.INVALID_CAPTURE_PLAN,
"A declared Node.js script or module is required after the node executable.",
)
# Compute the directory and file name for the V8 profile output.
output_path = Path(output)
prof_dir = str(output_path.parent)
prof_name = output_path.name
if adapter_name == "node-cpu-prof":
prof_flags = (
"--cpu-prof",
"--cpu-prof-dir=" + prof_dir,
"--cpu-prof-name=" + prof_name,
)
else:
prof_flags = (
"--heap-prof",
"--heap-prof-dir=" + prof_dir,
"--heap-prof-name=" + prof_name,
)
argv = (node_executable, *prof_flags, *workload_argv[1:])
return CaptureInvocation(
argv=argv,
artifact_kinds=adapter.artifact_kinds,
expected_overhead=adapter.expected_overhead or "",
limitations=adapter.capture_limitations,
environment={},
)


def _compute_sanitizer_capture_invocation(
adapter: BuiltinAdapter,
workload_argv: tuple[str, ...],
Expand Down
Loading
Loading