diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index ff956e9..ff8168d 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -44,4 +44,4 @@ jobs:
run: python -m pip install ".[test]"
- name: Run Python tests
- run: python -m pytest -v tests/python_bindings_tests.py tests/profiler_loader_tests.py
+ run: python -m pytest -v tests/python_bindings_tests.py
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 246c13f..eef22e8 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -77,7 +77,7 @@ jobs:
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28
CIBW_TEST_COMMAND: >-
- python -c "import calf, google.protobuf; from calf.protobuf import calf_trace_pb2; assert calf.__version__ == '${{ needs.release-version.outputs.version }}'; assert calf_trace_pb2.TraceFile() is not None"
+ python -c "import calf; assert calf.__version__ == '${{ needs.release-version.outputs.version }}'"
MACOSX_DEPLOYMENT_TARGET: "15.0"
run: python -m cibuildwheel --output-dir wheelhouse
diff --git a/README.md b/README.md
index b4721d6..478f58f 100644
--- a/README.md
+++ b/README.md
@@ -25,22 +25,12 @@ writes standard `.perfetto-trace` protobuf streams that open directly in the
while `LOG` messages are instant TrackEvents with source locations and an
`args` debug annotation.
-## CALF Inspector
-
-CALF includes an interactive Inspector for exploring and analysing structured
-trace logs. Its responsive web interface provides call-tree navigation, lazy
-trace loading, search, timing details, and aggregated statistics.
-
-See the [CALF Inspector documentation](profiler/README.md) for installation,
-usage and web-server options.
-
-
## Requirements
- C++17 or later
- CMake 3.16 or later
- Linux for `SyscallLogger` backend
-- Python 3.10 or later for the Python bindings and Inspector
+- Python 3.10 or later for the Python bindings
## Testing
@@ -185,8 +175,7 @@ cmake -S . -B build \
### Python bindings
-The Python package includes bindings for the STL file logger and stdout logger,
-as well as the CALF Inspector.
+The Python package includes bindings for the STL file logger and stdout logger.
#### Installation
@@ -263,17 +252,6 @@ calf.StdoutLogger.print("finished")
Available colors are `CLI_LEVEL_RESET`, `CLI_LEVEL_STATUS`, `CLI_LEVEL_INFO`,
`CLI_LEVEL_WARNING`, and `CLI_LEVEL_ERROR`.
-#### Inspector
-
-Installing the package also installs the bundled CALF Inspector:
-
-```bash
-calf calf_logs
-```
-
-It can also be launched with `python -m calf`. See the
-[CALF Inspector documentation](profiler/README.md) for all options.
-
### Server / non-interceptor code
Include `StlLogger.h`. Logging is enabled by default on every thread.
diff --git a/bindings/calf/__main__.py b/bindings/calf/__main__.py
deleted file mode 100644
index ecd85b9..0000000
--- a/bindings/calf/__main__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from .profiler.__main__ import main
-
-
-if __name__ == "__main__":
- main()
diff --git a/bindings/calf/protobuf/__init__.py b/bindings/calf/protobuf/__init__.py
deleted file mode 100644
index 225163e..0000000
--- a/bindings/calf/protobuf/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Generated CALF protobuf messages."""
diff --git a/bindings/calf/protobuf/calf_trace_pb2.py b/bindings/calf/protobuf/calf_trace_pb2.py
deleted file mode 100644
index 49e3fbc..0000000
--- a/bindings/calf/protobuf/calf_trace_pb2.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by the protocol buffer compiler. DO NOT EDIT!
-# NO CHECKED-IN PROTOBUF GENCODE
-# source: calf/protobuf/calf_trace.proto
-# Protobuf Python Version: 6.31.1
-"""Generated protocol buffer code."""
-from google.protobuf import descriptor as _descriptor
-from google.protobuf import descriptor_pool as _descriptor_pool
-from google.protobuf import runtime_version as _runtime_version
-from google.protobuf import symbol_database as _symbol_database
-from google.protobuf.internal import builder as _builder
-_runtime_version.ValidateProtobufRuntimeVersion(
- _runtime_version.Domain.PUBLIC,
- 6,
- 31,
- 1,
- '',
- 'calf/protobuf/calf_trace.proto'
-)
-# @@protoc_insertion_point(imports)
-
-_sym_db = _symbol_database.Default()
-
-
-
-
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x63\x61lf/protobuf/calf_trace.proto\x12\x0fperfetto.protos\"5\n\x05Trace\x12,\n\x06packet\x18\x01 \x03(\x0b\x32\x1c.perfetto.protos.TracePacket\"\xce\x01\n\x0bTracePacket\x12\x11\n\ttimestamp\x18\x08 \x01(\x04\x12\"\n\x1atrusted_packet_sequence_id\x18\n \x01(\r\x12\x30\n\x0btrack_event\x18\x0b \x01(\x0b\x32\x1b.perfetto.protos.TrackEvent\x12\x1a\n\x12timestamp_clock_id\x18: \x01(\r\x12:\n\x10track_descriptor\x18< \x01(\x0b\x32 .perfetto.protos.TrackDescriptor\"\xc3\x02\n\nTrackEvent\x12;\n\x11\x64\x65\x62ug_annotations\x18\x04 \x03(\x0b\x32 .perfetto.protos.DebugAnnotation\x12.\n\x04type\x18\t \x01(\x0e\x32 .perfetto.protos.TrackEvent.Type\x12\x12\n\ntrack_uuid\x18\x0b \x01(\x04\x12\x12\n\ncategories\x18\x16 \x03(\t\x12\x0c\n\x04name\x18\x17 \x01(\t\x12\x38\n\x0fsource_location\x18! \x01(\x0b\x32\x1f.perfetto.protos.SourceLocation\"X\n\x04Type\x12\x14\n\x10TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10TYPE_SLICE_BEGIN\x10\x01\x12\x12\n\x0eTYPE_SLICE_END\x10\x02\x12\x10\n\x0cTYPE_INSTANT\x10\x03\"5\n\x0f\x44\x65\x62ugAnnotation\x12\x14\n\x0cstring_value\x18\x06 \x01(\t\x12\x0c\n\x04name\x18\n \x01(\t\"O\n\x0eSourceLocation\x12\x11\n\tfile_name\x18\x02 \x01(\t\x12\x15\n\rfunction_name\x18\x03 \x01(\t\x12\x13\n\x0bline_number\x18\x04 \x01(\r\"`\n\x0fTrackDescriptor\x12\x0c\n\x04uuid\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x31\n\x06thread\x18\x04 \x01(\x0b\x32!.perfetto.protos.ThreadDescriptor\"A\n\x10ThreadDescriptor\x12\x0b\n\x03pid\x18\x01 \x01(\x05\x12\x0b\n\x03tid\x18\x02 \x01(\x03\x12\x13\n\x0bthread_name\x18\x05 \x01(\t')
-
-_globals = globals()
-_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
-_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'calf.protobuf.calf_trace_pb2', _globals)
-if not _descriptor._USE_C_DESCRIPTORS:
- DESCRIPTOR._loaded_options = None
- _globals['_TRACE']._serialized_start=51
- _globals['_TRACE']._serialized_end=104
- _globals['_TRACEPACKET']._serialized_start=107
- _globals['_TRACEPACKET']._serialized_end=313
- _globals['_TRACKEVENT']._serialized_start=316
- _globals['_TRACKEVENT']._serialized_end=639
- _globals['_TRACKEVENT_TYPE']._serialized_start=551
- _globals['_TRACKEVENT_TYPE']._serialized_end=639
- _globals['_DEBUGANNOTATION']._serialized_start=641
- _globals['_DEBUGANNOTATION']._serialized_end=694
- _globals['_SOURCELOCATION']._serialized_start=696
- _globals['_SOURCELOCATION']._serialized_end=775
- _globals['_TRACKDESCRIPTOR']._serialized_start=777
- _globals['_TRACKDESCRIPTOR']._serialized_end=873
- _globals['_THREADDESCRIPTOR']._serialized_start=875
- _globals['_THREADDESCRIPTOR']._serialized_end=940
-# @@protoc_insertion_point(module_scope)
diff --git a/profiler/README.md b/profiler/README.md
deleted file mode 100644
index 1557050..0000000
--- a/profiler/README.md
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
-
-
-# CALF Inspector
-
-CALF Inspector is an interactive viewer and analyser for structured trace logs
-produced by [CALF](../README.md). It provides a responsive browser interface
-backed by the CALF trace loader and statistics engine.
-
-## Features
-
-- Navigate logs by host, component, and thread.
-- Explore nested scopes and events as a call tree.
-- Inspect source locations, arguments, timestamps, and durations.
-- Search invokers, arguments, and source paths.
-- Search every visible web column at once. Serialized `req=` messages are
- correlated by their complete normalized request and occurrence order, with a
- shared-parameter fallback for older logs; other calls use invoker and time.
-- Compare aggregated timing statistics by invoker.
-- Review duration percentiles, slow scopes, frequent calls, and costly call paths.
-- Export filtered web events as JSON, CSV, or a standalone HTML report.
-- Repair and read logs truncated by an abrupt process exit.
-- Load web traces one level at a time to limit browser memory usage.
-
-## Requirements
-
-- Python 3.10 or later
-- A modern browser
-
-Python dependencies are installed automatically from `pyproject.toml`.
-
-## Installation
-
-From the CALF repository root:
-
-```bash
-python -m pip install .
-```
-
-For development, use an editable installation:
-
-```bash
-python -m pip install -e .
-```
-
-To install directly from GitHub:
-
-```bash
-python -m pip install "capio-calf @ git+https://github.com/High-Performance-IO/CALF.git"
-```
-
-Installation provides the `calf` command.
-
-## Log directory
-
-Inspector expects the directory structure generated by CALF:
-
-```text
-calf_logs/
-|-- /
-| `-- _.perfetto-trace
-|-- syscall/
-| `-- /
-| `-- .log
-`-- stl/
- `-- /
- `-- .log
-```
-
-Each Perfetto file contains all component threads for one process. JSON thread
-files are presented independently. Other files are grouped under an `unknown`
-backend when possible.
-
-## Web interface
-
-Launch the browser interface with:
-
-```bash
-calf calf_logs
-```
-
-If the directory is omitted, Inspector uses `calf_logs` in the current working
-directory.
-
-Inspector listens on `127.0.0.1:8765` and opens the default browser. The page
-starts with one trace column. Each column selects a log directory and provides
-tabs for its thread logs, a scrollable call tree, a paginated event table, node
-details, server-side search, and aggregated statistics. Use **Add column** to
-compare directories or threads side by side.
-
-The browser initially receives only root trace nodes. Expanding a node requests
-only its immediate children. The **Expand level** control advances one loaded
-level at a time rather than downloading the complete trace tree.
-
-### Web options
-
-| Option | Default | Description |
-|----------------|-------------|------------------------------------------|
-| `--host HOST` | `127.0.0.1` | Address used by the web server |
-| `--port PORT` | `8765` | Port used by the web server |
-| `--no-browser` | Disabled | Do not open a browser automatically |
-
-For remote access, bind to all interfaces and disable automatic browser launch:
-
-```bash
-calf calf_logs --host 0.0.0.0 --port 8765 --no-browser
-```
-
-The built-in server does not provide authentication or TLS. Expose it only on a
-trusted network, or place it behind an authenticated reverse proxy.
-
-## Trace format
-
-CALF logs contain a JSON array. Entries are either scopes or events:
-
-- A scope contains `ts_enter`, `ts_exit`, and a nested `events` array.
-- An event contains a single `ts` timestamp.
-- Both forms include an `invoker`, source `file`, `line`, and formatted `args`.
-
-If a process exits while writing, Inspector attempts to close incomplete JSON
-structures before parsing the file. Files that cannot be repaired are skipped
-and reported during loading.
-
-## Related documentation
-
-See the [main CALF README](../README.md) for C++ integration, logger backends,
-configuration options, macros, and environment variables.
diff --git a/profiler/__init__.py b/profiler/__init__.py
deleted file mode 100644
index 1286bd9..0000000
--- a/profiler/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""Interactive viewer and analyzer for CALF JSON and Perfetto traces."""
-
-from .. import __version__
diff --git a/profiler/__main__.py b/profiler/__main__.py
deleted file mode 100644
index 1eb7a50..0000000
--- a/profiler/__main__.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from __future__ import annotations
-
-import argparse
-import sys
-
-from .loader import discover_tabs
-
-
-def main() -> None:
- parser = argparse.ArgumentParser(
- prog="calf",
- description=(
- "Web inspector and profiler for CALF JSON and Perfetto traces.\n\n"
- "Traces are read from a log directory laid out as:\n"
- " /syscall//.log\n"
- " /stl//.log\n"
- " //_.perfetto-trace\n\n"
- "Each trace file becomes a tab in the web inspector."
- ),
- formatter_class=argparse.RawDescriptionHelpFormatter,
- )
- parser.add_argument(
- "log_dir",
- nargs="?",
- default="calf_logs",
- help="Root log directory (default: calf_logs)",
- )
- parser.add_argument(
- "--host", default="127.0.0.1",
- help="Web server bind address (default: 127.0.0.1)",
- )
- parser.add_argument(
- "--port", type=int, default=8765,
- help="Web server port (default: 8765)",
- )
-
- args = parser.parse_args()
-
- # Discover tabs (fast — no file parsing yet)
- try:
- tabs = discover_tabs(args.log_dir)
- except (FileNotFoundError, RuntimeError) as exc:
- print(f"error: {exc}", file=sys.stderr)
- sys.exit(1)
-
- total_files = len(tabs)
- print(f"Found {len(tabs)} tab(s) across {total_files} file(s):")
-
- for tab in tabs:
- print(f" [READY: {tab.kind:8}] {tab.hostname} tid={tab.tid}")
-
- print("Trace data will be loaded when selected.")
- from .web import run_web
- run_web(tabs, args.host, args.port)
-
-
-if __name__ == "__main__":
- main()
diff --git a/profiler/loader.py b/profiler/loader.py
deleted file mode 100644
index d60cd84..0000000
--- a/profiler/loader.py
+++ /dev/null
@@ -1,369 +0,0 @@
-from __future__ import annotations
-
-import json
-import os
-import threading
-from dataclasses import dataclass, field
-from typing import BinaryIO, Iterable, Iterator, Optional
-
-from calf.protobuf import calf_trace_pb2
-
-
-def repair_and_load(path: str) -> list:
- """
- Load a CALF JSON file, repairing truncation caused by an abrupt
- process exit mid-write.
-
- Strategy: walk the raw text tracking bracket depth (skipping string
- contents and escape sequences), then append the exact closing tokens
- that are missing. Falls back to stripping after the last complete
- root-level entry if the first attempt still fails to parse.
- """
- with open(path, "r", encoding="utf-8", errors="replace") as f:
- raw = f.read().rstrip()
-
- if not raw:
- return []
-
- depth_stack: list[str] = []
- in_string = False
- escape_next = False
-
- for ch in raw:
- if escape_next:
- escape_next = False
- continue
- if ch == "\\" and in_string:
- escape_next = True
- continue
- if ch == '"':
- in_string = not in_string
- continue
- if in_string:
- continue
- if ch == "{":
- depth_stack.append("}")
- elif ch == "[":
- depth_stack.append("]")
- elif ch in ("}", "]"):
- if depth_stack and depth_stack[-1] == ch:
- depth_stack.pop()
-
- # If truncation happened inside a string, close it first.
- if in_string:
- depth_stack.append('"')
-
- repaired = raw + "".join(reversed(depth_stack))
-
- try:
- data = json.loads(repaired)
- except json.JSONDecodeError as exc:
- pos = repaired.rfind("\n }")
- if pos != -1:
- repaired = repaired[: pos + 4] + "\n]"
- try:
- data = json.loads(repaired)
- except json.JSONDecodeError:
- raise RuntimeError(f"Cannot parse {path}: {exc}") from exc
- else:
- raise RuntimeError(f"Cannot parse {path}: {exc}") from exc
-
- if isinstance(data, dict):
- data = [data]
- return data
-
-
-def load_protobuf(path: str) -> list:
- """Load a Perfetto trace stream, ignoring an incomplete final packet."""
- return _perfetto_packets_to_entries(_iter_perfetto_packets(path))
-
-
-def _iter_perfetto_packets(path: str) -> Iterator[calf_trace_pb2.TracePacket]:
- with open(path, "rb", buffering=1024 * 1024) as trace_file:
- offset = 0
- while field := trace_file.read(1):
- if field != b"\x0a": # perfetto.protos.Trace.packet
- raise RuntimeError(
- f"Cannot parse {path}: invalid protobuf field at byte {offset}"
- )
- offset += 1
- length, offset = _read_varint(trace_file, path, offset)
- if length is None:
- break
- payload = trace_file.read(length)
- if len(payload) < length:
- break
- offset += length
- packet = calf_trace_pb2.TracePacket()
- packet.ParseFromString(payload)
- yield packet
-
-
-def _read_varint(
- trace_file: BinaryIO, path: str, offset: int
-) -> tuple[Optional[int], int]:
- value = 0
- shift = 0
- start = offset
- while shift < 64:
- raw = trace_file.read(1)
- if not raw:
- return None, offset
- byte = raw[0]
- offset += 1
- value |= (byte & 0x7F) << shift
- if byte < 0x80:
- return value, offset
- shift += 7
- raise RuntimeError(f"Cannot parse {path}: invalid protobuf varint at byte {start}")
-
-
-def _perfetto_packets_to_entries(
- packets: Iterable[calf_trace_pb2.TracePacket],
-) -> list:
- roots = []
- stacks: dict[int, list[dict]] = {}
-
- for packet in packets:
- if not packet.HasField("track_event"):
- continue
- event = packet.track_event
- stack = stacks.setdefault(event.track_uuid, [])
- timestamp_ms = packet.timestamp // 1_000_000
- source = event.source_location
- args = next(
- (
- annotation.string_value
- for annotation in event.debug_annotations
- if annotation.name == "args"
- ),
- "",
- )
-
- if event.type == calf_trace_pb2.TrackEvent.TYPE_SLICE_BEGIN:
- entry = {
- "invoker": event.name or source.function_name or "?",
- "args": args,
- "file": source.file_name,
- "line": source.line_number,
- "ts_enter": timestamp_ms,
- "ts_exit": None,
- "events": [],
- }
- (stack[-1]["events"] if stack else roots).append(entry)
- stack.append(entry)
- elif event.type == calf_trace_pb2.TrackEvent.TYPE_INSTANT:
- entry = {
- "invoker": event.name or source.function_name or "?",
- "args": args,
- "file": source.file_name,
- "line": source.line_number,
- "ts": timestamp_ms,
- }
- (stack[-1]["events"] if stack else roots).append(entry)
- elif event.type == calf_trace_pb2.TrackEvent.TYPE_SLICE_END and stack:
- stack.pop()["ts_exit"] = timestamp_ms
-
- return roots
-
-
-def load_trace(path: str) -> list:
- if path.lower().endswith(".perfetto-trace"):
- return load_protobuf(path)
- return repair_and_load(path)
-
-
-@dataclass
-class TraceNode:
- """
- One node in the trace tree.
-
- Scope nodes — have ts_enter / ts_exit and a children list.
- Leaf nodes — have ts only; they are LOG() events with no children.
- """
- invoker: str
- args: str
- file: str
- line: int
- ts_enter: Optional[int]
- ts_exit: Optional[int]
- ts: Optional[int]
- children: list[TraceNode]
- is_leaf: bool
- depth: int = 0
- parent: Optional[TraceNode] = field(default=None, repr=False)
-
- @property
- def duration_ms(self) -> Optional[int]:
- if self.ts_enter is not None and self.ts_exit is not None:
- return self.ts_exit - self.ts_enter
- return None
-
- @property
- def timestamp(self) -> Optional[int]:
- return self.ts if self.is_leaf else self.ts_enter
-
- @property
- def short_file(self) -> str:
- """Last two path components, e.g. server/client_manager.cpp"""
- parts = self.file.replace("\\", "/").split("/")
- return "/".join(parts[-2:]) if len(parts) >= 2 else self.file
-
-
-def build_tree(entries: list, depth: int = 0,
- parent: Optional[TraceNode] = None) -> list[TraceNode]:
- nodes: list[TraceNode] = []
- for entry in entries:
- is_scope = "ts_enter" in entry
- children_raw = entry.get("events", [])
- node = TraceNode(
- invoker = entry.get("invoker", "?"),
- args = entry.get("args", ""),
- file = entry.get("file", ""),
- line = entry.get("line", 0),
- ts_enter = entry.get("ts_enter"),
- ts_exit = entry.get("ts_exit"),
- ts = entry.get("ts"),
- children = [],
- is_leaf = not is_scope,
- depth = depth,
- parent = parent,
- )
- node.children = build_tree(children_raw, depth + 1, node)
- nodes.append(node)
- return nodes
-
-
-def flatten(nodes: list[TraceNode]) -> list[TraceNode]:
- """Return all nodes in the tree (depth-first), regardless of expansion."""
- result: list[TraceNode] = []
- for node in nodes:
- result.append(node)
- result.extend(flatten(node.children))
- return result
-
-def compute_stats(roots: list[TraceNode]) -> list[dict]:
- """
- Walk the full tree and compute per-invoker timing statistics.
- Returns a list of dicts sorted by total_ms descending.
- """
- from collections import defaultdict
- import numpy as np
-
- buckets: dict[str, list[int]] = defaultdict(list)
-
- def walk(nodes: list[TraceNode]) -> None:
- for n in nodes:
- if not n.is_leaf and n.duration_ms is not None:
- buckets[n.invoker].append(n.duration_ms)
- walk(n.children)
-
- walk(roots)
-
- rows = []
- for invoker, times in buckets.items():
- arr = np.array(times, dtype=float)
- rows.append({
- "invoker": invoker,
- "count": len(times),
- "total_ms": int(arr.sum()),
- "mean_ms": float(arr.mean()),
- "median_ms": float(np.median(arr)),
- "max_ms": int(arr.max()),
- "min_ms": int(arr.min()),
- "std_ms": float(arr.std()),
- })
-
- rows.sort(key=lambda r: r["total_ms"], reverse=True)
- return rows
-
-@dataclass
-class TraceTab:
- """
- One tab = one log file.
-
- JSON tabs represent one thread. Perfetto tabs contain every thread for a
- component on a host.
- """
- hostname: str
- kind: str # "syscall", "stl", or ...
- path: str # path to the single .log or Perfetto file this tab represents
- _roots: Optional[list[TraceNode]] = field(default=None, repr=False)
- _load_lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
-
- @property
- def tid(self) -> str:
- """Thread-id portion of the filename, e.g. '44623' from 'stl_44623.log'."""
- if self.path.lower().endswith(".perfetto-trace"):
- stem = os.path.basename(self.path)[: -len(".perfetto-trace")]
- _, separator, pid = stem.rpartition("_")
- return f"process {pid} (all threads)" if separator and pid.isdigit() else "all threads"
- stem = os.path.splitext(os.path.basename(self.path))[0]
- # Strip the kind prefix if present (stl_44623 -> 44623)
- if "_" in stem:
- return stem.rsplit("_", 1)[-1]
- return stem
-
- @property
- def label(self) -> str:
- return f"{self.kind} / {self.hostname} / {self.tid}"
-
- @property
- def roots(self) -> list[TraceNode]:
- if self._roots is None:
- with self._load_lock:
- if self._roots is None:
- try:
- data = load_trace(self.path)
- self._roots = build_tree(data)
- except Exception:
- self._roots = []
- return self._roots
-
- @property
- def total_nodes(self) -> int:
- return len(flatten(self.roots))
-
-
-def discover_tabs(log_dir: str) -> list[TraceTab]:
- """
- Walk log_dir and create one TraceTab per .log or .perfetto-trace file.
-
- Expected CALF layout:
- /syscall//.log
- /stl//.log
- //_.perfetto-trace
- """
- if not os.path.isdir(log_dir):
- raise FileNotFoundError(f"Log directory not found: {log_dir!r}")
-
- tabs: list[TraceTab] = []
-
- for root, dirs, files in os.walk(log_dir):
- dirs.sort()
- for fname in sorted(files):
- if not fname.lower().endswith((".log", ".perfetto-trace")):
- continue
- full = os.path.join(root, fname)
- rel = os.path.relpath(full, log_dir).replace("\\", "/")
- parts = rel.split("/")
-
- if fname.lower().endswith(".perfetto-trace"):
- hostname = os.path.basename(root) or "unknown"
- stem = fname[: -len(".perfetto-trace")]
- component, separator, pid = stem.rpartition("_")
- kind = component if separator and pid.isdigit() else stem or "unknown"
- elif len(parts) == 3:
- kind, hostname = parts[0], parts[1]
- else:
- hostname = os.path.basename(root) or "unknown"
- kind = "unknown"
-
- tabs.append(TraceTab(hostname=hostname, kind=kind, path=full))
-
- if not tabs:
- raise RuntimeError(f"No .log or .perfetto-trace files found under {log_dir!r}")
-
- order = {"syscall": 0, "stl": 1, "unknown": 2}
- tabs.sort(key=lambda t: (order.get(t.kind, 9), t.hostname, t.tid))
- return tabs
diff --git a/profiler/web.py b/profiler/web.py
deleted file mode 100644
index 9874bdd..0000000
--- a/profiler/web.py
+++ /dev/null
@@ -1,639 +0,0 @@
-from __future__ import annotations
-
-import csv
-import html
-import io
-import json
-import mimetypes
-import re
-import threading
-import webbrowser
-from collections import Counter, defaultdict
-from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
-from pathlib import Path
-from typing import Optional
-from urllib.parse import parse_qs, urlparse
-
-from .loader import TraceNode, TraceTab, compute_stats, flatten
-
-
-_ASSET_DIR = Path(__file__).with_name("web")
-_ASSETS = {
- "/": "index.html",
- "/index.html": "index.html",
- "/app.js": "app.js",
- "/style.css": "style.css",
- "/calf.svg": "calf.svg",
-}
-
-
-def _time_range(nodes: list[TraceNode]) -> tuple[Optional[int], Optional[int]]:
- timestamps = [
- value
- for node in nodes
- for value in (node.ts, node.ts_enter, node.ts_exit)
- if value is not None
- ]
- if not timestamps:
- return None, None
- return min(timestamps), max(timestamps)
-
-
-def _tab_summary(index: int, tab: TraceTab) -> dict:
- nodes = flatten(tab.roots)
- scopes = sum(not node.is_leaf for node in nodes)
- start, end = _time_range(nodes)
- return {
- "id": index,
- "hostname": tab.hostname,
- "kind": tab.kind,
- "tid": tab.tid,
- "file": Path(tab.path).name,
- "nodes": len(nodes),
- "scopes": scopes,
- "events": len(nodes) - scopes,
- "start_ms": start,
- "end_ms": end,
- "window_ms": end - start if start is not None and end is not None else None,
- }
-
-
-def _tab_metadata(index: int, tab: TraceTab) -> dict:
- return {
- "id": index,
- "hostname": tab.hostname,
- "kind": tab.kind,
- "tid": tab.tid,
- "file": Path(tab.path).name,
- "nodes": None,
- "scopes": None,
- "events": None,
- "start_ms": None,
- "end_ms": None,
- "window_ms": None,
- }
-
-
-def _serialize_node(node: TraceNode, node_id: str) -> dict:
- return {
- "id": node_id,
- "invoker": node.invoker,
- "args": node.args,
- "file": node.short_file,
- "line": node.line,
- "depth": node.depth,
- "is_event": node.is_leaf,
- "duration_ms": node.duration_ms,
- "timestamp_ms": node.timestamp,
- "enter_ms": node.ts_enter,
- "exit_ms": node.ts_exit,
- "child_count": len(node.children),
- "request": _request_identity(node.args),
- "linked_requests": _linked_requests(node, node_id),
- }
-
-
-def _serialize_level(nodes: list[TraceNode], parent_id: str = "") -> list[dict]:
- return [
- _serialize_node(node, f"{parent_id}.{index}" if parent_id else str(index))
- for index, node in enumerate(nodes)
- ]
-
-
-def _request_terms(args: str) -> tuple[str, ...]:
- values = re.findall(r"=([^,)]+)", args)
- terms: list[str] = []
- for value in values:
- for term in value.strip().split():
- normalized = term.strip("'\"")
- if re.fullmatch(r"\d{4}", normalized) or normalized in {"0", "1"}:
- continue
- if normalized and not normalized.startswith("0x"):
- terms.append(normalized.casefold())
- return tuple(terms)
-
-
-def _request_identity(args: str) -> Optional[str]:
- match = re.search(r"(?:^|[\s(])req=([^)]*)", args)
- if not match:
- return None
- return " ".join(match.group(1).strip().casefold().split())
-
-
-def _request_surface_id(node: TraceNode, node_id: str) -> str:
- chain: list[TraceNode] = []
- current: Optional[TraceNode] = node
- while current is not None:
- chain.append(current)
- current = current.parent
- parts = node_id.split(".")
- for candidate in reversed(chain):
- if len(_request_entries(candidate, "", limit=2)) == 1:
- if candidate.parent is None and node.depth == 1:
- continue
- return ".".join(parts[: candidate.depth + 1])
- return node_id
-
-
-def _linked_requests(node: TraceNode, node_id: str) -> list[dict]:
- requests = _request_entries(node, node_id, limit=2)
- if len(requests) != 1:
- return []
- request = requests[0]
- current = node
- base_depth = len(node_id.split("."))
- for index in request["id"].split(".")[base_depth:]:
- current = current.children[int(index)]
- return requests if _request_surface_id(current, request["id"]) == node_id else []
-
-
-def _request_entries(
- node: TraceNode, node_id: str, limit: int = 2
-) -> list[dict]:
- requests: list[dict] = []
-
- identity = _request_identity(node.args)
- if identity:
- requests.append({"id": node_id, "request": identity})
-
- def walk(children: list[TraceNode], parent_id: str) -> None:
- for index, child in enumerate(children):
- if len(requests) >= limit:
- return
- child_id = f"{parent_id}.{index}"
- child_identity = _request_identity(child.args)
- if child_identity:
- requests.append({"id": child_id, "request": child_identity})
- walk(child.children, child_id)
-
- walk(node.children, node_id)
- return requests
-
-
-def _is_request_node(node: TraceNode) -> bool:
- return (
- "req=" in node.args
- or "request" in node.invoker.casefold()
- or node.file.replace("\\", "/").endswith("/requests.hpp")
- )
-
-
-def _request_anchor(node: TraceNode) -> TraceNode:
- current = node
- while current is not None:
- if _is_request_node(current):
- return current
- current = current.parent
- return node
-
-
-def _request_score(
- terms: tuple[str, ...], candidate: TraceNode, identity: Optional[str] = None
-) -> float:
- if not terms or not _is_request_node(candidate):
- return 0
- candidate_identity = _request_identity(candidate.args)
- if identity and candidate_identity:
- return 1 if identity == candidate_identity else 0
- source = set(terms)
- target = set(_request_terms(candidate.args))
- if not target:
- return 0
- score = len(source & target) / len(source | target)
- return score if not identity else score * 0.9
-
-
-def _node_at(roots: list[TraceNode], node_id: str) -> Optional[TraceNode]:
- try:
- indexes = [int(part) for part in node_id.split(".")]
- if not indexes or any(index < 0 for index in indexes):
- return None
- node = roots[indexes[0]]
- for index in indexes[1:]:
- node = node.children[index]
- return node
- except (ValueError, IndexError):
- return None
-
-
-def _search_nodes(roots: list[TraceNode], query: str, limit: int = 500) -> list[dict]:
- matches: list[dict] = []
- needle = query.casefold()
- request_terms = _request_terms(query) if "req=" in needle else ()
- request_identity = _request_identity(query)
-
- def walk(nodes: list[TraceNode], parent_id: str = "") -> None:
- for index, node in enumerate(nodes):
- if len(matches) >= limit:
- return
- node_id = f"{parent_id}.{index}" if parent_id else str(index)
- haystack = f"{node.invoker} {node.args} {node.file}".casefold()
- request_match = (
- request_terms
- and _request_score(request_terms, node, request_identity) >= 0.6
- )
- if needle in haystack or request_match:
- matches.append(_serialize_node(node, node_id))
- walk(node.children, node_id)
-
- walk(roots)
- return matches
-
-
-def _event_nodes(roots: list[TraceNode], query: str = "") -> list[dict]:
- events: list[dict] = []
- needle = query.casefold()
-
- def walk(nodes: list[TraceNode], parent_id: str = "") -> None:
- for index, node in enumerate(nodes):
- node_id = f"{parent_id}.{index}" if parent_id else str(index)
- if node.is_leaf:
- haystack = f"{node.invoker} {node.args} {node.file}".casefold()
- if not needle or needle in haystack:
- event = _serialize_node(node, node_id)
- event["context"] = node.parent.invoker if node.parent else "Root"
- events.append(event)
- walk(node.children, node_id)
-
- walk(roots)
- events.sort(
- key=lambda event: (
- event["timestamp_ms"] if event["timestamp_ms"] is not None else -1
- )
- )
- return events
-
-
-def _indexed_nodes(
- roots: list[TraceNode],
-) -> list[tuple[TraceNode, str, tuple[str, ...]]]:
- indexed: list[tuple[TraceNode, str, tuple[str, ...]]] = []
-
- def walk(
- nodes: list[TraceNode], parent_id: str = "", path: tuple[str, ...] = ()
- ) -> None:
- for index, node in enumerate(nodes):
- node_id = f"{parent_id}.{index}" if parent_id else str(index)
- node_path = (*path, node.invoker)
- indexed.append((node, node_id, node_path))
- walk(node.children, node_id, node_path)
-
- walk(roots)
- return indexed
-
-
-def _percentile(values: list[int], percentile: int) -> Optional[float]:
- if not values:
- return None
- ordered = sorted(values)
- position = (len(ordered) - 1) * percentile / 100
- lower = int(position)
- upper = min(lower + 1, len(ordered) - 1)
- fraction = position - lower
- return round(ordered[lower] + (ordered[upper] - ordered[lower]) * fraction, 1)
-
-
-def _trace_analysis(roots: list[TraceNode]) -> dict:
- indexed = _indexed_nodes(roots)
- scopes = [(node, node_id, path) for node, node_id, path in indexed if not node.is_leaf]
- durations = [node.duration_ms for node, _, _ in scopes if node.duration_ms is not None]
- slowest = sorted(
- ((node, node_id, path) for node, node_id, path in scopes if node.duration_ms is not None),
- key=lambda item: item[0].duration_ms or 0,
- reverse=True,
- )[:10]
-
- counts = Counter(
- (node.invoker, "event" if node.is_leaf else "scope")
- for node, _, _ in indexed
- )
- frequent = [
- {
- "invoker": invoker,
- "count": count,
- "kind": kind,
- }
- for (invoker, kind), count in counts.most_common(10)
- ]
-
- paths: dict[str, dict[str, int]] = defaultdict(lambda: {"count": 0, "total_ms": 0})
- for node, _, path in scopes:
- if node.duration_ms is None:
- continue
- label = " > ".join(path)
- paths[label]["count"] += 1
- paths[label]["total_ms"] += node.duration_ms
- call_paths = [
- {"path": path, **values}
- for path, values in sorted(
- paths.items(), key=lambda item: item[1]["total_ms"], reverse=True
- )[:10]
- ]
-
- return {
- "percentiles": {
- f"p{percentile}": _percentile(durations, percentile)
- for percentile in (50, 90, 95, 99)
- },
- "slowest": [
- {**_serialize_node(node, node_id), "path": " > ".join(path)}
- for node, node_id, path in slowest
- ],
- "frequent": frequent,
- "call_paths": call_paths,
- }
-
-
-def _export_events(tab: TraceTab, summary: dict, query: str, format_name: str) -> tuple[bytes, str, str]:
- events = _event_nodes(tab.roots, query)
- stem = f"{summary['kind']}-{summary['hostname']}-{summary['tid']}-events"
- if format_name == "json":
- body = json.dumps({"summary": summary, "query": query, "events": events}, indent=2)
- return body.encode("utf-8"), "application/json", f"{stem}.json"
- if format_name == "csv":
- output = io.StringIO()
- writer = csv.writer(output)
- writer.writerow(["timestamp_ms", "event", "details", "context", "source", "line"])
- for event in events:
- writer.writerow([
- event["timestamp_ms"], event["invoker"], event["args"],
- event["context"], event["file"], event["line"],
- ])
- return output.getvalue().encode("utf-8"), "text/csv", f"{stem}.csv"
- if format_name == "html":
- rows = "".join(
- ""
- f"| {html.escape(str(event['timestamp_ms']))} | "
- f"{html.escape(event['invoker'])} | "
- f"{html.escape(event['args'])} | "
- f"{html.escape(event['context'])} | "
- f"{html.escape(event['file'])}:{event['line']} | "
- "
"
- for event in events
- )
- title = html.escape(f"{summary['kind']} / {summary['hostname']} / {summary['tid']}")
- document = f"""
-{title}
-
-{title}
{len(events)} events{f' matching {html.escape(query)}' if query else ''}
-| Time (ms) | Event | Details | Context | Source |
{rows}
"""
- return document.encode("utf-8"), "text/html", f"{stem}.html"
- raise ValueError("Unknown export format")
-
-
-def create_web_server(
- tabs: list[TraceTab], host: str = "127.0.0.1", port: int = 8765
-) -> ThreadingHTTPServer:
- summaries = [_tab_metadata(index, tab) for index, tab in enumerate(tabs)]
-
- class TraceHandler(BaseHTTPRequestHandler):
- def _send_json(self, payload: object, status: int = 200) -> None:
- body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
- self.send_response(status)
- self.send_header("Content-Type", "application/json; charset=utf-8")
- self.send_header("Content-Length", str(len(body)))
- self.send_header("Cache-Control", "no-store")
- self.end_headers()
- self.wfile.write(body)
-
- def _send_asset(self, name: str) -> None:
- path = _ASSET_DIR / name
- try:
- body = path.read_bytes()
- except OSError:
- self.send_error(404)
- return
- content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
- self.send_response(200)
- self.send_header("Content-Type", f"{content_type}; charset=utf-8")
- self.send_header("Content-Length", str(len(body)))
- self.send_header("Cache-Control", "no-store")
- self.end_headers()
- self.wfile.write(body)
-
- def _send_download(self, body: bytes, content_type: str, filename: str) -> None:
- self.send_response(200)
- self.send_header("Content-Type", f"{content_type}; charset=utf-8")
- self.send_header("Content-Disposition", f'attachment; filename="{filename}"')
- self.send_header("Content-Length", str(len(body)))
- self.end_headers()
- self.wfile.write(body)
-
- def do_GET(self) -> None:
- parsed = urlparse(self.path)
- if parsed.path == "/api/overview":
- self._send_json({"tabs": summaries})
- return
- if parsed.path == "/api/trace":
- try:
- index = int(parse_qs(parsed.query).get("id", [""])[0])
- tab = tabs[index]
- if index < 0:
- raise IndexError
- except (ValueError, IndexError):
- self._send_json({"error": "Unknown trace"}, status=404)
- return
- roots = tab.roots
- summary = _tab_summary(index, tab)
- summaries[index] = summary
- self._send_json({
- "summary": summary,
- "roots": _serialize_level(roots),
- "stats": compute_stats(roots),
- "analysis": _trace_analysis(roots),
- })
- return
- if parsed.path == "/api/children":
- query = parse_qs(parsed.query)
- try:
- index = int(query.get("trace", [""])[0])
- tab = tabs[index]
- if index < 0:
- raise IndexError
- except (ValueError, IndexError):
- self._send_json({"error": "Unknown trace"}, status=404)
- return
- node_id = query.get("node", [""])[0]
- node = _node_at(tab.roots, node_id)
- if node is None:
- self._send_json({"error": "Unknown node"}, status=404)
- return
- self._send_json({"nodes": _serialize_level(node.children, node_id)})
- return
- if parsed.path == "/api/search":
- query = parse_qs(parsed.query)
- try:
- index = int(query.get("trace", [""])[0])
- tab = tabs[index]
- if index < 0:
- raise IndexError
- except (ValueError, IndexError):
- self._send_json({"error": "Unknown trace"}, status=404)
- return
- term = query.get("q", [""])[0].strip()
- matches = _search_nodes(tab.roots, term) if term else []
- self._send_json({"matches": matches, "limited": len(matches) >= 500})
- return
- if parsed.path == "/api/events":
- query = parse_qs(parsed.query)
- try:
- index = int(query.get("trace", [""])[0])
- offset = max(0, int(query.get("offset", ["0"])[0]))
- limit = min(1000, max(1, int(query.get("limit", ["250"])[0])))
- tab = tabs[index]
- if index < 0:
- raise IndexError
- except (ValueError, IndexError):
- self._send_json({"error": "Invalid event query"}, status=404)
- return
- events = _event_nodes(tab.roots, query.get("q", [""])[0].strip())
- self._send_json({
- "events": events[offset:offset + limit],
- "total": len(events),
- "offset": offset,
- })
- return
- if parsed.path == "/api/correlate":
- query = parse_qs(parsed.query)
- try:
- source_index = int(query.get("source", [""])[0])
- indexes = [
- int(value) for value in query.get("traces", [""])[0].split(",")
- if value
- ]
- if (
- source_index < 0
- or source_index >= len(tabs)
- or not indexes
- or any(index < 0 or index >= len(tabs) for index in indexes)
- ):
- raise ValueError
- except ValueError:
- self._send_json({"error": "Invalid correlation query"}, status=400)
- return
- source_node = _node_at(
- tabs[source_index].roots, query.get("node", [""])[0]
- )
- if source_node is None:
- self._send_json({"error": "Unknown source node"}, status=404)
- return
- source_node = _request_anchor(source_node)
- terms = _request_terms(source_node.args)
- identity = _request_identity(source_node.args)
- request_mode = _is_request_node(source_node) and bool(terms)
-
- source_equivalents = [
- node
- for node, _, _ in _indexed_nodes(tabs[source_index].roots)
- if _is_request_node(node)
- and (
- _request_identity(node.args) == identity
- if identity
- else set(_request_terms(node.args)) == set(terms)
- )
- ]
- source_ordinal = next(
- (
- index
- for index, node in enumerate(source_equivalents)
- if node is source_node
- ),
- 0,
- )
- matches = []
- for index in indexes:
- if index == source_index:
- continue
- indexed = _indexed_nodes(tabs[index].roots)
- if request_mode:
- scored = [
- (_request_score(terms, node, identity), node, node_id)
- for node, node_id, _ in indexed
- if _is_request_node(node)
- ]
- best_score = max((item[0] for item in scored), default=0)
- candidates = [
- (node, node_id)
- for score, node, node_id in scored
- if score == best_score and score >= 0.6
- ]
- else:
- best_score = 1
- candidates = [
- (node, node_id)
- for node, node_id, _ in indexed
- if node.invoker == source_node.invoker
- and node.timestamp is not None
- ]
- if not candidates:
- continue
- if request_mode:
- node, node_id = candidates[min(source_ordinal, len(candidates) - 1)]
- else:
- node, node_id = min(
- candidates,
- key=lambda item: abs(
- (item[0].timestamp or 0) - (source_node.timestamp or 0)
- ),
- )
- matches.append({
- "trace": index,
- "delta_ms": (
- None
- if request_mode
- else (node.timestamp or 0) - (source_node.timestamp or 0)
- ),
- "score": round(best_score, 2),
- "node": _serialize_node(node, node_id),
- })
- self._send_json({
- "mode": "request" if request_mode else "invoker",
- "request": identity,
- "terms": list(dict.fromkeys(terms)),
- "matches": matches,
- })
- return
- if parsed.path == "/api/export":
- query = parse_qs(parsed.query)
- try:
- index = int(query.get("trace", [""])[0])
- tab = tabs[index]
- if index < 0:
- raise IndexError
- body, content_type, filename = _export_events(
- tab,
- _tab_summary(index, tab),
- query.get("q", [""])[0].strip(),
- query.get("format", ["json"])[0],
- )
- except (ValueError, IndexError):
- self._send_json({"error": "Invalid export query"}, status=400)
- return
- self._send_download(body, content_type, filename)
- return
- asset = _ASSETS.get(parsed.path)
- if asset is not None:
- self._send_asset(asset)
- return
- self.send_error(404)
-
- def log_message(self, format: str, *args: object) -> None:
- return
-
- return ThreadingHTTPServer((host, port), TraceHandler)
-
-
-def run_web(
- tabs: list[TraceTab], host: str = "127.0.0.1", port: int = 8765
-) -> None:
- server = create_web_server(tabs, host, port)
- bound_host, bound_port = server.server_address[:2]
- browser_host = "127.0.0.1" if bound_host in ("0.0.0.0", "::") else bound_host
- print(f"CALF web explorer: http://{browser_host}:{bound_port}")
- print("Press Ctrl-C to stop.")
- try:
- server.serve_forever()
- except KeyboardInterrupt:
- pass
- finally:
- server.server_close()
diff --git a/profiler/web/app.js b/profiler/web/app.js
deleted file mode 100644
index 0028cd3..0000000
--- a/profiler/web/app.js
+++ /dev/null
@@ -1,648 +0,0 @@
-const workspace = { tabs: [], columns: [], nextColumnId: 1 };
-const $ = (selector, root = document) => root.querySelector(selector);
-const columnThemes = [
- ["#315e57", "#e7f0ee"], ["#35679a", "#e8f0f8"], ["#76539a", "#f0eaf6"],
- ["#a56324", "#f8eee3"], ["#a64458", "#f8e9ed"], ["#36775a", "#e7f3ec"],
-];
-let activeConnection = null;
-
-function correlationOverlay() {
- let overlay = $("#correlation-overlay");
- if (overlay) return overlay;
- overlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- overlay.id = "correlation-overlay";
- overlay.innerHTML = '';
- const dismiss = event => {
- event.preventDefault(); event.stopPropagation(); hideCorrelationArrow();
- };
- $(".correlation-line", overlay).addEventListener("click", dismiss);
- $(".correlation-hit", overlay).addEventListener("click", dismiss);
- document.body.append(overlay);
- return overlay;
-}
-
-function hideCorrelationArrow() {
- activeConnection?.observer?.disconnect();
- activeConnection?.hiddenButtons?.forEach(button => button.hidden = false);
- document.querySelectorAll(".connection-close").forEach(button => button.remove());
- document.querySelectorAll(".correlation-jump:not(.connection-close)").forEach(button => button.hidden = false);
- activeConnection = null;
- const overlay = $("#correlation-overlay");
- if (overlay) overlay.remove();
-}
-
-function drawCorrelationArrow() {
- if (!activeConnection) return;
- const { sourceRow, targetRow } = activeConnection;
- if (!rowIsVisible(sourceRow) || !rowIsVisible(targetRow)) {
- hideCorrelationArrow(); return;
- }
- const source = sourceRow.getBoundingClientRect();
- const target = targetRow.getBoundingClientRect();
- const leftToRight = source.left < target.left;
- const x1 = leftToRight ? source.right : source.left;
- const x2 = leftToRight ? target.left : target.right;
- const y1 = source.top + source.height / 2;
- const y2 = target.top + target.height / 2;
- const bend = Math.max(50, Math.abs(x2 - x1) * 0.45);
- const direction = leftToRight ? 1 : -1;
- const path = `M ${x1} ${y1} C ${x1 + bend * direction} ${y1}, ${x2 - bend * direction} ${y2}, ${x2} ${y2}`;
- const overlay = correlationOverlay();
- const gradient = $("#correlation-gradient", overlay);
- gradient.setAttribute("x1", x1); gradient.setAttribute("y1", y1);
- gradient.setAttribute("x2", x2); gradient.setAttribute("y2", y2);
- const sourceColor = getComputedStyle(sourceRow.closest(".trace-column")).getPropertyValue("--accent").trim();
- const targetColor = getComputedStyle(targetRow.closest(".trace-column")).getPropertyValue("--accent").trim();
- $("stop:first-child", gradient).setAttribute("stop-color", sourceColor);
- $("stop:last-child", gradient).setAttribute("stop-color", targetColor);
- $("#correlation-arrowhead path", overlay).setAttribute("fill", targetColor);
- $(".correlation-line", overlay).setAttribute("d", path);
- $(".correlation-hit", overlay).setAttribute("d", path);
- overlay.hidden = false;
-}
-
-function rowIsVisible(row) {
- if (!row.isConnected || !row.offsetParent) return false;
- const rect = row.getBoundingClientRect();
- if (rect.bottom <= 0 || rect.top >= window.innerHeight || rect.right <= 0 || rect.left >= window.innerWidth) return false;
- const scroller = row.closest(".tree, .event-table-wrap");
- if (!scroller) return true;
- const clip = scroller.getBoundingClientRect();
- return rect.bottom > clip.top && rect.top < clip.bottom && rect.right > clip.left && rect.left < clip.right;
-}
-
-function addConnectionClose(row) {
- const host = row.matches("tr") ? row.lastElementChild : row;
- const button = document.createElement("button");
- button.className = "correlation-jump connection-close";
- button.textContent = "Close connection";
- button.addEventListener("click", event => {
- event.preventDefault(); event.stopPropagation(); hideCorrelationArrow();
- });
- host.append(button);
-}
-
-function showCorrelationArrow(sourceRow, targetRow, source, target, sourceTrace, targetTrace) {
- const observer = new IntersectionObserver(entries => {
- if (entries.some(entry => !entry.isIntersecting)) hideCorrelationArrow();
- }, { threshold: 0.01 });
- observer.observe(sourceRow); observer.observe(targetRow);
- const hiddenButtons = [
- ...source.root.querySelectorAll(`.correlation-jump[data-trace-id="${targetTrace}"]`),
- ...target.root.querySelectorAll(`.correlation-jump[data-trace-id="${sourceTrace}"]`),
- ];
- hiddenButtons.forEach(button => button.hidden = true);
- activeConnection = { sourceRow, targetRow, source, target, sourceTrace, targetTrace, observer, hiddenButtons };
- addConnectionClose(sourceRow); addConnectionClose(targetRow);
- requestAnimationFrame(() => requestAnimationFrame(drawCorrelationArrow));
-}
-
-function ms(value) {
- if (value === null || value === undefined) return "--";
- return value >= 1000 ? `${(value / 1000).toFixed(2)}s` : `${value}ms`;
-}
-
-function durationClass(value) {
- if (value === null || value === undefined) return "";
- if (value < 10) return "time-fast";
- if (value < 100) return "time-warm";
- if (value < 1000) return "time-slow";
- return "time-hot";
-}
-
-function metric(label, value) {
- const item = document.createElement("div"); item.className = "metric";
- const small = document.createElement("small"); small.textContent = label;
- const strong = document.createElement("strong"); strong.textContent = value;
- item.append(small, strong); return item;
-}
-
-function createColumn(kind, initialTraceId = null, pendingNode = null) {
- const column = {
- id: workspace.nextColumnId++, kind, trace: null, nodes: new Map(), matches: [],
- match: -1, searchVersion: 0, loadVersion: 0, view: "tree", eventOffset: 0,
- eventTotal: 0, correlationVersion: 0, selectedNode: null, pendingNode,
- requestLinks: new Set(), root: document.createElement("section"),
- };
- column.root.className = "trace-column";
- const theme = columnThemes[(column.id - 1) % columnThemes.length];
- column.root.style.setProperty("--accent", theme[0]);
- column.root.style.setProperty("--accent-soft", theme[1]);
- column.root.innerHTML = `
-
-
- No trace selected
Loading...
-
-
-
Call treeevent fast hot
No trace nodes match this search.
-
Event log0 events
| Time | Event | Details | Context | Source |
|---|
No events match this search.
-
-
`;
-
- const directories = [...new Set(workspace.tabs.map(tab => tab.kind))];
- const select = $(".directory-select", column.root);
- directories.forEach(directory => {
- const option = document.createElement("option"); option.value = directory; option.textContent = directory; select.append(option);
- });
- select.value = kind;
- select.addEventListener("change", () => setDirectory(column, select.value));
- $(".remove-column", column.root).addEventListener("click", () => removeColumn(column));
- $(".show-tree", column.root).addEventListener("click", () => setView(column, "tree"));
- $(".show-events", column.root).addEventListener("click", () => setView(column, "events"));
- $(".load-events", column.root).addEventListener("click", () => loadEvents(column));
- $(".previous-match", column.root).addEventListener("click", () => stepMatch(column, -1));
- $(".next-match", column.root).addEventListener("click", () => stepMatch(column, 1));
- $(".expand-all", column.root).addEventListener("click", () => expandLoadedLevel(column));
- $(".collapse-all", column.root).addEventListener("click", () => column.root.querySelectorAll(".tree-node").forEach(collapseNode));
- $(".show-insights", column.root).addEventListener("click", () => showInsights(column));
- column.root.querySelectorAll(".export-menu [data-format]").forEach(button => button.addEventListener("click", () => exportEvents(column, button.dataset.format)));
- $(".copy-link", column.root).addEventListener("click", event => copyLink(column, event.currentTarget));
- const searchInput = $(".search input", column.root);
- searchInput.addEventListener("input", () => search(column));
- searchInput.addEventListener("keydown", event => {
- if (event.key === "Enter" && column.view === "tree") stepMatch(column, event.shiftKey ? -1 : 1);
- });
-
- workspace.columns.push(column);
- $("#columns").append(column.root);
- updateRemoveButtons();
- column.ready = setDirectory(column, kind, initialTraceId);
- return column;
-}
-
-function removeColumn(column) {
- if (workspace.columns.length === 1) return;
- hideCorrelationArrow();
- workspace.columns = workspace.columns.filter(item => item !== column);
- column.root.remove();
- updateRemoveButtons();
-}
-
-function updateRemoveButtons() {
- const disabled = workspace.columns.length === 1;
- workspace.columns.forEach(column => $(".remove-column", column.root).hidden = disabled);
-}
-
-function setDirectory(column, kind, initialTraceId = null) {
- hideCorrelationArrow();
- column.kind = kind;
- $(".directory-select", column.root).value = kind;
- const tabs = workspace.tabs.filter(tab => tab.kind === kind);
- const nav = $(".thread-tabs", column.root); nav.replaceChildren();
- tabs.forEach(tab => {
- const button = document.createElement("button"); button.dataset.id = tab.id;
- button.textContent = `${tab.hostname} / ${tab.tid}`;
- button.title = tab.file;
- button.addEventListener("click", () => loadTrace(column, tab.id));
- nav.append(button);
- });
- const selected = tabs.find(tab => tab.id === initialTraceId) || tabs[0];
- return selected ? loadTrace(column, selected.id) : Promise.resolve();
-}
-
-function registerNodes(column, nodes, parent = null) {
- for (const node of nodes) {
- node.parent = parent; node.children = null; column.nodes.set(node.id, node);
- }
-}
-
-async function loadTrace(column, id) {
- hideCorrelationArrow();
- const version = ++column.loadVersion;
- column.root.querySelectorAll(".thread-tabs button").forEach(item => item.classList.toggle("active", Number(item.dataset.id) === id));
- $(".trace-summary h1", column.root).textContent = "Loading...";
- const response = await fetch(`/api/trace?id=${id}`);
- const trace = await response.json();
- if (version !== column.loadVersion) return;
- column.trace = trace; column.nodes.clear(); registerNodes(column, trace.roots);
- column.requestLinks.clear();
- const summary = trace.summary;
- $(".trace-path", column.root).textContent = `${summary.hostname} / ${summary.kind}`;
- $(".trace-summary h1", column.root).textContent = `Thread ${summary.tid}`;
- $(".metrics", column.root).replaceChildren(
- metric("Nodes", summary.nodes.toLocaleString()), metric("Scopes", summary.scopes.toLocaleString()),
- metric("Events", summary.events.toLocaleString()), metric("Window", ms(summary.window_ms)),
- );
- const input = $(".search input", column.root); input.value = "";
- column.matches = []; column.match = -1; column.eventOffset = 0; column.eventTotal = 0;
- $(".match-count", column.root).textContent = "";
- renderTree(column); showEmptyInspector(column);
- if (column.view === "events") loadEvents(column, true);
- if (column.pendingNode) {
- const nodeId = column.pendingNode; column.pendingNode = null;
- await revealNodeId(column, nodeId);
- }
-}
-
-function createNode(column, node) {
- const wrapper = document.createElement("div"); wrapper.className = `tree-node ${node.is_event ? "event" : "scope"}`; wrapper.dataset.nodeId = node.id;
- const row = document.createElement("div"); row.className = "node-row"; row.style.paddingLeft = `${8 + node.depth * 16}px`; row.setAttribute("role", "treeitem"); row.tabIndex = -1;
- const toggle = document.createElement(node.child_count ? "button" : "span"); toggle.className = node.child_count ? "toggle" : "toggle-spacer";
- if (node.child_count) { toggle.type = "button"; toggle.textContent = "+"; toggle.setAttribute("aria-label", `Expand ${node.invoker}`); toggle.setAttribute("aria-expanded", "false"); }
- const name = document.createElement("span"); name.className = "node-name"; name.textContent = node.invoker;
- const time = document.createElement("span"); time.className = `node-time ${durationClass(node.duration_ms)}`; time.textContent = node.is_event ? `@${ms(node.timestamp_ms)}` : ms(node.duration_ms);
- const args = document.createElement("span"); args.className = "node-args"; args.textContent = node.args ? `| ${node.args}` : "";
- row.dataset.invoker = node.invoker; row.dataset.nodeId = node.id;
- row.append(toggle, name, time, args); wrapper.append(row);
- row.addEventListener("click", () => selectNode(column, node, row));
- if (node.child_count) toggle.addEventListener("click", event => { event.stopPropagation(); toggleNode(column, wrapper, node); });
- attachRequestJumps(column, node, row);
- (node.linked_requests || []).filter(request => request.id !== node.id).forEach(request => addSyncJump(row, column, request));
- return wrapper;
-}
-
-async function expandNode(column, wrapper, node) {
- let children = $(":scope > .children", wrapper);
- const toggle = $(":scope > .node-row > .toggle", wrapper);
- if (node.children === null) {
- if (node.loading) return;
- node.loading = true; toggle.textContent = "...";
- try {
- const traceId = column.trace.summary.id;
- const response = await fetch(`/api/children?trace=${traceId}&node=${encodeURIComponent(node.id)}`);
- if (!response.ok) throw new Error("Unable to load child traces");
- const payload = await response.json();
- if (!column.trace || column.trace.summary.id !== traceId) return;
- node.children = payload.nodes; registerNodes(column, node.children, node);
- } catch (error) { toggle.textContent = "+"; console.error(error); return; }
- finally { node.loading = false; }
- }
- if (!children) {
- children = document.createElement("div"); children.className = "children";
- node.children.forEach(child => children.append(createNode(column, child))); wrapper.append(children);
- }
- children.hidden = false; toggle.textContent = "-"; toggle.setAttribute("aria-expanded", "true");
-}
-
-function collapseNode(wrapper) {
- hideCorrelationArrow();
- const children = $(":scope > .children", wrapper); if (!children) return;
- children.hidden = true; const toggle = $(":scope > .node-row > .toggle", wrapper);
- toggle.textContent = "+"; toggle.setAttribute("aria-expanded", "false");
-}
-
-async function toggleNode(column, wrapper, node) {
- const children = $(":scope > .children", wrapper);
- if (!children || children.hidden) await expandNode(column, wrapper, node); else collapseNode(wrapper);
-}
-
-function renderTree(column) {
- const tree = $(".tree", column.root); tree.replaceChildren();
- column.trace.roots.forEach(root => tree.append(createNode(column, root)));
- $(".tree-empty", column.root).hidden = true;
-}
-
-function selectNode(column, node, row) {
- column.root.querySelectorAll(".node-row.selected, .event-table tr.selected").forEach(item => item.classList.remove("selected"));
- row.classList.add("selected");
- column.selectedNode = node;
- const inspector = $(".inspector", column.root); inspector.replaceChildren();
- const label = document.createElement("div"); label.className = "panel-label"; label.textContent = "Inspector";
- const body = document.createElement("div"); body.className = "inspect-body";
- body.innerHTML = ``;
- $(".inspect-kind", body).textContent = node.is_event ? "Event" : `Scope / depth ${node.depth}`;
- $("h2", body).textContent = node.invoker;
- const values = [["Source", `${node.file}:${node.line}`], ["Duration", ms(node.duration_ms)], ["Timestamp", ms(node.timestamp_ms)], ["Enter", ms(node.enter_ms)], ["Exit", ms(node.exit_ms)], ["Children", node.child_count]];
- const grid = $(".inspect-grid", body);
- values.forEach(([key, value]) => { const cell = document.createElement("div"); const small = document.createElement("small"); small.textContent = key; const span = document.createElement("span"); span.textContent = value; cell.append(small, span); grid.append(cell); });
- $(".inspect-args", body).textContent = node.args || "No arguments"; inspector.append(label, body);
- correlateSelection(column, node, inspector);
-}
-
-function showEmptyInspector(column) {
- $(".inspector", column.root).innerHTML = 'Inspector
Select a trace row to inspect its timing and source.
';
-}
-
-function createEventRow(column, event) {
- const row = document.createElement("tr");
- row.dataset.invoker = event.invoker;
- row.dataset.nodeId = event.id;
- [ms(event.timestamp_ms), event.invoker, event.args || "--", event.context, `${event.file}:${event.line}`].forEach((value, index) => {
- const cell = document.createElement("td"); cell.textContent = value;
- if (index === 0) cell.className = "event-time"; if (index === 1) cell.className = "event-name"; row.append(cell);
- });
- row.addEventListener("click", () => selectNode(column, event, row));
- attachRequestJumps(column, event, row);
- return row;
-}
-
-async function loadEvents(column, reset = false) {
- if (!column.trace) return;
- if (reset) { column.eventOffset = 0; $(".event-table tbody", column.root).replaceChildren(); }
- const version = ++column.searchVersion; const traceId = column.trace.summary.id;
- const query = $(".search input", column.root).value.trim();
- $(".event-count", column.root).textContent = "Loading...";
- const response = await fetch(`/api/events?trace=${traceId}&offset=${column.eventOffset}&limit=250&q=${encodeURIComponent(query)}`);
- if (version !== column.searchVersion || !column.trace || column.trace.summary.id !== traceId) return;
- const payload = await response.json(); const body = $(".event-table tbody", column.root);
- payload.events.forEach(event => body.append(createEventRow(column, event)));
- column.eventOffset += payload.events.length; column.eventTotal = payload.total;
- $(".event-count", column.root).textContent = `${column.eventOffset.toLocaleString()} of ${column.eventTotal.toLocaleString()}`;
- $(".events-empty", column.root).hidden = column.eventTotal > 0;
- $(".load-events", column.root).hidden = column.eventOffset >= column.eventTotal;
- $(".match-count", column.root).textContent = query ? `${column.eventTotal.toLocaleString()} found` : "";
-}
-
-function setView(column, view) {
- hideCorrelationArrow();
- column.view = view; const events = view === "events";
- $(".tree-panel", column.root).hidden = events; $(".events-panel", column.root).hidden = !events;
- $(".tree-actions", column.root).hidden = events; $(".previous-match", column.root).hidden = events; $(".next-match", column.root).hidden = events;
- $(".show-tree", column.root).classList.toggle("active", !events); $(".show-tree", column.root).setAttribute("aria-pressed", String(!events));
- $(".show-events", column.root).classList.toggle("active", events); $(".show-events", column.root).setAttribute("aria-pressed", String(events));
- $(".search input", column.root).placeholder = events ? "Filter events, details, or source" : "Invoker, arguments, or source";
- $(".match-count", column.root).textContent = "";
- if (events) loadEvents(column, true); else search(column);
-}
-
-async function revealNodeId(column, nodeId, behavior = "smooth") {
- setView(column, "tree");
- const parts = nodeId.split(".");
- for (let length = 1; length < parts.length; length += 1) {
- const ancestorId = parts.slice(0, length).join(".");
- const ancestor = column.nodes.get(ancestorId);
- const wrapper = column.root.querySelector(`[data-node-id="${ancestorId}"]`);
- if (!ancestor || !wrapper) return null;
- await expandNode(column, wrapper, ancestor);
- }
- const node = column.nodes.get(nodeId);
- const wrapper = column.root.querySelector(`[data-node-id="${nodeId}"]`);
- if (!node || !wrapper) return null;
- const row = $(":scope > .node-row", wrapper);
- selectNode(column, node, row);
- row.focus({ preventScroll: true });
- const tree = $(".tree", column.root);
- const viewport = tree.getBoundingClientRect();
- const bounds = row.getBoundingClientRect();
- const margin = 10;
- let delta = 0;
- if (bounds.top < viewport.top + margin) delta = bounds.top - viewport.top - margin;
- else if (bounds.bottom > viewport.bottom - margin) delta = bounds.bottom - viewport.bottom + margin;
- if (delta) tree.scrollTo({ top: tree.scrollTop + delta, behavior });
- return row;
-}
-
-async function revealMatch(column) {
- if (!column.matches.length) return;
- const match = column.matches[column.match]; const parts = match.id.split(".");
- for (let length = 1; length < parts.length; length += 1) {
- const ancestorId = parts.slice(0, length).join("."); const ancestor = column.nodes.get(ancestorId);
- const wrapper = column.root.querySelector(`[data-node-id="${ancestorId}"]`);
- if (!ancestor || !wrapper) return; await expandNode(column, wrapper, ancestor);
- }
- const node = column.nodes.get(match.id) || match;
- const wrapper = column.root.querySelector(`[data-node-id="${node.id}"]`); if (!wrapper) return;
- const row = $(":scope > .node-row", wrapper); selectNode(column, node, row); row.scrollIntoView({ block: "center", behavior: "smooth" });
- $(".match-count", column.root).textContent = `${column.match + 1}/${column.matches.length}`;
-}
-
-async function search(column) {
- if (!column.trace) return;
- if (column.view === "events") {
- const version = ++column.searchVersion; await new Promise(resolve => setTimeout(resolve, 180));
- if (version === column.searchVersion) loadEvents(column, true); return;
- }
- const query = $(".search input", column.root).value.trim(); const version = ++column.searchVersion;
- if (!query) { column.matches = []; column.match = -1; $(".match-count", column.root).textContent = ""; $(".tree-empty", column.root).hidden = true; return; }
- $(".match-count", column.root).textContent = "..."; await new Promise(resolve => setTimeout(resolve, 180));
- if (version !== column.searchVersion) return;
- const traceId = column.trace.summary.id; const response = await fetch(`/api/search?trace=${traceId}&q=${encodeURIComponent(query)}`);
- if (version !== column.searchVersion || column.trace.summary.id !== traceId) return;
- const payload = await response.json(); column.matches = payload.matches; column.match = column.matches.length ? 0 : -1;
- $(".match-count", column.root).textContent = `0/${column.matches.length}${payload.limited ? "+" : ""}`;
- $(".tree-empty", column.root).hidden = column.matches.length > 0;
- if (column.matches.length) await revealMatch(column);
-}
-
-async function stepMatch(column, direction) {
- if (!column.matches.length) return;
- column.match = (column.match + direction + column.matches.length) % column.matches.length; await revealMatch(column);
-}
-
-async function expandLoadedLevel(column) {
- const currentLevel = [...column.nodes.values()];
- await Promise.all(currentLevel.map(node => { const wrapper = column.root.querySelector(`[data-node-id="${node.id}"]`); return wrapper && node.child_count ? expandNode(column, wrapper, node) : null; }));
-}
-
-function appendTableRow(body, values, onClick = null) {
- const row = document.createElement("tr");
- values.forEach(value => { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); });
- if (onClick) { row.classList.add("clickable"); row.addEventListener("click", onClick); }
- body.append(row);
-}
-
-function showInsights(column) {
- const analysis = column.trace.analysis;
- $("#insights-title").textContent = `${column.kind} / Thread ${column.trace.summary.tid}`;
- const percentiles = $("#percentiles"); percentiles.replaceChildren();
- Object.entries(analysis.percentiles).forEach(([label, value]) => percentiles.append(metric(label, ms(value))));
- const slowest = $("#slowest-body"); slowest.replaceChildren();
- analysis.slowest.forEach(item => appendTableRow(slowest, [item.invoker, item.path, ms(item.duration_ms)], () => { $("#insights-panel").hidden = true; revealNodeId(column, item.id); }));
- const frequent = $("#frequent-body"); frequent.replaceChildren();
- analysis.frequent.forEach(item => appendTableRow(frequent, [item.invoker, item.kind, item.count.toLocaleString()]));
- const paths = $("#paths-body"); paths.replaceChildren();
- analysis.call_paths.forEach(item => appendTableRow(paths, [item.path, item.count.toLocaleString(), ms(item.total_ms)]));
- const stats = $("#stats-body"); stats.replaceChildren();
- column.trace.stats.forEach(item => appendTableRow(stats, [item.invoker, item.count, ms(item.total_ms), ms(item.mean_ms.toFixed(1)), ms(item.median_ms.toFixed(1)), ms(item.max_ms), ms(item.min_ms), ms(item.std_ms.toFixed(1))]));
- $("#insights-panel").hidden = false;
-}
-
-function exportEvents(column, format) {
- const query = $(".search input", column.root).value.trim();
- window.location.href = `/api/export?trace=${column.trace.summary.id}&format=${format}&q=${encodeURIComponent(query)}`;
- $(".export-menu", column.root).open = false;
-}
-
-async function copyLink(column, button) {
- const url = new URL(window.location.href);
- url.search = "";
- url.searchParams.set("trace", column.trace.summary.id);
- if (column.selectedNode) url.searchParams.set("node", column.selectedNode.id);
- try {
- await navigator.clipboard.writeText(url.toString());
- const original = button.textContent; button.textContent = "Copied";
- setTimeout(() => { button.textContent = original; }, 1200);
- } catch (error) {
- window.prompt("Copy trace link", url.toString());
- }
-}
-
-async function openCorrelatedNode(target, traceId, nodeId) {
- if (!target.trace || target.trace.summary.id !== traceId) await loadTrace(target, traceId);
- return revealNodeId(target, nodeId, "auto");
-}
-
-function correlationTarget(source, traceId) {
- const tab = workspace.tabs.find(item => item.id === traceId);
- return workspace.columns.find(item => item !== source && item.kind === tab?.kind) || null;
-}
-
-async function openCorrelationTarget(source, traceId, nodeId) {
- const tab = workspace.tabs.find(item => item.id === traceId);
- if (!tab) return null;
- let target = correlationTarget(source, traceId);
- if (!target) {
- target = createColumn(tab.kind, traceId);
- await target.ready;
- }
- const row = await openCorrelatedNode(target, traceId, nodeId);
- return { target, row };
-}
-
-async function connectCorrelatedNodes(source, sourceNodeId, traceId, targetNodeId, sourceRow = null) {
- hideCorrelationArrow();
- const destinationPromise = openCorrelationTarget(source, traceId, targetNodeId);
- const sourcePromise = sourceRow?.isConnected
- ? Promise.resolve(sourceRow)
- : revealNodeId(source, sourceNodeId, "auto");
- const [destinationResult, sourceResult] = await Promise.allSettled([destinationPromise, sourcePromise]);
- if (destinationResult.status === "rejected") throw destinationResult.reason;
- const destination = destinationResult.value;
- const resolvedSourceRow = sourceResult.status === "fulfilled" ? sourceResult.value : null;
- if (resolvedSourceRow && destination?.row) {
- showCorrelationArrow(resolvedSourceRow, destination.row, source, destination.target, source.trace.summary.id, traceId);
- }
-}
-
-function addCorrelationJump(row, source, sourceNodeId, traceId, node, persistent = false) {
- if (!row) return;
- const host = row.matches("tr") ? row.lastElementChild : row;
- if (host.querySelector(`.correlation-jump[data-trace-id="${traceId}"]`)) return;
- const tab = workspace.tabs.find(item => item.id === traceId);
- const button = document.createElement("button");
- button.className = "correlation-jump";
- button.classList.toggle("persistent", persistent);
- button.dataset.traceId = traceId;
- button.textContent = `Open ${tab?.kind || "match"}`;
- button.title = `Open the associated request in ${tab?.kind || "another component"} / thread ${tab?.tid || "?"}`;
- button.addEventListener("click", async event => {
- event.stopPropagation();
- const label = button.textContent;
- button.textContent = "Opening..."; button.disabled = true;
- try {
- await connectCorrelatedNodes(source, sourceNodeId, traceId, node.id, row);
- } catch (error) {
- console.error(error);
- button.textContent = `Retry ${tab?.kind || "match"}`;
- button.disabled = false;
- return;
- }
- button.textContent = label; button.disabled = false;
- });
- host.append(button);
- if (activeConnection && (
- (source === activeConnection.source && traceId === activeConnection.targetTrace)
- || (source === activeConnection.target && traceId === activeConnection.sourceTrace)
- )) {
- button.hidden = true;
- activeConnection.hiddenButtons.push(button);
- }
-}
-
-function addSyncJump(row, column, request) {
- if (row.querySelector(`.sync-jump[data-node-id="${request.id}"]`)) return;
- const button = document.createElement("button");
- button.className = "sync-jump";
- button.dataset.nodeId = request.id;
- button.textContent = "Go to sync event";
- button.title = `Expand to request ${request.request}`;
- button.addEventListener("click", event => {
- event.preventDefault(); event.stopPropagation();
- revealNodeId(column, request.id, "auto");
- });
- row.append(button);
-}
-
-async function attachRequestJumps(column, node, row) {
- if (!node.request) return;
- const key = `${column.trace.summary.id}:${node.id}`;
- if (column.requestLinks.has(key)) return;
- column.requestLinks.add(key);
- const traceIds = workspace.tabs.map(tab => tab.id);
- const response = await fetch(`/api/correlate?source=${column.trace.summary.id}&node=${encodeURIComponent(node.id)}&traces=${traceIds.join(",")}`);
- const payload = await response.json();
- if (!row.isConnected || !column.trace || column.trace.summary.id !== Number(row.closest(".trace-column")?.querySelector(".thread-tabs .active")?.dataset.id)) return;
- payload.matches.forEach(match => addCorrelationJump(row, column, node.id, match.trace, match.node, true));
-}
-
-async function correlateSelection(column, node, inspector) {
- const version = ++column.correlationVersion;
- workspace.columns.forEach(item => {
- item.root.querySelectorAll(".correlation-jump:not(.persistent)").forEach(button => button.remove());
- item.root.querySelectorAll("[data-invoker]").forEach(row => {
- row.classList.toggle("correlated", item !== column && row.dataset.invoker === node.invoker);
- });
- });
- const traceIds = workspace.tabs.map(tab => tab.id);
- const response = await fetch(`/api/correlate?source=${column.trace.summary.id}&node=${encodeURIComponent(node.id)}&traces=${traceIds.join(",")}`);
- if (version !== column.correlationVersion || column.selectedNode !== node) return;
- const payload = await response.json();
- const matches = payload.matches;
- if (!matches.length) return;
- const section = document.createElement("div"); section.className = "correlations";
- const title = document.createElement("small");
- title.textContent = payload.mode === "request" ? `Request interaction: ${payload.request || payload.terms.join(" / ")}` : "Cross-column matches";
- section.append(title);
- const sourceRow = [...column.root.querySelectorAll(".node-row, .event-table tr")].find(row => row.dataset.nodeId === node.id);
- matches.forEach(match => {
- const targetTab = workspace.tabs.find(tab => tab.id === match.trace);
- const target = correlationTarget(column, match.trace);
- const loadedRow = target?.trace?.summary.id === match.trace
- ? [...target.root.querySelectorAll(".node-row, .event-table tr")].find(row => row.dataset.nodeId === match.node.id)
- : null;
- if (loadedRow) {
- loadedRow.classList.add("correlated");
- addCorrelationJump(loadedRow, target, match.node.id, column.trace.summary.id, node, true);
- }
- addCorrelationJump(sourceRow, column, node.id, match.trace, match.node, true);
- const button = document.createElement("button");
- const relation = payload.mode === "request" ? `${Math.round(match.score * 100)}% parameter match` : `${match.delta_ms >= 0 ? "+" : ""}${ms(match.delta_ms)}`;
- button.textContent = `${targetTab.kind} / thread ${targetTab.tid} ${relation}`;
- button.addEventListener("click", () => connectCorrelatedNodes(column, node.id, match.trace, match.node.id, sourceRow)); section.append(button);
- });
- inspector.append(section);
-}
-
-function addColumn() {
- const directories = [...new Set(workspace.tabs.map(tab => tab.kind))];
- const used = new Set(workspace.columns.map(column => column.kind));
- createColumn(directories.find(directory => !used.has(directory)) || directories[0]);
-}
-
-$("#add-column").addEventListener("click", addColumn);
-$("#close-insights").addEventListener("click", () => $("#insights-panel").hidden = true);
-let globalSearchTimer;
-$("#global-search").addEventListener("input", event => {
- clearTimeout(globalSearchTimer);
- globalSearchTimer = setTimeout(() => {
- workspace.columns.forEach(column => { $(".search input", column.root).value = event.target.value; search(column); });
- }, 120);
-});
-document.addEventListener("keydown", event => {
- if (event.key === "/" && !event.target.matches("input, select")) { event.preventDefault(); $("#global-search").focus(); }
- if (event.key === "Escape") $("#insights-panel").hidden = true;
-});
-window.addEventListener("resize", drawCorrelationArrow);
-document.addEventListener("scroll", () => {
- if (activeConnection) requestAnimationFrame(drawCorrelationArrow);
-}, true);
-
-fetch("/api/overview").then(response => response.json()).then(data => {
- workspace.tabs = data.tabs; $("#columns").replaceChildren();
- const params = new URLSearchParams(window.location.search);
- const linkedTrace = Number(params.get("trace"));
- const linkedTab = params.has("trace") ? workspace.tabs.find(tab => tab.id === linkedTrace) : null;
- if (linkedTab) createColumn(linkedTab.kind, linkedTab.id, params.get("node"));
- else if (workspace.tabs.length) createColumn(workspace.tabs[0].kind);
- else $("#columns").innerHTML = 'No trace logs found.
';
-});
diff --git a/profiler/web/calf.svg b/profiler/web/calf.svg
deleted file mode 100644
index b200c2d..0000000
--- a/profiler/web/calf.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-
diff --git a/profiler/web/index.html b/profiler/web/index.html
deleted file mode 100644
index 52793d8..0000000
--- a/profiler/web/index.html
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
- CALF Trace Explorer
-
-
-
-
-
- Loading trace directories...
-
- Trace analysis
Performance analysis
-
-
-
-
-
-
Invoker statistics
| Invoker | Calls | Total | Mean | Median | Max | Min | Std dev |
|---|
-
-
-
-
-
diff --git a/profiler/web/style.css b/profiler/web/style.css
deleted file mode 100644
index 983dc2d..0000000
--- a/profiler/web/style.css
+++ /dev/null
@@ -1,177 +0,0 @@
-:root {
- --ink: #26313d;
- --strong: #111820;
- --muted: #687583;
- --faint: #929ca7;
- --canvas: #eef0f1;
- --surface: #fff;
- --soft: #f7f8f8;
- --line: #dde1e3;
- --line-strong: #cbd1d4;
- --accent: #315e57;
- --accent-soft: #e7f0ee;
- --green: #27815f;
- --yellow: #a66b13;
- --orange: #c25a2a;
- --red: #bc3f4b;
- --cyan: #197980;
-}
-
-* { box-sizing: border-box; }
-html, body { height: 100%; }
-body { margin: 0; overflow: hidden; color: var(--ink); background: var(--canvas); font: 14px/1.45 Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
-button, input, select { font: inherit; }
-button { border: 0; }
-[hidden] { display: none !important; }
-#correlation-overlay { position: fixed; z-index: 30; inset: 0; width: 100vw; height: 100vh; overflow: visible; pointer-events: none; }
-#correlation-overlay .correlation-line { fill: none; stroke: url(#correlation-gradient); stroke-width: 3; marker-end: url(#correlation-arrowhead); filter: drop-shadow(0 1px 1px rgb(255 255 255 / 90%)); pointer-events: stroke; cursor: pointer; }
-#correlation-overlay .correlation-hit { fill: none; stroke: rgb(0 0 0 / .001); stroke-width: 18; pointer-events: all; cursor: pointer; }
-
-.topbar { height: 56px; display: flex; align-items: center; justify-content: space-between; padding: 0 16px; background: var(--surface); border-bottom: 1px solid var(--line); }
-.brand { display: flex; align-items: center; gap: 9px; }
-.brand-mark { width: 38px; height: 30px; object-fit: contain; }
-.brand strong { display: block; color: var(--strong); font-size: 13px; }
-.brand small { display: block; color: var(--muted); font-size: 10px; }
-.topbar-actions, .health { display: flex; align-items: center; gap: 10px; }
-.health { color: var(--muted); font-size: 11px; }
-.health span { width: 7px; height: 7px; border-radius: 50%; background: #3c9c74; }
-.global-search { width: min(300px, 32vw); display: flex; align-items: center; gap: 7px; height: 32px; padding: 0 9px; border: 1px solid var(--line); border-radius: 5px; }
-.global-search:focus-within { border-color: #8fa9a3; box-shadow: 0 0 0 2px var(--accent-soft); }
-.global-search span { flex: 0 0 auto; color: var(--faint); font-size: 8px; font-weight: 700; text-transform: uppercase; }
-.global-search input { min-width: 0; width: 100%; padding: 0; color: var(--ink); background: transparent; border: 0; outline: 0; font-size: 11px; }
-.add-column { min-height: 32px; padding: 0 12px; color: white; background: var(--accent); border-radius: 5px; font-size: 11px; font-weight: 700; cursor: pointer; }
-.add-column:hover { background: #274e48; }
-
-.workspace { height: calc(100vh - 56px); overflow-x: auto; overflow-y: hidden; padding: 10px; }
-.columns { min-width: 100%; height: 100%; display: flex; gap: 10px; }
-.loading { margin: auto; color: var(--muted); font-size: 12px; }
-.trace-column { min-width: 520px; flex: 1 1 0; height: 100%; display: flex; flex-direction: column; overflow: hidden; background: var(--surface); border: 1px solid var(--line-strong); border-radius: 7px; container-type: inline-size; }
-.column-head { min-height: 43px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 10px; background: var(--soft); border-bottom: 1px solid var(--line); }
-.column-head label { display: flex; align-items: center; gap: 8px; }
-.column-head label span { color: var(--muted); font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; }
-.directory-select { min-width: 135px; height: 29px; padding: 0 28px 0 9px; color: var(--strong); background: var(--surface); border: 1px solid var(--line-strong); border-radius: 4px; font-size: 12px; font-weight: 650; }
-.remove-column { padding: 5px 7px; color: var(--faint); background: transparent; font-size: 10px; cursor: pointer; }
-.remove-column:hover { color: var(--red); }
-
-.thread-tabs { min-height: 37px; display: flex; align-items: end; gap: 2px; overflow-x: auto; padding: 5px 7px 0; border-bottom: 1px solid var(--line); scrollbar-width: thin; }
-.thread-tabs button { flex: 0 0 auto; height: 31px; padding: 0 9px; color: var(--muted); background: transparent; border-bottom: 2px solid transparent; font-size: 10px; cursor: pointer; }
-.thread-tabs button:hover { color: var(--strong); }
-.thread-tabs button.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 700; }
-
-.trace-summary { min-height: 58px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 11px; border-bottom: 1px solid var(--line); }
-.eyebrow { color: var(--accent); font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
-.trace-summary h1 { margin: 2px 0 0; color: var(--strong); font-size: 17px; line-height: 1.2; }
-.metrics { display: flex; align-items: center; overflow-x: auto; }
-.metric { min-width: 72px; padding: 2px 9px; border-left: 1px solid var(--line); }
-.metric small { display: block; color: var(--faint); font-size: 8px; font-weight: 700; text-transform: uppercase; }
-.metric strong { display: block; margin-top: 1px; color: var(--strong); font-size: 11px; }
-
-.toolbar { min-height: 43px; display: flex; align-items: center; gap: 4px; padding: 5px 7px; border-bottom: 1px solid var(--line); }
-.toolbar button, .load-events, .stats-head button { min-height: 29px; padding: 0 8px; color: #53606c; background: var(--surface); border: 1px solid var(--line); border-radius: 4px; font-size: 10px; font-weight: 650; cursor: pointer; }
-.toolbar button:hover, .load-events:hover, .stats-head button:hover { color: var(--accent); border-color: #9eafab; }
-.view-switch { flex: 0 0 auto; display: flex; padding: 2px; background: #e8eaeb; border-radius: 5px; }
-.view-switch button { min-height: 25px; background: transparent; border: 0; }
-.view-switch button.active { color: var(--strong); background: var(--surface); box-shadow: 0 1px 2px rgb(20 30 40 / 10%); }
-.search { min-width: 130px; flex: 1; display: flex; align-items: center; gap: 6px; height: 29px; padding: 0 8px; background: var(--surface); border: 1px solid var(--line); border-radius: 4px; }
-.search:focus-within { border-color: #8fa9a3; box-shadow: 0 0 0 2px var(--accent-soft); }
-.search span { color: var(--faint); font-size: 8px; font-weight: 700; text-transform: uppercase; }
-.search input { min-width: 0; width: 100%; padding: 0; color: var(--ink); background: transparent; border: 0; outline: 0; font-size: 11px; }
-.search input::placeholder { color: #a2aab2; }
-.search kbd { color: var(--muted); white-space: nowrap; font-family: inherit; font-size: 9px; }
-.tree-actions { display: flex; gap: 4px; }
-.export-menu { position: relative; flex: 0 0 auto; }
-.export-menu summary { min-height: 29px; display: grid; place-items: center; padding: 0 8px; color: #53606c; background: var(--surface); border: 1px solid var(--line); border-radius: 4px; font-size: 10px; font-weight: 650; cursor: pointer; list-style: none; }
-.export-menu summary::-webkit-details-marker { display: none; }
-.export-menu[open] summary { color: var(--accent); border-color: #9eafab; }
-.export-menu > div { position: absolute; z-index: 20; top: calc(100% + 4px); right: 0; width: 120px; padding: 4px; background: var(--surface); border: 1px solid var(--line-strong); border-radius: 5px; box-shadow: 0 8px 24px rgb(20 30 35 / 16%); }
-.export-menu > div button { width: 100%; display: block; border: 0; text-align: left; }
-
-.content-grid { min-height: 0; flex: 1; display: grid; grid-template-columns: minmax(0, 1fr) 250px; overflow: hidden; }
-.tree-panel, .events-panel { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; border-right: 1px solid var(--line); }
-.panel-label { min-height: 34px; display: flex; align-items: center; justify-content: space-between; padding: 0 10px; color: var(--muted); background: var(--soft); border-bottom: 1px solid var(--line); font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }
-.legend { display: flex; align-items: center; gap: 3px; color: var(--faint); font-size: 8px; font-weight: 500; text-transform: lowercase; letter-spacing: 0; }
-.legend i { width: 5px; height: 5px; margin-left: 5px; border-radius: 50%; }
-.event-dot { background: var(--cyan); } .fast-dot { background: var(--green); } .hot-dot { background: var(--red); }
-.tree { min-width: 0; min-height: 0; flex: 1; overflow: auto; padding: 4px 5px 18px; scrollbar-gutter: stable; }
-.tree-node { user-select: none; }
-.node-row { min-height: 31px; display: flex; align-items: center; gap: 6px; margin: 1px 0; padding-right: 8px; border-radius: 3px; cursor: pointer; }
-.node-row:hover { background: #f2f4f4; }
-.node-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
-.node-row.selected, .event-table tr.selected { background: var(--accent-soft); box-shadow: inset 2px 0 var(--accent); }
-.node-row.correlated, .event-table tr.correlated { background: #fff4d8; box-shadow: inset 2px 0 #c18a20; }
-.correlation-jump, .sync-jump { flex: 0 0 auto; min-height: 21px; margin-left: auto; padding: 0 6px; color: var(--accent); background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 35%, white); border-radius: 3px; white-space: nowrap; font-size: 8px; font-weight: 700; cursor: pointer; }
-.correlation-jump:hover, .sync-jump:hover { background: color-mix(in srgb, var(--accent-soft) 70%, var(--accent)); }
-.correlation-jump.connection-close { color: #8f2f37; background: #fcedef; border-color: #dda6ac; }
-.correlation-jump.connection-close:hover { background: #f8dce0; }
-.event-table td .correlation-jump { float: right; margin: -3px 0 -3px 6px; }
-.toggle { flex: 0 0 21px; width: 21px; height: 21px; display: grid; place-items: center; padding: 0; color: var(--muted); background: var(--surface); border: 1px solid var(--line-strong); border-radius: 3px; font-size: 14px; line-height: 1; cursor: pointer; }
-.toggle:hover { color: var(--accent); border-color: #9aacaa; }
-.toggle-spacer { flex: 0 0 21px; width: 21px; }
-.node-name { color: #345d75; font: 600 11px ui-monospace, "SFMono-Regular", Consolas, monospace; }
-.event .node-name { color: var(--cyan); }
-.node-time { flex: 0 0 auto; padding: 2px 4px; color: var(--muted); background: #f0f2f3; border-radius: 3px; font: 9px ui-monospace, monospace; }
-.time-fast { color: var(--green); } .time-warm { color: var(--yellow); } .time-slow { color: var(--orange); } .time-hot { color: var(--red); font-weight: 700; }
-.node-args { min-width: 0; overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; font: 9px ui-monospace, monospace; }
-.children[hidden] { display: none; }
-
-.inspector { min-height: 0; overflow: auto; background: var(--soft); }
-.inspector-empty { max-width: 230px; padding: 18px 13px; color: var(--muted); font-size: 11px; line-height: 1.55; }
-.inspect-body { padding: 14px 12px; }
-.inspect-kind { color: var(--accent); font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; }
-.inspect-body h2 { margin: 4px 0 14px; overflow-wrap: anywhere; color: var(--strong); font: 650 13px ui-monospace, monospace; }
-.inspect-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 7px; }
-.inspect-grid small { display: block; margin-bottom: 2px; color: var(--faint); font-size: 7px; font-weight: 700; text-transform: uppercase; }
-.inspect-grid span { color: var(--ink); font: 9px ui-monospace, monospace; }
-.inspect-args { margin-top: 14px; padding: 9px; overflow-wrap: anywhere; color: var(--muted); background: var(--surface); border: 1px solid var(--line); border-radius: 4px; font: 9px/1.5 ui-monospace, monospace; }
-.correlations { margin-top: 14px; padding-top: 11px; border-top: 1px solid var(--line); }
-.correlations small { display: block; margin-bottom: 5px; color: var(--faint); font-size: 7px; font-weight: 700; text-transform: uppercase; }
-.correlations button { width: 100%; margin-top: 3px; padding: 6px 7px; overflow: hidden; color: var(--accent); background: var(--accent-soft); border-radius: 3px; text-align: left; text-overflow: ellipsis; white-space: nowrap; font-size: 9px; cursor: pointer; }
-
-.event-table-wrap { min-height: 0; flex: 1; overflow: auto; }
-table { width: 100%; border-collapse: collapse; color: var(--ink); font-size: 10px; }
-th { position: sticky; top: 0; z-index: 1; color: var(--muted); background: #f4f6f6; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; text-align: left; }
-th, td { padding: 7px 8px; border-bottom: 1px solid var(--line); }
-.event-table { min-width: 650px; table-layout: fixed; }
-.event-table th:nth-child(1) { width: 75px; } .event-table th:nth-child(2) { width: 20%; } .event-table th:nth-child(3) { width: 31%; } .event-table th:nth-child(4) { width: 18%; } .event-table th:nth-child(5) { width: 23%; }
-.event-table tbody tr { cursor: pointer; }
-.event-table tbody tr:hover, .table-wrap tbody tr:hover { background: #f6f8f7; }
-.event-table td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.event-time { color: var(--muted); font-family: ui-monospace, monospace; }
-.event-name { color: var(--cyan); font-family: ui-monospace, monospace; font-weight: 650; }
-.load-events { align-self: center; margin: 6px; }
-.empty { padding: 30px; color: var(--muted); text-align: center; font-size: 10px; }
-
-.insights-panel { position: fixed; z-index: 40; inset: 5vh 3vw; overflow: hidden; padding: 18px; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 0 0 100vmax rgb(25 31 35 / 35%), 0 20px 60px rgb(25 31 35 / 20%); }
-.stats-head { display: flex; align-items: center; justify-content: space-between; }
-.stats-head h2 { margin: 3px 0 14px; color: var(--strong); font-size: 19px; }
-.table-wrap { max-height: calc(100% - 55px); overflow: auto; border: 1px solid var(--line); border-radius: 5px; }
-.table-wrap td:not(:first-child), .table-wrap th:not(:first-child) { text-align: right; }
-.table-wrap tr.clickable { cursor: pointer; }
-.insights-content { height: calc(100% - 55px); display: grid; grid-template-columns: 1fr 1fr; gap: 16px; overflow: auto; }
-.insights-content section { min-width: 0; display: flex; flex-direction: column; }
-.insights-content h3 { margin: 0 0 7px; color: var(--strong); font-size: 11px; }
-.insights-content .table-wrap { min-height: 160px; max-height: none; flex: 1; }
-.insights-content .invoker-stats { grid-column: 1 / -1; min-height: 260px; }
-.percentiles { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--line); border-radius: 5px; }
-.percentiles .metric { padding: 12px; }
-.percentiles .metric:first-child { border-left: 0; }
-
-@container (max-width: 760px) {
- .trace-summary { align-items: flex-start; }
- .metrics { max-width: 55%; }
- .metric { min-width: 60px; padding-inline: 6px; }
- .toolbar { flex-wrap: wrap; }
- .search { order: -1; min-width: calc(100% - 100px); }
- .content-grid { grid-template-columns: 1fr; grid-template-rows: minmax(220px, 1fr) minmax(120px, .38fr); }
- .tree-panel, .events-panel { border-right: 0; border-bottom: 1px solid var(--line); }
-}
-
-@media (max-width: 620px) {
- .global-search { width: 42vw; }
- .global-search span { display: none; }
- .workspace { padding: 7px; }
- .columns { gap: 7px; }
- .trace-column { min-width: calc(100vw - 14px); }
- .insights-panel { inset: 3vh 8px; padding: 12px; }
- .insights-content { grid-template-columns: 1fr; }
-}
diff --git a/pyproject.toml b/pyproject.toml
index 2876d13..9bdcbe3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,6 @@
[build-system]
requires = [
"scikit-build-core[pyproject]==1.0.3",
- "dynamic-metadata==0.5.0",
"pybind11==3.0.4",
"setuptools==80.9.0",
"wheel==0.45.1",
@@ -10,8 +9,8 @@ build-backend = "scikit_build_core.build"
[project]
name = "capio-calf"
-dynamic = ["version", "dependencies"]
-description = "CAPIO Logging Facility and interactive trace inspector"
+dynamic = ["version"]
+description = "CAPIO Logging Facility"
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.10"
@@ -26,20 +25,11 @@ test = ["pytest>=8.0"]
Repository = "https://github.com/High-Performance-IO/CALF"
Issues = "https://github.com/High-Performance-IO/CALF/issues"
-[project.scripts]
-calf = "calf.profiler.__main__:main"
-
[tool.scikit-build]
cmake.version = ">=3.16"
-wheel.exclude = [
- "calf/profiler/build/**",
- "calf/profiler/*.egg-info/**",
- "calf/profiler/__pycache__/**",
-]
[tool.scikit-build.wheel.packages]
"calf" = "bindings/calf"
-"calf/profiler" = "profiler"
[tool.scikit-build.cmake.define]
CALF_BUILD_PYTHON_BINDINGS = true
@@ -51,8 +41,3 @@ field = "version"
input = "CMakeLists.txt"
regex = "(?m)VERSION\\s+(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)"
result = "{major}.{minor}.{patch}"
-
-[[tool.dynamic-metadata]]
-provider = "dynamic_metadata.from_file"
-field = "dependencies"
-path = "requirements.txt"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 783e8e4..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-numpy>=1.24.0
-protobuf>=7.35.1,<8
diff --git a/tests/profiler_loader_tests.py b/tests/profiler_loader_tests.py
deleted file mode 100644
index c57eeb8..0000000
--- a/tests/profiler_loader_tests.py
+++ /dev/null
@@ -1,118 +0,0 @@
-import json
-import sys
-
-from calf.profiler.loader import TraceTab, discover_tabs, load_trace
-from calf.protobuf import calf_trace_pb2
-
-
-def _event(trace, kind, timestamp, name="", args="", track_uuid=1):
- packet = trace.packet.add(timestamp=timestamp * 1_000_000)
- event = packet.track_event
- event.type = kind
- event.track_uuid = track_uuid
- event.name = name
- event.source_location.function_name = name
- if args:
- annotation = event.debug_annotations.add(name="args")
- annotation.string_value = args
-
-
-def test_loads_perfetto_trace_tree_and_truncated_tail(tmp_path):
- trace = calf_trace_pb2.Trace()
- _event(trace, calf_trace_pb2.TrackEvent.TYPE_SLICE_BEGIN, 10, "outer")
- _event(trace, calf_trace_pb2.TrackEvent.TYPE_SLICE_BEGIN, 11, "inner")
- _event(trace, calf_trace_pb2.TrackEvent.TYPE_INSTANT, 12, "inner", "event")
- _event(trace, calf_trace_pb2.TrackEvent.TYPE_SLICE_END, 13)
- _event(trace, calf_trace_pb2.TrackEvent.TYPE_SLICE_END, 14)
-
- path = tmp_path / "syscall_42.perfetto-trace"
- encoded = trace.SerializeToString()
- path.write_bytes(encoded[:-1])
-
- roots = load_trace(str(path))
- assert roots[0]["invoker"] == "outer"
- assert roots[0]["events"][0]["invoker"] == "inner"
- assert roots[0]["events"][0]["events"][0]["args"] == "event"
- assert roots[0]["events"][0]["ts_exit"] == 13
- assert roots[0]["ts_exit"] is None
-
-
-def test_discovers_json_and_perfetto_traces(tmp_path):
- trace_dir = tmp_path / "syscall" / "host"
- trace_dir.mkdir(parents=True)
- (trace_dir / "syscall_1.log").write_text(json.dumps({"invoker": "json", "ts": 1}))
- perfetto_dir = tmp_path / "host"
- perfetto_dir.mkdir()
- (perfetto_dir / "calf_42.perfetto-trace").write_bytes(
- calf_trace_pb2.Trace().SerializeToString()
- )
-
- tabs = discover_tabs(str(tmp_path))
- assert [(tab.kind, tab.tid) for tab in tabs] == [
- ("syscall", "1"),
- ("calf", "process 42 (all threads)"),
- ]
-
-
-def test_perfetto_loader_uses_bounded_reads(tmp_path, monkeypatch):
- trace = calf_trace_pb2.Trace()
- _event(trace, calf_trace_pb2.TrackEvent.TYPE_INSTANT, 10, "event")
- path = tmp_path / "trace.perfetto-trace"
- path.write_bytes(trace.SerializeToString())
-
- real_open = open
-
- class BoundedReader:
- def __init__(self, wrapped):
- self.wrapped = wrapped
-
- def __enter__(self):
- return self
-
- def __exit__(self, *args):
- return self.wrapped.__exit__(*args)
-
- def read(self, size=-1):
- assert size >= 0
- return self.wrapped.read(size)
-
- def bounded_open(*args, **kwargs):
- return BoundedReader(real_open(*args, **kwargs))
-
- monkeypatch.setattr("builtins.open", bounded_open)
- assert load_trace(str(path))[0]["invoker"] == "event"
-
-
-def test_web_server_creation_does_not_load_tabs(tmp_path):
- from calf.profiler.web import create_web_server
-
- path = tmp_path / "trace.perfetto-trace"
- path.write_bytes(calf_trace_pb2.Trace().SerializeToString())
- tab = TraceTab(hostname="host", kind="syscall", path=str(path))
-
- server = create_web_server([tab], port=0)
- try:
- assert tab._roots is None
- finally:
- server.server_close()
-
-
-def test_cli_does_not_load_tabs(tmp_path, monkeypatch):
- from calf.profiler import __main__ as profiler_main
-
- trace_dir = tmp_path / "host"
- trace_dir.mkdir(parents=True)
- (trace_dir / "calf_42.perfetto-trace").write_bytes(
- calf_trace_pb2.Trace().SerializeToString()
- )
- captured_tabs = []
-
- def fake_run_web(tabs, host, port):
- captured_tabs.extend(tabs)
-
- monkeypatch.setattr("calf.profiler.web.run_web", fake_run_web)
- monkeypatch.setattr(sys, "argv", ["calf", str(tmp_path)])
-
- profiler_main.main()
- assert captured_tabs
- assert all(tab._roots is None for tab in captured_tabs)
diff --git a/tests/python_bindings_tests.py b/tests/python_bindings_tests.py
index 7d1fc89..ab83a7e 100644
--- a/tests/python_bindings_tests.py
+++ b/tests/python_bindings_tests.py
@@ -2,8 +2,6 @@
import json
import os
from pathlib import Path
-import subprocess
-import sys
import calf
import pytest
@@ -76,13 +74,5 @@ def test_color_constants_are_exported():
assert calf.CLI_LEVEL_ERROR == "\x1b[1;31m"
-def test_installed_distribution_metadata_and_cli():
+def test_installed_distribution_metadata():
assert calf.__version__ == version("capio-calf")
- result = subprocess.run(
- [sys.executable, "-m", "calf", "--help"],
- check=False,
- capture_output=True,
- text=True,
- )
- assert result.returncode == 0, result.stderr
- assert "Web inspector and profiler" in result.stdout